From f3010498af2a4b98512d219b8e09101176c172fe Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Thu, 14 May 2015 10:49:55 +0200 Subject: Add Domain Level feature https://fedorahosted.org/freeipa/ticket/5018 Reviewed-By: Jan Cholasta Reviewed-By: Petr Vobornik --- ACI.txt | 2 + API.txt | 9 ++ VERSION | 4 +- 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 +- 17 files changed, 280 insertions(+), 14 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 bf5398929..3c4ebde5b 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 38deafefa..66f55e2d1 100644 --- a/API.txt +++ b/API.txt @@ -1283,6 +1283,15 @@ option: Str('version?', exclude='webui') output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) output: Output('summary', (, ), None) output: PrimaryKey('value', None, None) +command: domainlevel_get +args: 0,1,1 +option: Str('version?', exclude='webui') +output: Output('result', , None) +command: domainlevel_set +args: 1,1,1 +arg: Int('ipadomainlevel', cli_name='level', minvalue=0) +option: Str('version?', exclude='webui') +output: Output('result', , None) command: env args: 1,3,4 arg: Str('variables*') diff --git a/VERSION b/VERSION index 33e7bebe4..071b444a3 100644 --- a/VERSION +++ b/VERSION @@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=119 -# Last change: edewata - Added vault plugin +IPA_API_VERSION_MINOR=120 +# Last change: tbabej - Add Domain Level feature diff --git a/install/share/72domainlevels.ldif b/install/share/72domainlevels.ldif new file mode 100644 index 000000000..184e1cb22 --- /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 c39352caa..8d336690f 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 000000000..21ed6a473 --- /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 34e5b3443..321b8c368 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 c75848b1a..1df782b73 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 9bb8955dc..c7d7c7bff 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 000000000..2e83c7be9 --- /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 66f6b9d37..4e2da05d6 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 195938a35..b99306eae 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 89b1ef2e0..63ec22269 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 000000000..64e383006 --- /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 09139405d..064a2ab1d 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 5fca37695..f30659fe9 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 1fbfd9993..11765fba3 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', + }, + }, } -- cgit