On 05/25/2015 12:42 PM, Tomas Babej wrote:
> 
> 
> On 05/25/2015 07:30 AM, Jan Cholasta wrote:
>> Dne 22.5.2015 v 12:36 Petr Vobornik napsal(a):
>>> On 05/22/2015 07:08 AM, Jan Cholasta wrote:
>>>> Dne 21.5.2015 v 18:18 Tomas Babej napsal(a):
>>>>>
>>>>>
>>>>> On 05/19/2015 04:07 PM, Tomas Babej wrote:
>>>>>>
>>>>>>
>>>>>> On 05/19/2015 03:59 PM, Martin Kosek wrote:
>>>>>>> On 05/19/2015 03:56 PM, Tomas Babej wrote:
>>>>>>>>
>>>>>>>> On 05/19/2015 03:51 PM, Martin Kosek wrote:
>>>>>>>>> On 05/19/2015 03:49 PM, Ludwig Krispenz wrote:
>>>>>>>>>> On 05/19/2015 03:36 PM, Martin Kosek wrote:
>>>>>>>>>>> On 05/19/2015 03:22 PM, Tomas Babej wrote:
>>>>>>>>>>> ...
>>>>>>>>>>>>> 3) Domain level is just a single integer and it should be
>>>>>>>>>>>>> treated as such,
>>>>>>>>>>>>> there's no need for an LDAPObject plugin and other unnecessary
>>>>>>>>>>>>> complexities.
>>>>>>>>>>>>> The implemetation could be as simple as (from top of my head,
>>>>>>>>>>>>> untested):
>>>>>>>>>>>> That's right, I also considered this approach, but as far as I
>>>>>>>>>>>> know you do
>>>>>>>>>>>> not
>>>>>>>>>>>> get the permission handling for the global DomainLevel entry
>>>>>>>>>>>> otherwise.
>>>>>>>>>>>>
>>>>>>>>>>>> Ludwig, I changed the path for the global entry to
>>>>>>>>>>>> cn=DomainLevel.
>>>>>>>>>>> I know this particular DN was added to the design by Simo, but
>>>>>>>>>>> why do we want
>>>>>>>>>>> to use CamelCase with LDAP object?
>>>>>>>>>>>
>>>>>>>>>>> Wouldn't "cn=Domain Level,cn=ipa,cn=etc,SUFFIX" be a better place
>>>>>>>>>>> for it? This
>>>>>>>>>>> is the last time we can change it, so I am asking now. Then, we
>>>>>>>>>>> will be stuck
>>>>>>>>>>> with this DN forever.
>>>>>>>>>> I don't mind using ""cn=Domain Level" ,
>>>>>>>>>>
>>>>>>>>>> but where does the entry live, here you say
>>>>>>>>>>
>>>>>>>>>> cn=Domain Level,cn=ipa,cn=etc,SUFFIX"
>>>>>>>>>>
>>>>>>>>>> and in the design page it is:
>>>>>>>>>>
>>>>>>>>>> cn=DomainLevel,cn=etc,SUFFIX
>>>>>>>>>>
>>>>>>>>>> The current version of the topology plugin is looking for
>>>>>>>>>>
>>>>>>>>>> cn=DomainLevel,cn=ipa,cn=etc,SUFFIX"
>>>>>>>>>> but I want to change it to do a search on
>>>>>>>>>> objectclass=ipaDomainLevelConfig
>>>>>>>>> I see - we all need to unify the location apparently. I updated the
>>>>>>>>> design page
>>>>>>>>> to use "cn=Domain Level,cn=ipa,cn=etc,SUFFIX". Tomas, please send
>>>>>>>>> the updated
>>>>>>>>> patch set, it should be an extremely simple change :-)
>>>>>>>> I prefer the ipa parent and the space in the name, so I'm glad we
>>>>>>>> could agree
>>>>>>>> on this without much bikeshedding.
>>>>>>>>
>>>>>>>> Updated patch attaced.
>>>>>>>>
>>>>>>>> Tomas
>>>>>>>>
>>>>>>>>
>>>>>>> I still see
>>>>>>>
>>>>>>> +# Create default Domain Level entry if it does not exist
>>>>>>> +dn: cn=DomainLevel,cn=ipa,cn=etc,$SUFFIX
>>>>>>> +default: objectClass: top
>>>>>>> +default: objectClass: nsContainer
>>>>>>> +default: objectClass: ipaDomainLevelConfig
>>>>>>> +default: ipaDomainLevel: 0
>>>>>>>
>>>>>>> ...
>>>>>>
>>>>>> Right, the space eluded me there, thanks for the catch.
>>>>>>
>>>>>> Tomas
>>>>>
>>>>> A new iteration of the patch, including the server-side checks for the
>>>>> installers.
>>>>>
>>>>> Tomas
>>>>
>>>> 1) https://www.redhat.com/archives/freeipa-devel/2015-May/msg00228.html
>>>> - I still don't agree that the plugin should be based on LDAPObject.
>>>
>>> On the other hand, with LDAPObject base, Web UI for this feature is much
>>> more simpler because it can rely on existing conventions.
>>
>> Following this logic, should *everything* be based on LDAPObject,
>> because it would satisfy the convetion? I don't think so. The convetion
>> should not apply here, because domain level is conceptually *not* an
>> object, it is a property. IMHO having a clean API should be preferred
>> over implementation convenience.
>>
> 
> I do not have strong opinions over this. Attached version implements
> a lightweight approach to the domainlevel related commands.
> 
> Tomas
> 
> 
> 

Fixes a slight schema glitch.
From 7930e267f4c513a6b2059a6c99d69155c1452c49 Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Thu, 14 May 2015 10:49:55 +0200
Subject: [PATCH] Add Domain Level feature

https://fedorahosted.org/freeipa/ticket/5018
---
 ACI.txt                                            |   2 +
 API.txt                                            |   9 ++
 install/share/72domainlevels.ldif                  |   6 +
 install/share/Makefile.am                          |   2 +
 install/share/domainlevel.ldif                     |   7 ++
 install/tools/ipa-replica-install                  |  32 ++++-
 install/tools/ipa-server-install                   |  22 +++-
 install/updates/72-domainlevels.update             |  14 +++
 install/updates/Makefile.am                        |   1 +
 ipalib/constants.py                                |   3 +
 ipalib/errors.py                                   |  16 +++
 ipalib/plugins/domainlevel.py                      | 132 +++++++++++++++++++++
 ipaserver/install/dsinstance.py                    |  12 +-
 ipaserver/install/ldapupdate.py                    |   5 +
 .../install/plugins/update_managed_permissions.py  |  11 +-
 15 files changed, 263 insertions(+), 11 deletions(-)
 create mode 100644 install/share/72domainlevels.ldif
 create mode 100644 install/share/domainlevel.ldif
 create mode 100644 install/updates/72-domainlevels.update
 create mode 100644 ipalib/plugins/domainlevel.py

diff --git a/ACI.txt b/ACI.txt
index bf539892910f14ebc3fbee88a72d2b57c0d1327b..1e4eb0f5a75e44a3c56dffe7df1655a9f0a1db87 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -323,6 +323,8 @@ aci: (targetattr = "cn || createtimestamp || dnahostname || dnaportnum || dnarem
 dn: ou=profile,dc=ipa,dc=example
 aci: (targetattr = "attributemap || authenticationmethod || bindtimelimit || cn || createtimestamp || credentiallevel || defaultsearchbase || defaultsearchscope || defaultserverlist || dereferencealiases || entryusn || followreferrals || modifytimestamp || objectclass || objectclassmap || ou || preferredserverlist || profilettl || searchtimelimit || serviceauthenticationmethod || servicecredentiallevel || servicesearchdescriptor")(targetfilter = "(|(objectclass=organizationalUnit)(objectclass=DUAConfigProfile))")(version 3.0;acl "permission:System: Read DUA Profile";allow (compare,read,search) userdn = "ldap:///anyone";;)
 dn: cn=masters,cn=ipa,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "createtimestamp || entryusn || ipadomainlevel || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipadomainlevelconfig)")(version 3.0;acl "permission:System: Read Domain Level";allow (compare,read,search) userdn = "ldap:///all";;)
+dn: cn=masters,cn=ipa,cn=etc,dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || entryusn || ipaconfigstring || modifytimestamp || objectclass")(targetfilter = "(objectclass=nscontainer)")(version 3.0;acl "permission:System: Read IPA Masters";allow (compare,read,search) groupdn = "ldap:///cn=System: Read IPA Masters,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=config
 aci: (targetattr = "cn || createtimestamp || description || entryusn || modifytimestamp || nsds50ruv || nsds5beginreplicarefresh || nsds5debugreplicatimeout || nsds5flags || nsds5replicaabortcleanruv || nsds5replicaautoreferral || nsds5replicabackoffmax || nsds5replicabackoffmin || nsds5replicabinddn || nsds5replicabindmethod || nsds5replicabusywaittime || nsds5replicachangecount || nsds5replicachangessentsincestartup || nsds5replicacleanruv || nsds5replicacleanruvnotified || nsds5replicacredentials || nsds5replicaenabled || nsds5replicahost || nsds5replicaid || nsds5replicalastinitend || nsds5replicalastinitstart || nsds5replicalastinitstatus || nsds5replicalastupdateend || nsds5replicalastupdatestart || nsds5replicalastupdatestatus || nsds5replicalegacyconsumer || nsds5replicaname || nsds5replicaport || nsds5replicaprotocoltimeout || nsds5replicapurgedelay || nsds5replicareferral || nsds5replicaroot || nsds5replicasessionpausetime || nsds5replicastripattrs || nsds5replicatedattributelist || nsds5replicatedattributelisttotal || nsds5replicatimeout || nsds5replicatombstonepurgeinterval || nsds5replicatransportinfo || nsds5replicatype || nsds5replicaupdateinprogress || nsds5replicaupdateschedule || nsds5task || nsds7directoryreplicasubtree || nsds7dirsynccookie || nsds7newwingroupsyncenabled || nsds7newwinusersyncenabled || nsds7windowsdomain || nsds7windowsreplicasubtree || nsruvreplicalastmodified || nsstate || objectclass || onewaysync || winsyncdirectoryfilter || winsyncinterval || winsyncmoveaction || winsyncsubtreepair || winsyncwindowsfilter")(targetfilter = "(|(objectclass=nsds5Replica)(objectclass=nsds5replicationagreement)(objectclass=nsDSWindowsReplicationAgreement)(objectClass=nsMappingTree))")(version 3.0;acl "permission:System: Read Replication Agreements";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Replication Agreements,cn=permissions,cn=pbac,dc=ipa,dc=example";)
diff --git a/API.txt b/API.txt
index 0808f3c64595495c8a9e60da5cbd689d5cdc6224..d707aef6b44955e65ee189acc373a9a8e44003f2 100644
--- a/API.txt
+++ b/API.txt
@@ -1283,6 +1283,15 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: domainlevel_set
+args: 1,1,1
+arg: Int('ipadomainlevel', cli_name='level', minvalue=0)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'int'>, None)
+command: domainlevel_show
+args: 0,1,1
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'int'>, None)
 command: env
 args: 1,3,4
 arg: Str('variables*')
diff --git a/install/share/72domainlevels.ldif b/install/share/72domainlevels.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..184e1cb220e80395bbe6ea063df9957ebde752ce
--- /dev/null
+++ b/install/share/72domainlevels.ldif
@@ -0,0 +1,6 @@
+dn: cn=schema
+attributeTypes: (2.16.840.1.113730.3.8.19.2.1 NAME 'ipaDomainLevel' DESC 'Domain Level value' EQUALITY numericStringMatch ORDERING numericStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 SINGLE-VALUE X-ORIGIN 'IPA v4')
+attributeTypes: (2.16.840.1.113730.3.8.19.2.2 NAME 'ipaMinDomainLevel' DESC 'Minimal supported Domain Level value' EQUALITY numericStringMatch ORDERING numericStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 SINGLE-VALUE X-ORIGIN 'IPA v4')
+attributeTypes: (2.16.840.1.113730.3.8.19.2.3 NAME 'ipaMaxDomainLevel' DESC 'Maximal supported Domain Level value' EQUALITY numericStringMatch ORDERING numericStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 SINGLE-VALUE X-ORIGIN 'IPA v4')
+objectClasses:  (2.16.840.1.113730.3.8.19.1.1  NAME 'ipaDomainLevelConfig' SUP ipaConfigObject AUXILIARY DESC 'Domain Level Configuration' MUST (ipaDomainLevel) X-ORIGIN 'IPA v4')
+objectClasses:  (2.16.840.1.113730.3.8.19.1.2  NAME 'ipaSupportedDomainLevelConfig' SUP ipaConfigObject AUXILIARY DESC 'Supported Domain Level Configuration' MUST (ipaMinDomainLevel $ ipaMaxDomainLevel) X-ORIGIN 'IPA v4')
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index ca6128e2911ab5c0a773dd553f8e67eab944f120..52f7e80d4c4268b965d11c2b64d8e52bb1c533a9 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -21,6 +21,7 @@ app_DATA =				\
 	65ipasudo.ldif			\
 	70ipaotp.ldif			\
 	71idviews.ldif			\
+	72domainlevels.ldif			\
 	anonymous-vlv.ldif		\
 	bootstrap-template.ldif		\
 	caJarSigningCert.cfg.template	\
@@ -33,6 +34,7 @@ app_DATA =				\
 	ds-nfiles.ldif			\
 	dns.ldif			\
 	dnssec.ldif			\
+	domainlevel.ldif			\
 	kerberos.ldif			\
 	indices.ldif			\
 	bind.named.conf.template	\
diff --git a/install/share/domainlevel.ldif b/install/share/domainlevel.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..cb90a6563b29abaf9a74e6d504af26666266d407
--- /dev/null
+++ b/install/share/domainlevel.ldif
@@ -0,0 +1,7 @@
+# Create default Domain Level for new masters
+dn: cn=Domain Level,cn=ipa,cn=etc,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: nsContainer
+objectClass: ipaDomainLevelConfig
+ipaDomainLevel: $DOMAINLEVEL
diff --git a/install/tools/ipa-replica-install b/install/tools/ipa-replica-install
index f68cc8cf4722264ecea2f1f50de3aa245be24ef9..2c5f934845e4cb6609f74e945b3a73de04db0a42 100755
--- a/install/tools/ipa-replica-install
+++ b/install/tools/ipa-replica-install
@@ -43,7 +43,7 @@ from ipaserver.install import cainstance
 from ipaserver.install import krainstance
 from ipaserver.install import dns as dns_installer
 from ipalib import api, create_api, errors, util, certstore, x509
-from ipalib.constants import CACERT
+from ipalib import constants
 from ipapython import version
 from ipapython.config import IPAOptionParser
 from ipapython import sysrestore
@@ -224,12 +224,12 @@ def install_ca_cert(ldap, base_dn, realm, cafile):
         try:
             certs = certstore.get_ca_certs(ldap, base_dn, realm, False)
         except errors.NotFound:
-            shutil.copy(cafile, CACERT)
+            shutil.copy(cafile, constants.CACERT)
         else:
             certs = [c[0] for c in certs if c[2] is not False]
-            x509.write_certificate_list(certs, CACERT)
+            x509.write_certificate_list(certs, constants.CACERT)
 
-        os.chmod(CACERT, 0444)
+        os.chmod(constants.CACERT, 0444)
     except Exception, e:
         print "error copying files: " + str(e)
         sys.exit(1)
@@ -569,6 +569,30 @@ def main():
                 print "    %% ipa-replica-manage del %s --force" % config.host_name
                 exit(3)
 
+            # Detect the current domain level
+            try:
+                current = remote_api.Command['domainlevel_show']['result']
+            except KeyError:
+                # If we're joining an older master, domainlevel_show is not
+                # available
+                current = 0
+
+            # Detect if current level is out of supported range
+            # for this IPA version
+            under_lower_bound = current < constants.MIN_DOMAIN_LEVEL
+            above_upper_bound = current > constants.MAX_DOMAIN_LEVEL
+
+            if under_lower_bound or above_upper_bound:
+                message = ("This version of FreeIPA does not support "
+                           "the Domain Level which is currently set for "
+                           "this domain. The Domain Level needs to be "
+                           "raised before installing a replica with "
+                           "this version is allowed to be installed "
+                           "within this domain.")
+                root_logger.error(message)
+                print(message)
+                exit(3)
+
             # Check pre-existing host entry
             try:
                 entry = conn.find_entries(u'fqdn=%s' % config.host_name, ['fqdn'], DN(api.env.container_host, api.env.basedn))
diff --git a/install/tools/ipa-server-install b/install/tools/ipa-server-install
index cb6e1abe2016c0f8cefc35b1d685373f05b3ef89..a62642e78eed19ccf6c678a074475ad3c02c62d6 100755
--- a/install/tools/ipa-server-install
+++ b/install/tools/ipa-server-install
@@ -70,7 +70,7 @@ from ipapython import sysrestore
 from ipapython.ipautil import *
 from ipapython import ipautil
 from ipapython import dogtag
-from ipalib import api, errors, util, x509
+from ipalib import api, errors, util, x509, constants
 from ipapython.config import IPAOptionParser
 from ipalib.util import validate_domain_name
 from ipalib.constants import CACERT
@@ -176,6 +176,8 @@ def parse_options():
                            help="create home directories for users "
                                 "on their first login")
     basic_group.add_option("--hostname", dest="host_name", help="fully qualified name of server")
+    basic_group.add_option("--domain-level", dest="domainlevel", help="IPA domain level",
+                           default=constants.MAX_DOMAIN_LEVEL, type=int)
     basic_group.add_option("--ip-address", dest="ip_addresses",
                       type="ip", ip_local=True, action="append", default=[],
                       help="Master Server IP Address. This option can be used multiple times",
@@ -327,6 +329,15 @@ def parse_options():
         except ValueError, e:
             parser.error("invalid domain: " + unicode(e))
 
+    # Check that Domain Level is within the allowed range
+    if not options.uninstall:
+        if options.domainlevel < constants.MIN_DOMAIN_LEVEL:
+            parser.error("Domain Level cannot be lower than {0}"
+                         .format(constants.MIN_DOMAIN_LEVEL))
+        elif options.domainlevel > constants.MAX_DOMAIN_LEVEL:
+            parser.error("Domain Level cannot be higher than {0}"
+                         .format(constants.MAX_DOMAIN_LEVEL))
+
     if not options.setup_dns:
         if options.forwarders:
             parser.error("You cannot specify a --forwarder option without the --setup-dns option")
@@ -1139,21 +1150,24 @@ def main():
                 ntp.create_instance()
 
         if options.dirsrv_cert_files:
-            ds = dsinstance.DsInstance(fstore=fstore)
+            ds = dsinstance.DsInstance(fstore=fstore,
+                                       domainlevel=options.domainlevel)
             ds.create_instance(realm_name, host_name, domain_name,
                             dm_password, dirsrv_pkcs12_info,
                             idstart=options.idstart, idmax=options.idmax,
                             subject_base=options.subject,
                             hbac_allow=not options.hbac_allow)
         else:
-            ds = dsinstance.DsInstance(fstore=fstore)
+            ds = dsinstance.DsInstance(fstore=fstore,
+                                       domainlevel=options.domainlevel)
             ds.create_instance(realm_name, host_name, domain_name,
                             dm_password,
                             idstart=options.idstart, idmax=options.idmax,
                             subject_base=options.subject,
                             hbac_allow=not options.hbac_allow)
     else:
-        ds = dsinstance.DsInstance(fstore=fstore)
+        ds = dsinstance.DsInstance(fstore=fstore,
+                                   domainlevel=options.domainlevel)
         ds.init_info(
             realm_name, host_name, domain_name, dm_password,
             options.subject, 1101, 1100, None)
diff --git a/install/updates/72-domainlevels.update b/install/updates/72-domainlevels.update
new file mode 100644
index 0000000000000000000000000000000000000000..2e83c7be9b200121081470a80a3a9303d685a789
--- /dev/null
+++ b/install/updates/72-domainlevels.update
@@ -0,0 +1,14 @@
+# Create default Domain Level entry if it does not exist
+dn: cn=Domain Level,cn=ipa,cn=etc,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: objectClass: ipaDomainLevelConfig
+default: ipaDomainLevel: 0
+
+# Create entry proclaiming Domain Level support of this master
+# This will update the supported Domain Levels during upgrade
+dn: cn=$FQDN,cn=masters,cn=ipa,cn=etc,$SUFFIX
+add: objectClass: ipaConfigObject
+add: objectClass: ipaSupportedDomainLevelConfig
+only: ipaMinDomainLevel: $MIN_DOMAIN_LEVEL
+only: ipaMaxDomainLevel: $MAX_DOMAIN_LEVEL
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 0d63d9ea8d85f1add5f036e7a39f89543586d33b..b52dc2570040336031b7fe01c1cec50156bd90c3 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -48,6 +48,7 @@ app_DATA =				\
 	61-trusts-s4u2proxy.update	\
 	62-ranges.update		\
 	71-idviews.update		\
+	72-domainlevels.update		\
 	90-post_upgrade_plugins.update	\
 	$(NULL)
 
diff --git a/ipalib/constants.py b/ipalib/constants.py
index f1e14702ffdf5a3bd23a62b1fdd2ee3cd95d84f8..04e29d25b1b41dbb67c23d02b4a534ec1735e44c 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -223,3 +223,6 @@ LDAP_GENERALIZED_TIME_FORMAT = "%Y%m%d%H%M%SZ"
 
 IPA_ANCHOR_PREFIX = ':IPA:'
 SID_ANCHOR_PREFIX = ':SID:'
+
+MIN_DOMAIN_LEVEL = 0
+MAX_DOMAIN_LEVEL = 1
diff --git a/ipalib/errors.py b/ipalib/errors.py
index 89b1ef2e0dc1d7346a69fb813bd71990746c620c..63ec22269467b769d276c443f6b3dbed38cd766e 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -1344,6 +1344,22 @@ class EmptyResult(NotFound):
 
     errno = 4031
 
+class InvalidDomainLevelError(ExecutionError):
+    """
+    **4032** Raised when a operation could not be completed due to a invalid
+             domain level.
+
+    For example:
+
+    >>> raise InvalidDomainLevelError(reason='feature requires domain level 4')
+    Traceback (most recent call last):
+      ...
+    InvalidDomainLevelError: feature requires domain level 4
+
+    """
+
+    errno = 4032
+
 class BuiltinError(ExecutionError):
     """
     **4100** Base class for builtin execution errors (*4100 - 4199*).
diff --git a/ipalib/plugins/domainlevel.py b/ipalib/plugins/domainlevel.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ace2aa06ab7ba586bab322458c1c85fb811ef8f
--- /dev/null
+++ b/ipalib/plugins/domainlevel.py
@@ -0,0 +1,132 @@
+#
+# Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+#
+
+from collections import namedtuple
+
+from ipalib import _
+from ipalib import api
+from ipalib import Command
+from ipalib import errors
+from ipalib import output
+from ipalib.parameters import Int
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import LDAPObject, LDAPUpdate, LDAPRetrieve
+
+from ipapython.dn import DN
+
+
+__doc__ = _("""
+Raise the IPA Domain Level.
+""")
+
+register = Registry()
+
+DomainLevelRange = namedtuple('DomainLevelRange', ['min', 'max'])
+
+domainlevel_output = (
+    output.Output('result', int),
+)
+
+domainlevel_dn = DN(
+    ('cn', 'Domain Level'),
+    ('cn', 'ipa'),
+    ('cn', 'etc'),
+    api.env.basedn
+)
+
+
+def get_domainlevel_range(master_entry):
+    try:
+        return DomainLevelRange(
+            int(master_entry['ipaMinDomainLevel'][0]),
+            int(master_entry['ipaMaxDomainLevel'][0])
+        )
+    except KeyError:
+        return DomainLevelRange(0, 0)
+
+
+def get_master_entries(ldap):
+    """
+    Returns list of LDAPEntries representing IPA masters.
+    """
+
+    container_masters = DN(
+        ('cn', 'masters'),
+        ('cn', 'ipa'),
+        ('cn', 'etc'),
+        api.env.basedn
+    )
+
+    masters, _ = ldap.find_entries(
+        filter="(cn=*)",
+        base_dn=container_masters,
+        scope=ldap.SCOPE_ONELEVEL,
+        paged_search=True,  # we need to make sure to get all of them
+    )
+
+    return masters
+
+
+@register()
+class domainlevel_show(Command):
+    __doc__ = _('Query current Domain Level.')
+
+    has_output = domainlevel_output
+
+    def execute(self, *args, **options):
+        ldap = self.api.Backend.ldap2
+        entry = ldap.get_entry(domainlevel_dn, ['ipaDomainLevel'])
+
+        return {'result': int(entry.single_value['ipaDomainLevel'])}
+
+
+@register()
+class domainlevel_set(Command):
+    __doc__ = _('Change current Domain Level.')
+
+    has_output = domainlevel_output
+
+    takes_args = (
+        Int('ipadomainlevel',
+            cli_name='level',
+            label=_('Domain Level'),
+            minvalue=0,
+        ),
+    )
+
+    def execute(self, *args, **options):
+        """
+        Checks all the IPA masters for supported domain level ranges.
+
+        If the desired domain level is within the supported range of all
+        masters, it will be raised.
+
+        Domain level cannot be lowered.
+        """
+
+        ldap = self.api.Backend.ldap2
+
+        current_entry = ldap.get_entry(domainlevel_dn)
+        current_value = int(current_entry.single_value['ipadomainlevel'])
+        desired_value = int(args[0])
+
+        # Domain level cannot be lowered
+        if int(desired_value) < int(current_value):
+            message = _("Domain Level cannot be lowered.")
+            raise errors.InvalidDomainLevelError(message)
+
+        # Check if every master supports the desired level
+        for master in get_master_entries(ldap):
+            supported = get_domainlevel_range(master)
+
+            if supported.min > desired_value or supported.max < desired_value:
+                message = _("Domain Level cannot be raised to {0}, server {1} "
+                            "does not support it."
+                            .format(desired_value, master['cn'][0]))
+                raise errors.InvalidDomainLevelError(message)
+
+        current_entry.single_value['ipaDomainLevel'] = desired_value
+        ldap.update_entry(current_entry)
+
+        return {'result': int(current_entry.single_value['ipaDomainLevel'])}
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index f1d24e49d1b184efde1c8d18ff37d0e329037ccc..7005513729628caef784d5fc2138768e8ac817a3 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -61,6 +61,7 @@ IPA_SCHEMA_FILES = ("60kerberos.ldif",
                     "65ipasudo.ldif",
                     "70ipaotp.ldif",
                     "71idviews.ldif",
+                    "72domainlevels.ldif",
                     "15rfc2307bis.ldif",
                     "15rfc4876.ldif")
 
@@ -185,7 +186,7 @@ info: IPA V2.0
 
 class DsInstance(service.Service):
     def __init__(self, realm_name=None, domain_name=None, dm_password=None,
-                 fstore=None):
+                 fstore=None, domainlevel=None):
         service.Service.__init__(self, "dirsrv",
             service_desc="directory server",
             dm_password=dm_password,
@@ -208,6 +209,7 @@ class DsInstance(service.Service):
         self.subject_base = None
         self.open_ports = []
         self.run_init_memberof = True
+        self.domainlevel = domainlevel
         if realm_name:
             self.suffix = ipautil.realm_to_suffix(self.realm)
             self.__setup_sub_dict()
@@ -252,6 +254,7 @@ class DsInstance(service.Service):
     def __common_post_setup(self):
         self.step("initializing group membership", self.init_memberof)
         self.step("adding master entry", self.__add_master_entry)
+        self.step("initializing domain level", self.__set_domain_level)
         self.step("configuring Posix uid/gid generation",
                   self.__config_uidgid_gen)
         self.step("adding replication acis", self.__add_replication_acis)
@@ -392,7 +395,8 @@ class DsInstance(service.Service):
                              IDMAX=self.idmax, HOST=self.fqdn,
                              ESCAPED_SUFFIX=str(self.suffix),
                              GROUP=DS_GROUP,
-                             IDRANGE_SIZE=idrange_size
+                             IDRANGE_SIZE=idrange_size,
+                             DOMAINLEVEL=self.domainlevel,
                          )
 
     def __create_instance(self):
@@ -1002,3 +1006,7 @@ class DsInstance(service.Service):
         root_logger.debug('Unable to find certificate subject base in '
                           'certmap.conf')
         return None
+
+    def __set_domain_level(self):
+        # Create global domain level antry and set the domain level
+        self._ldap_mod("domainlevel.ldif", self.sub_dict)
diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py
index 2f5bcc748eb546b4dad7e1aeeb7a2882a40d8d35..4aa463152b9ec05fb5e1de9e1a5e386f6fc46e6f 100644
--- a/ipaserver/install/ldapupdate.py
+++ b/ipaserver/install/ldapupdate.py
@@ -39,6 +39,7 @@ from ipaserver.install import installutils
 from ipapython import ipautil, ipaldap
 from ipalib import errors
 from ipalib import api, create_api
+from ipalib import constants
 from ipaplatform.paths import paths
 from ipaplatform import services
 from ipapython.dn import DN
@@ -305,6 +306,10 @@ class LDAPUpdate:
             self.sub_dict["TIME"] = int(time.time())
         if not self.sub_dict.get("DOMAIN") and domain is not None:
             self.sub_dict["DOMAIN"] = domain
+        if not self.sub_dict.get("MIN_DOMAIN_LEVEL"):
+            self.sub_dict["MIN_DOMAIN_LEVEL"] = str(constants.MIN_DOMAIN_LEVEL)
+        if not self.sub_dict.get("MAX_DOMAIN_LEVEL"):
+            self.sub_dict["MAX_DOMAIN_LEVEL"] = str(constants.MAX_DOMAIN_LEVEL)
         self.api = create_api(mode=None)
         self.api.bootstrap(in_server=True, context='updates')
         self.api.finalize()
diff --git a/ipaserver/install/plugins/update_managed_permissions.py b/ipaserver/install/plugins/update_managed_permissions.py
index 1fbfd9993fa2c871690b58cdce7000cd3deba0d5..cc2fc2f99db122a64234a66b31f3b68b81fcc8a1 100644
--- a/ipaserver/install/plugins/update_managed_permissions.py
+++ b/ipaserver/install/plugins/update_managed_permissions.py
@@ -338,7 +338,16 @@ NONOBJECT_PERMISSIONS = {
             'serviceAuthenticationMethod', 'objectclassMap', 'attributeMap',
             'profileTTL'
         },
-    }
+    },
+    'System: Read Domain Level': {
+        'ipapermlocation': DN('cn=masters,cn=ipa,cn=etc', api.env.basedn),
+        'ipapermtargetfilter': {'(objectclass=ipadomainlevelconfig)'},
+        'ipapermbindruletype': 'all',
+        'ipapermright': {'read', 'search', 'compare'},
+        'ipapermdefaultattr': {
+            'ipadomainlevel', 'objectclass',
+        },
+    },
 }
 
 
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to