On 05/26/2015 01:51 PM, Tomas Babej wrote:
> 
> 
> On 05/26/2015 12:39 PM, Tomas Babej wrote:
>>
>>
>> On 05/26/2015 11:57 AM, Jan Cholasta wrote:
>>> Dne 25.5.2015 v 17:15 Tomas Babej napsal(a):
>>>>
>>>>
>>>> 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.
>>>>
>>>
>>> Thanks for the patch!
>>>
>>> 1)
>>>
>>> +            # 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
>>>
>>> KeyError? That does not look right. remote_api differs from api only in
>>> that it sets up ldap2 to connect to the remote server, but it uses local
>>> plugins and everything, so domainlevel_show should always be available.
>>>
>>>
>>> 2) Could you also set supported domain levels in
>>> install/share/master-entry.ldif? I think it makes sense to have them
>>> there right from the beginning of server install.
>>>
>>>
>>> 3) I think you should use the per-plugin api object instead of
>>> ipalib.api when constructing DNs (domainlevel_dn, container_masters).
>>>
>>>
>>> 4) I would say the opposite of "domainlevel-set" should be
>>> "domainlevel-get", not "domainlevel-show". IMO it's OK since property
>>> commands are an uncharted territory and don't have to (maybe even
>>> shouldn't) use the same convention as objects.
>>>
>>>
>>> 5)
>>>
>>> +    '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',
>>> +        },
>>> +    },
>>>
>>> Shouldn't ipapermlocation say "cn=Domain Level,cn=ipa,cn=etc"?
>>>
>>
>> Thanks for the review, I fixed all the issues raised.
>>
>> Tomas
>>
> 
> Added a small fix for replca install, to avoid duplicated creation of
> the domainlevel entry.
> 
> Tomas
> 

Aand the correct patch.

Tomas
>From 4d4b6bae3e64b9e1b311bf36c76f304066fb34be 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/share/master-entry.ldif                    |   6 +-
 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                      | 138 +++++++++++++++++++++
 ipaserver/install/dsinstance.py                    |  16 ++-
 ipaserver/install/ldapupdate.py                    |   5 +
 .../install/plugins/update_managed_permissions.py  |  11 +-
 16 files changed, 278 insertions(+), 12 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..3c4ebde5b3ac2eb0b8e9465c5f2bd74f5bdbfb01 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -322,6 +322,8 @@ dn: cn=dna,cn=ipa,cn=etc,dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || dnahostname || dnaportnum || dnaremainingvalues || dnaremotebindmethod || dnaremoteconnprotocol || dnasecureportnum || entryusn || modifytimestamp || objectclass")(targetfilter = "(objectclass=dnasharedconfig)")(version 3.0;acl "permission:System: Read DNA Configuration";allow (compare,read,search) userdn = "ldap:///all";;)
 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=Domain Level,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
diff --git a/API.txt b/API.txt
index 38deafefa57942bf242f989d79b1e93ee2c2013e..66f55e2d1f72857a02a0f2e1a3ba9472334524b7 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_get
+args: 0,1,1
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'int'>, 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: 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 c39352caae69f9aa60ccb4f2ffce7ec01da26da1..8d336690f184025f8199ed1d2c57d8274f0d3886 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -22,6 +22,7 @@ app_DATA =				\
 	70ipaotp.ldif			\
 	70topology.ldif			\
 	71idviews.ldif			\
+	72domainlevels.ldif			\
 	anonymous-vlv.ldif		\
 	bootstrap-template.ldif		\
 	caJarSigningCert.cfg.template	\
@@ -34,6 +35,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..21ed6a4738f25ce6668806cd44483e2d291dc361
--- /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: $DOMAIN_LEVEL
diff --git a/install/share/master-entry.ldif b/install/share/master-entry.ldif
index 34e5b34437eef663d37942b2f5d51c33ca999151..321b8c3681e2b86f3cb35cc512c04e45f34ea022 100644
--- a/install/share/master-entry.ldif
+++ b/install/share/master-entry.ldif
@@ -3,5 +3,9 @@ changetype: add
 objectclass: top
 objectclass: nsContainer
 objectclass: ipaReplTopoManagedServer
-ipaReplTopoManagedSuffix: $SUFFIX
+objectClass: ipaConfigObject
+objectClass: ipaSupportedDomainLevelConfig
 cn: $FQDN
+ipaReplTopoManagedSuffix: $SUFFIX
+ipaMinDomainLevel: $MIN_DOMAIN_LEVEL
+ipaMaxDomainLevel: $MAX_DOMAIN_LEVEL
diff --git a/install/tools/ipa-replica-install b/install/tools/ipa-replica-install
index c75848b1ada91254a41245df240ede24c477d5b1..1df782b7304b3303f8e67ad943f0f2fbf1e96f9c 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 kra
 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_get']()['result']
+            except errors.NotFound:
+                # If we're joining an older master, domain entry 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 9bb8955dc15d1682edf33d7652de0829771267f3..c7d7c7bff7d5a5e818eaa4a8cb93be94eae7d0c1 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")
@@ -1143,21 +1154,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 66f6b9d37971f8b8501d73fc6ddca21b6686ff4b..4e2da05d61a41543914e79c4634331df6018c041 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -49,6 +49,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 195938a355d1b24c02aa0a5833c1725c76e85c76..b99306eaec1d7bcbec4612a8aa4a599d02ac73e5 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -224,3 +224,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..64e383006722fb2f32f5300d627b18b6daf051d4
--- /dev/null
+++ b/ipalib/plugins/domainlevel.py
@@ -0,0 +1,138 @@
+#
+# Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+#
+
+from collections import namedtuple
+
+from ipalib import _
+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, _('Current domain level:')),
+)
+
+
+def get_domainlevel_dn(api):
+    domainlevel_dn = DN(
+        ('cn', 'Domain Level'),
+        ('cn', 'ipa'),
+        ('cn', 'etc'),
+        api.env.basedn
+    )
+
+    return domainlevel_dn
+
+
+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, api):
+    """
+    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_get(Command):
+    __doc__ = _('Query current Domain Level.')
+
+    has_output = domainlevel_output
+
+    def execute(self, *args, **options):
+        ldap = self.api.Backend.ldap2
+        entry = ldap.get_entry(
+            get_domainlevel_dn(self.api),
+            ['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(get_domainlevel_dn(self.api))
+        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, self.api):
+            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 09139405dfebae0ee3f82848a1b6b73e49e49687..064a2ab1db61b465638a77e13e1d9ea43b1cce63 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -40,6 +40,7 @@ from ipaserver.install import upgradeinstance
 from ipalib import api
 from ipalib import certstore
 from ipalib import errors
+from ipalib import constants
 from ipaplatform.tasks import tasks
 from ipalib.constants import CACERT
 from ipapython.dn import DN
@@ -62,6 +63,7 @@ IPA_SCHEMA_FILES = ("60kerberos.ldif",
                     "70ipaotp.ldif",
                     "70topology.ldif",
                     "71idviews.ldif",
+                    "72domainlevels.ldif",
                     "15rfc2307bis.ldif",
                     "15rfc4876.ldif")
 
@@ -186,7 +188,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,
@@ -209,6 +211,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()
@@ -254,6 +257,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)
@@ -395,7 +399,10 @@ 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,
+                             DOMAIN_LEVEL=self.domainlevel,
+                             MAX_DOMAIN_LEVEL=constants.MAX_DOMAIN_LEVEL,
+                             MIN_DOMAIN_LEVEL=constants.MIN_DOMAIN_LEVEL,
                          )
 
     def __create_instance(self):
@@ -1011,3 +1018,8 @@ 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 entry and set the domain level
+        if self.domainlevel is not None:
+            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..11765fba33342eb0168cfffa2f354f5ffc8cf4ef 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=Domain Level,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