From 1b5f56d15455b6019dd532cb9635fa2c44cb0022 Mon Sep 17 00:00:00 2001 From: Martin Babinsky Date: Thu, 9 Mar 2017 18:14:52 +0100 Subject: ipaconfig: add the ability to manipulate domain resolution order optional attribute was added to config object along with validator that check for valid domain names and also checks whether the specified domains exist in FreeIPA or in trusted forests and, in case of trusted domains, are not disabled. Part of http://www.freeipa.org/page/V4/AD_User_Short_Names https://pagure.io/freeipa/issue/6372 Reviewed-By: Martin Basti Reviewed-By: Alexander Bokovoy Reviewed-By: Jan Cholasta --- ACI.txt | 2 +- API.txt | 3 +- VERSION.m4 | 4 +- ipaserver/plugins/config.py | 115 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 6 deletions(-) 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, -- cgit