From 67d8b434c5272fd47d2e168c2b97077c70c016c2 Mon Sep 17 00:00:00 2001 From: Martin Kosek Date: Fri, 25 Jan 2013 10:10:17 +0100 Subject: Add trusconfig-show and trustconfig-mod commands Global trust configuration is generated ipa-adtrust-install script is run. Add convenience commands to show auto-generated options like SID or GUID or options chosen by user (NetBIOS). Most of these options are not modifiable via trustconfig-mod command as it would break current trusts. Unit test file covering these new commands was added. https://fedorahosted.org/freeipa/ticket/3333 --- API.txt | 24 +++++ VERSION | 2 +- ipalib/plugins/trust.py | 190 +++++++++++++++++++++++++++++++-- tests/test_xmlrpc/test_trust_plugin.py | 159 +++++++++++++++++++++++++++ tests/test_xmlrpc/xmlrpc_test.py | 10 ++ 5 files changed, 377 insertions(+), 8 deletions(-) create mode 100644 tests/test_xmlrpc/test_trust_plugin.py diff --git a/API.txt b/API.txt index 8fbfe6f5d..6e5c8c587 100644 --- a/API.txt +++ b/API.txt @@ -3262,6 +3262,30 @@ option: Str('version?', exclude='webui') output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) output: Output('summary', (, ), None) output: Output('value', , None) +command: trustconfig_mod +args: 0,9,3 +option: Str('addattr*', cli_name='addattr', exclude='webui') +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Str('delattr*', cli_name='delattr', exclude='webui') +option: Str('ipantfallbackprimarygroup', attribute=True, autofill=False, cli_name='fallback_primary_group', multivalue=False, required=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Flag('rights', autofill=True, default=False) +option: Str('setattr*', cli_name='setattr', exclude='webui') +option: StrEnum('trust_type', autofill=True, cli_name='type', default=u'ad', values=(u'ad',)) +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: Output('value', , None) +command: trustconfig_show +args: 0,5,3 +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Flag('rights', autofill=True, default=False) +option: StrEnum('trust_type', autofill=True, cli_name='type', default=u'ad', values=(u'ad',)) +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: Output('value', , None) command: user_add args: 1,34,3 arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, required=True) diff --git a/VERSION b/VERSION index 61f578dbf..37af5ef73 100644 --- a/VERSION +++ b/VERSION @@ -89,4 +89,4 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=47 +IPA_API_VERSION_MINOR=48 diff --git a/ipalib/plugins/trust.py b/ipalib/plugins/trust.py index 2019d910b..a5211bfab 100644 --- a/ipalib/plugins/trust.py +++ b/ipalib/plugins/trust.py @@ -1,5 +1,6 @@ # Authors: # Alexander Bokovoy +# Martin Kosek # # Copyright (C) 2011 Red Hat # see file 'COPYING' for use and warranty information @@ -95,6 +96,30 @@ Example: 4. List members of external members of ad_admins_external group to see their SIDs: ipa group-show ad_admins_external + + +GLOBAL TRUST CONFIGURATION + +When IPA AD trust subpackage is installed and ipa-adtrust-install is run, +a local domain configuration (SID, GUID, NetBIOS name) is generated. These +identifiers are then used when communicating with a trusted domain of the +particular type. + +1. Show global trust configuration for Active Directory type of trusts: + + ipa trustconfig-show --type ad + +2. Modify global configuration for all trusts of Active Directory type and set + a different fallback primary group (fallback primary group GID is used as + a primary user GID if user authenticating to IPA domain does not have any other + primary GID already set): + + ipa trustconfig-mod --type ad --fallback-primary-group "alternative AD group" + +3. Change primary fallback group back to default hidden group (any group with + posixGroup object class is allowed): + + ipa trustconfig-mod --type ad --fallback-primary-group "Default SMB Group" """) trust_output_params = ( @@ -120,6 +145,14 @@ _trust_status_dict = {True : _('Established and verified'), False : _('Waiting for confirmation by remote side')} _trust_type_dict_unknown = _('Unknown') +_trust_type_option = StrEnum('trust_type', + cli_name='type', + label=_('Trust type (ad for Active Directory, default)'), + values=(u'ad',), + default=u'ad', + autofill=True, + ) + def trust_type_string(level): """ Returns a string representing a type of the trust. The original field is an enum: @@ -193,13 +226,7 @@ sides. ''') takes_options = LDAPCreate.takes_options + ( - StrEnum('trust_type', - cli_name='type', - label=_('Trust type (ad for Active Directory, default)'), - values=(u'ad',), - default=u'ad', - autofill=True, - ), + _trust_type_option, Str('realm_admin?', cli_name='admin', label=_("Active Directory domain administrator"), @@ -482,3 +509,152 @@ api.register(trust_mod) api.register(trust_del) api.register(trust_find) api.register(trust_show) + +_trustconfig_dn = { + u'ad': DN(('cn', api.env.domain), api.env.container_cifsdomains, api.env.basedn), +} + + +class trustconfig(LDAPObject): + """ + Trusts global configuration object + """ + object_name = _('trust configuration') + default_attributes = [ + 'cn', 'ipantsecurityidentifier', 'ipantflatname', 'ipantdomainguid', + 'ipantfallbackprimarygroup', + ] + + label = _('Global Trust Configuration') + label_singular = _('Global Trust Configuration') + + takes_params = ( + Str('cn', + label=_('Domain'), + flags=['no_update'], + ), + Str('ipantsecurityidentifier', + label=_('Security Identifier'), + flags=['no_update'], + ), + Str('ipantflatname', + label=_('NetBIOS name'), + flags=['no_update'], + ), + Str('ipantdomainguid', + label=_('Domain GUID'), + flags=['no_update'], + ), + Str('ipantfallbackprimarygroup', + cli_name='fallback_primary_group', + label=_('Fallback primary group'), + ), + ) + + def get_dn(self, *keys, **kwargs): + trust_type = kwargs.get('trust_type') + if trust_type is None: + raise errors.RequirementError(name='trust_type') + try: + return _trustconfig_dn[kwargs['trust_type']] + except KeyError: + raise errors.ValidationError(name='trust_type', + error=_("unsupported trust type")) + + def _normalize_groupdn(self, entry_attrs): + """ + Checks that group with given name/DN exists and updates the entry_attrs + """ + if 'ipantfallbackprimarygroup' not in entry_attrs: + return + + group = entry_attrs['ipantfallbackprimarygroup'] + if isinstance(group, (list, tuple)): + group = group[0] + + if group is None: + return + + try: + dn = DN(group) + # group is in a form of a DN + try: + self.backend.get_entry(dn) + except errors.NotFound: + self.api.Object['group'].handle_not_found(group) + # DN is valid, we can just return + return + except ValueError: + # The search is performed for groups with "posixgroup" objectclass + # and not "ipausergroup" so that it can also match groups like + # "Default SMB Group" which does not have this objectclass. + try: + (dn, group_entry) = self.backend.find_entry_by_attr( + self.api.Object['group'].primary_key.name, + group, + ['posixgroup'], + [''], + self.api.Object['group'].container_dn) + except errors.NotFound: + self.api.Object['group'].handle_not_found(group) + else: + entry_attrs['ipantfallbackprimarygroup'] = [dn] + + def _convert_groupdn(self, entry_attrs, options): + """ + Convert an group dn into a name. As we use CN as user RDN, its value + can be extracted from the DN without further LDAP queries. + """ + if options.get('raw', False): + return + + try: + groupdn = entry_attrs['ipantfallbackprimarygroup'][0] + except (IndexError, KeyError): + groupdn = None + + if groupdn is None: + return + assert isinstance(groupdn, DN) + + entry_attrs['ipantfallbackprimarygroup'] = [groupdn[0][0].value] + +api.register(trustconfig) + +class trustconfig_mod(LDAPUpdate): + __doc__ = _('Modify global trust configuration.') + + takes_options = LDAPUpdate.takes_options + (_trust_type_option,) + msg_summary = _('Modified "%(value)s" trust configuration') + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + self.obj._normalize_groupdn(entry_attrs) + return dn + + def execute(self, *keys, **options): + result = super(trustconfig_mod, self).execute(*keys, **options) + result['value'] = options['trust_type'] + return result + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + self.obj._convert_groupdn(entry_attrs, options) + return dn + +api.register(trustconfig_mod) + + +class trustconfig_show(LDAPRetrieve): + __doc__ = _('Show global trust configuration.') + + takes_options = LDAPRetrieve.takes_options + (_trust_type_option,) + + def execute(self, *keys, **options): + result = super(trustconfig_show, self).execute(*keys, **options) + result['value'] = options['trust_type'] + return result + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + self.obj._convert_groupdn(entry_attrs, options) + return dn + +api.register(trustconfig_show) diff --git a/tests/test_xmlrpc/test_trust_plugin.py b/tests/test_xmlrpc/test_trust_plugin.py new file mode 100644 index 000000000..7627be748 --- /dev/null +++ b/tests/test_xmlrpc/test_trust_plugin.py @@ -0,0 +1,159 @@ +# Authors: +# Martin Kosek +# +# Copyright (C) 2010 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Test the `ipalib/plugins/trust.py` module. +""" + +import nose +from ipalib import api, errors +from ipapython.dn import DN +from tests.test_xmlrpc import objectclasses +from xmlrpc_test import (Declarative, fuzzy_guid, fuzzy_domain_sid, fuzzy_string, + fuzzy_uuid, fuzzy_digits) + + +trustconfig_ad_config = DN(('cn', api.env.domain), + api.env.container_cifsdomains, api.env.basedn) +testgroup = u'adtestgroup' +testgroup_dn = DN(('cn', testgroup), api.env.container_group, api.env.basedn) + +default_group = u'Default SMB Group' +default_group_dn = DN(('cn', default_group), api.env.container_group, api.env.basedn) + +class test_trustconfig(Declarative): + + @classmethod + def setUpClass(cls): + super(test_trustconfig, cls).setUpClass() + if not api.Backend.xmlclient.isconnected(): + api.Backend.xmlclient.connect(fallback=False) + try: + api.Command['trustconfig_show'](trust_type=u'ad') + except errors.NotFound: + raise nose.SkipTest('Trusts are not configured') + + cleanup_commands = [ + ('group_del', [testgroup], {}), + ('trustconfig_mod', [], {'trust_type': u'ad', + 'ipantfallbackprimarygroup': default_group}), + ] + + tests = [ + + dict( + desc='Retrieve trust configuration for AD domains', + command=('trustconfig_show', [], {'trust_type': u'ad'}), + expected={ + 'value': u'ad', + 'summary': None, + 'result': { + 'dn': trustconfig_ad_config, + 'cn': [api.env.domain], + 'ipantdomainguid': [fuzzy_guid], + 'ipantfallbackprimarygroup': [default_group], + 'ipantflatname': [fuzzy_string], + 'ipantsecurityidentifier': [fuzzy_domain_sid] + }, + }, + ), + + dict( + desc='Retrieve trust configuration for AD domains with --raw', + command=('trustconfig_show', [], {'trust_type': u'ad', 'raw': True}), + expected={ + 'value': u'ad', + 'summary': None, + 'result': { + 'dn': trustconfig_ad_config, + 'cn': [api.env.domain], + 'ipantdomainguid': [fuzzy_guid], + 'ipantfallbackprimarygroup': [default_group_dn], + 'ipantflatname': [fuzzy_string], + 'ipantsecurityidentifier': [fuzzy_domain_sid] + }, + }, + ), + + dict( + desc='Create auxiliary group %r' % testgroup, + command=( + 'group_add', [testgroup], dict(description=u'Test group') + ), + expected=dict( + value=testgroup, + summary=u'Added group "%s"' % testgroup, + result=dict( + cn=[testgroup], + description=[u'Test group'], + gidnumber=[fuzzy_digits], + objectclass=objectclasses.group + [u'posixgroup'], + ipauniqueid=[fuzzy_uuid], + dn=testgroup_dn, + ), + ), + ), + + dict( + desc='Try to change primary fallback group to nonexistent group', + command=('trustconfig_mod', [], + {'trust_type': u'ad', 'ipantfallbackprimarygroup': u'doesnotexist'}), + expected=errors.NotFound(reason=u'%s: group not found' % 'doesnotexist') + ), + + dict( + desc='Try to change primary fallback group to nonexistent group DN', + command=('trustconfig_mod', [], {'trust_type': u'ad', + 'ipantfallbackprimarygroup': u'cn=doesnotexist,dc=test'}), + expected=errors.NotFound(reason=u'%s: group not found' % 'cn=doesnotexist,dc=test') + ), + + dict( + desc='Change primary fallback group to "%s"' % testgroup, + command=('trustconfig_mod', [], {'trust_type': u'ad', + 'ipantfallbackprimarygroup': testgroup}), + expected={ + 'value': u'ad', + 'summary': u'Modified "ad" trust configuration', + 'result': { + 'cn': [api.env.domain], + 'ipantdomainguid': [fuzzy_guid], + 'ipantfallbackprimarygroup': [testgroup], + 'ipantflatname': [fuzzy_string], + 'ipantsecurityidentifier': [fuzzy_domain_sid] + }, + }, + ), + + dict( + desc='Change primary fallback group back to "%s" using DN' % default_group, + command=('trustconfig_mod', [], {'trust_type': u'ad', + 'ipantfallbackprimarygroup': unicode(default_group_dn)}), + expected={ + 'value': u'ad', + 'summary': u'Modified "ad" trust configuration', + 'result': { + 'cn': [api.env.domain], + 'ipantdomainguid': [fuzzy_guid], + 'ipantfallbackprimarygroup': [default_group], + 'ipantflatname': [fuzzy_string], + 'ipantsecurityidentifier': [fuzzy_domain_sid] + }, + }, + ), + ] diff --git a/tests/test_xmlrpc/xmlrpc_test.py b/tests/test_xmlrpc/xmlrpc_test.py index 7c32be0db..610fa97c5 100644 --- a/tests/test_xmlrpc/xmlrpc_test.py +++ b/tests/test_xmlrpc/xmlrpc_test.py @@ -40,6 +40,16 @@ fuzzy_uuid = Fuzzy( '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' ) +# Matches trusted domain GUID, like u'463bf2be-3456-4a57-979e-120304f2a0eb' +fuzzy_guid = fuzzy_uuid + +# Matches SID of a trusted domain +# SID syntax: http://msdn.microsoft.com/en-us/library/ff632068.aspx +_sid_identifier_authority = '(0x[0-9a-f]{1,12}|[0-9]{1,10})' +fuzzy_domain_sid = Fuzzy( + '^S-1-5-21-%(idauth)s-%(idauth)s-%(idauth)s$' % dict(idauth=_sid_identifier_authority) +) + # Matches netgroup dn. Note (?i) at the beginning of the regexp is the ingnore case flag fuzzy_netgroupdn = Fuzzy( '(?i)ipauniqueid=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12},cn=ng,cn=alt,%s' % api.env.basedn -- cgit