diff --git a/ACI.txt b/ACI.txt index 5e84d05e8..a4c346f2c 100644 --- a/ACI.txt +++ b/ACI.txt @@ -61,7 +61,7 @@ aci: (targetattr = "cn || description || ipacertprofilestoreissued")(targetfilte dn: cn=certprofiles,cn=ca,dc=ipa,dc=example aci: (targetattr = "cn || createtimestamp || description || entryusn || ipacertprofilestoreissued || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Read Certificate Profiles";allow (compare,read,search) userdn = "ldap:///all";) dn: cn=ipaconfig,cn=etc,dc=ipa,dc=example -aci: (targetattr = "cn || createtimestamp || entryusn || ipacertificatesubjectbase || ipaconfigstring || ipacustomfields || ipadefaultemaildomain || ipadefaultloginshell || ipadefaultprimarygroup || ipagroupobjectclasses || ipagroupsearchfields || ipahomesrootdir || ipakrbauthzdata || ipamaxusernamelength || ipamigrationenabled || ipapwdexpadvnotify || ipasearchrecordslimit || ipasearchtimelimit || ipaselinuxusermapdefault || ipaselinuxusermaporder || ipauserauthtype || ipauserobjectclasses || ipausersearchfields || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaguiconfig)")(version 3.0;acl "permission:System: Read Global Configuration";allow (compare,read,search) userdn = "ldap:///all";) +aci: (targetattr = "cn || createtimestamp || entryusn || ipacertificatesubjectbase || ipaconfigstring || ipacustomfields || ipadefaultemaildomain || ipadefaultloginshell || ipadefaultprimarygroup || ipadomainresolutionorder || ipagroupobjectclasses || ipagroupsearchfields || ipahomesrootdir || ipakrbauthzdata || ipamaxusernamelength || ipamigrationenabled || ipapwdexpadvnotify || ipasearchrecordslimit || ipasearchtimelimit || ipaselinuxusermapdefault || ipaselinuxusermaporder || ipauserauthtype || ipauserobjectclasses || ipausersearchfields || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaguiconfig)")(version 3.0;acl "permission:System: Read Global Configuration";allow (compare,read,search) userdn = "ldap:///all";) dn: cn=costemplates,cn=accounts,dc=ipa,dc=example aci: (targetfilter = "(objectclass=costemplate)")(version 3.0;acl "permission:System: Add Group Password Policy costemplate";allow (add) groupdn = "ldap:///cn=System: Add Group Password Policy costemplate,cn=permissions,cn=pbac,dc=ipa,dc=example";) dn: cn=costemplates,cn=accounts,dc=ipa,dc=example diff --git a/API.txt b/API.txt index 2d6b401be..f876afc76 100644 --- a/API.txt +++ b/API.txt @@ -1061,7 +1061,7 @@ args: 0,1,1 option: Str('version?') output: Output('result') command: config_mod/1 -args: 0,26,3 +args: 0,27,3 option: Str('addattr*', cli_name='addattr') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('ca_renewal_master_server?', autofill=False) @@ -1070,6 +1070,7 @@ option: StrEnum('ipaconfigstring*', autofill=False, cli_name='ipaconfigstring', option: Str('ipadefaultemaildomain?', autofill=False, cli_name='emaildomain') option: Str('ipadefaultloginshell?', autofill=False, cli_name='defaultshell') option: Str('ipadefaultprimarygroup?', autofill=False, cli_name='defaultgroup') +option: Str('ipadomainresolutionorder?', autofill=False, cli_name='domain_resolution_order') option: Str('ipagroupobjectclasses*', autofill=False, cli_name='groupobjectclasses') option: IA5Str('ipagroupsearchfields?', autofill=False, cli_name='groupsearch') option: IA5Str('ipahomesrootdir?', autofill=False, cli_name='homedirectory') diff --git a/VERSION.m4 b/VERSION.m4 index 246d6bb59..9766c749b 100644 --- a/VERSION.m4 +++ b/VERSION.m4 @@ -73,8 +73,8 @@ define(IPA_DATA_VERSION, 20100614120000) # # ######################################################## define(IPA_API_VERSION_MAJOR, 2) -define(IPA_API_VERSION_MINOR, 221) -# Last change: cert: include certificate chain in cert command output +define(IPA_API_VERSION_MINOR, 222) +>>>>>>> ipaconfig: add the ability to manipulate domain resolution order ######################################################## diff --git a/ipaserver/plugins/config.py b/ipaserver/plugins/config.py index 5d574657e..232c88121 100644 --- a/ipaserver/plugins/config.py +++ b/ipaserver/plugins/config.py @@ -22,6 +22,7 @@ from ipalib import api from ipalib import Bool, Int, Str, IA5Str, StrEnum, DNParam from ipalib import errors from ipalib.plugable import Registry +from ipalib.util import validate_domain_name from .baseldap import ( LDAPObject, LDAPUpdate, @@ -34,6 +35,8 @@ from ipapython.dn import DN OPERATIONAL_ATTRIBUTES = ('nsaccountlock', 'member', 'memberof', 'memberindirect', 'memberofindirect',) +DOMAIN_RESOLUTION_ORDER_SEPARATOR = u':' + __doc__ = _(""" Server configuration @@ -95,7 +98,7 @@ class config(LDAPObject): 'ipamigrationenabled', 'ipacertificatesubjectbase', 'ipapwdexpadvnotify', 'ipaselinuxusermaporder', 'ipaselinuxusermapdefault', 'ipaconfigstring', 'ipakrbauthzdata', - 'ipauserauthtype' + 'ipauserauthtype', 'ipadomainresolutionorder' ] container_dn = DN(('cn', 'ipaconfig'), ('cn', 'etc')) permission_filter_objectclasses = ['ipaguiconfig'] @@ -108,7 +111,8 @@ class config(LDAPObject): 'cn', 'objectclass', 'ipacertificatesubjectbase', 'ipaconfigstring', 'ipadefaultemaildomain', 'ipadefaultloginshell', - 'ipadefaultprimarygroup', 'ipagroupobjectclasses', + 'ipadefaultprimarygroup', 'ipadomainresolutionorder', + 'ipagroupobjectclasses', 'ipagroupsearchfields', 'ipahomesrootdir', 'ipakrbauthzdata', 'ipamaxusernamelength', 'ipamigrationenabled', 'ipapwdexpadvnotify', @@ -250,6 +254,13 @@ class config(LDAPObject): label=_('IPA CA renewal master'), doc=_('Renewal master for IPA certificate authority'), flags={'virtual_attribute', 'no_create'} + ), + Str( + 'ipadomainresolutionorder?', + cli_name='domain_resolution_order', + label=_('Domain resolution order'), + doc=_('colon-separated list of domains used for short name' + ' qualification') ) ) @@ -266,6 +277,104 @@ class config(LDAPObject): config = backend.config_retrieve(role) entry_attrs.update(config) + def gather_trusted_domains(self): + """ + Aggregate all trusted domains into a dict keyed by domain names with + values corresponding to domain status (enabled/disabled) + """ + command = self.api.Command + try: + ad_forests = command.trust_find(sizelimit=0)['result'] + except errors.NotFound: + return {} + + trusted_domains = {} + for forest_name in [a['cn'][0] for a in ad_forests]: + forest_domains = command.trustdomain_find( + forest_name, sizelimit=0)['result'] + + trusted_domains.update( + { + dom['cn'][0]: dom['domain_enabled'][0] + for dom in forest_domains if 'domain_enabled' in dom + } + ) + + return trusted_domains + + def _validate_single_domain(self, attr_name, domain, known_domains): + """ + Validate a single domain from domain resolution order + + :param attr_name: name of attribute that holds domain resolution order + :param domain: domain name + :param known_domains: dict of domains known to IPA keyed by domain name + and valued by boolean value corresponding to domain status + (enabled/disabled) + + :raises: ValidationError if the domain name is empty, syntactically + invalid or corresponds to a disable domain + NotFound if a syntactically correct domain name unknown to IPA + is supplied (not IPA domain and not any of trusted domains) + """ + if not domain: + raise errors.ValidationError( + name=attr_name, + error=_("Empty domain is not allowed") + ) + + try: + validate_domain_name(domain) + except ValueError as e: + raise errors.ValidationError( + name=attr_name, + error=_("Invalid domain name '%(domain)s': %(e)s") + % dict(domain=domain, e=e)) + + if domain not in known_domains: + raise errors.NotFound( + reason=_("Server has no information about domain '%(domain)s'") + % dict(domain=domain) + ) + + if not known_domains[domain]: + raise errors.ValidationError( + name=attr_name, + error=_("Disabled domain '%(domain)s' is not allowed") + % dict(domain=domain) + ) + + def validate_domain_resolution_order(self, entry_attrs): + """ + Validate domain resolution order, e.g. split by the delimiter (colon) + and check each domain name for non-emptiness, syntactic correctness, + and status (enabled/disabled). + + supplying empty order (':') bypasses validations and allows to specify + empty attribute value. + """ + attr_name = 'ipadomainresolutionorder' + if attr_name not in entry_attrs: + return + + domain_resolution_order = entry_attrs[attr_name] + + # empty resolution order is signalized by single separator, do nothing + # and let it pass + if domain_resolution_order == DOMAIN_RESOLUTION_ORDER_SEPARATOR: + return + + submitted_domains = domain_resolution_order.split( + DOMAIN_RESOLUTION_ORDER_SEPARATOR) + + known_domains = self.gather_trusted_domains() + + # add FreeIPA domain to the list of domains. This one is always enabled + known_domains.update({self.api.env.domain: True}) + + for domain in submitted_domains: + self._validate_single_domain(attr_name, domain, known_domains) + @register() class config_mod(LDAPUpdate): @@ -396,6 +505,8 @@ class config_mod(LDAPUpdate): backend = self.api.Backend.serverroles backend.config_update(ca_renewal_master_server=new_master) + self.obj.validate_domain_resolution_order(entry_attrs) + return dn def exc_callback(self, keys, options, exc, call_func,