[Freeipa-devel] [PATCH] External group membership for trusted domains

Alexander Bokovoy abokovoy at redhat.com
Thu Jun 21 15:26:02 UTC 2012


Hi!

Attached is the patch to support external group membership for trusted
domains. This is needed to get proper group membership with the work
Sumit and Jan are doing on both IPA and SSSD sides.

We already have ipaExternalGroup class that includes ipaExternalMember
attribute (multivalued case-insensitive string). The group that has 
ipaExternalGroup object class will have to be non-POSIX and ipaExternalMember
attribute will contain security identifiers (SIDs) of members from
trusted domains.

The patch takes care of three things:
1. Extends 'ipa group-add' with --external option to add
    ipaExternalGroup object class to a new group
2. Modifies 'ipa group-add-member' to accept --external CSV argument
    to specify SIDs
3. Modifies 'ipa group-del-member' to allow removing external members.

When adding new external member we also perform SID correctness checks.
This is important part of the patch due to potential security
implications of allowing random SIDs. SIDs are universal identifiers and
can point to objects in own domain as well as any other. If so-called
builtin SIDs are used, they are resolved against local domain which will
allow granting permissions trusted domain user should have never had.

Below is how we do perform validation of SIDs:
1. Use Samba 4 bindings to parse SID and validate its format
2. If SID is outside S-1-5- prefix (SID_NT_AUTHORITY), we reject it.
3. If SID is from our own domain, we reject it.
4. If SID is from any of our trusted domains, we accept it
5. Otherwise we reject SID.

Here is real code:
+    def is_trusted_sid_valid(self, sid):
+        if not self.domain:
+            # our domain is not configured or self.is_configured() never run
+            # reject SIDs as we can't check correctness of them
+            return False
+        # Parse sid string to see if it is really in a SID format
+        try:
+            test_sid = security.dom_sid(sid)
+        except TypeError:
+            return False
+        (dom, sid_rid) = test_sid.split()
+        sid_dom = str(dom)
+        # Now we have domain prefix of the sid as sid_dom string and can
+        # analyze it against known prefixes
+        if sid_dom.find(security.SID_NT_AUTHORITY) != 0:
+            # Ignore any potential SIDs that are not S-1-5-*
+            return False
+        if sid_dom.find(self.sid) == 0:
+            # A SID from our own domain cannot be treated as trusted domain's SID
+            return False
+        # At this point we have SID_NT_AUTHORITY family SID and really need to
+        # check it against prefixes of domain SIDs we trust to
+        if not self._domains:
+            self._domains = self.get_trusted_domains()
+        if len(self._domains) == 0:
+            # Our domain is configured but no trusted domains are configured
+            # This means we can't check the correctness of a trusted domain SIDs 
+            return False
+        # We have non-zero list of trusted domains and have to go through them
+        # one by one and check their sids as prefixes
+        for (dn, domaininfo) in self._domains:
+            if sid_dom.find(domaininfo[self.ATTR_TRUSTED_SID][0]) == 0:
+                return True
+        return False



-- 
/ Alexander Bokovoy
-------------- next part --------------
>From 5d68493a061797e671dd42eb278ede1a04cc1a22 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy at redhat.com>
Date: Wed, 20 Jun 2012 16:08:33 +0300
Subject: [PATCH 1/3] Add support for external group members

When using ipaExternalGroup/ipaExternalMember attributes it is
possible to add group members which don't exist in IPA database.
This is primarily is required for AD trusts support and therefore
validation is accepting only secure identifier (SID) format.

https://fedorahosted.org/freeipa/ticket/2664
---
 API.txt                    |   12 ++++--
 ipalib/errors.py           |   17 ++++++++
 ipalib/plugins/baseldap.py |   18 ++++++--
 ipalib/plugins/group.py    |  103 +++++++++++++++++++++++++++++++++++++++++---
 ipalib/plugins/trust.py    |    4 ++
 ipaserver/dcerpc.py        |   93 +++++++++++++++++++++++++++++++++++----
 6 files changed, 226 insertions(+), 21 deletions(-)

diff --git a/API.txt b/API.txt
index 8127b90b91415d165590845f0ba1b6d94dab28aa..6e993cc9412a354cb882e8f5cc2bd3caede53100 100644
--- a/API.txt
+++ b/API.txt
@@ -1196,13 +1196,14 @@ output: Output('total', <type 'int'>, None)
 output: Output('count', <type 'int'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 command: group_add
-args: 1,8,3
+args: 1,9,3
 arg: Str('cn', attribute=True, cli_name='group_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, required=True)
 option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=True)
 option: Int('gidnumber', attribute=True, cli_name='gid', minvalue=1, multivalue=False, required=False)
 option: Str('setattr*', cli_name='setattr', exclude='webui')
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('nonposix', autofill=True, cli_name='nonposix', default=False)
+option: Flag('external', autofill=True, cli_name='external', default=False)
 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: Str('version?', exclude='webui')
@@ -1210,8 +1211,9 @@ output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('value', <type 'unicode'>, None)
 command: group_add_member
-args: 1,5,3
+args: 1,6,3
 arg: Str('cn', attribute=True, cli_name='group_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
+option: Str('ipaexternalmember*', cli_name='external', csv=True)
 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: Str('version?', exclude='webui')
@@ -1265,7 +1267,7 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('count', <type 'int'>, None)
 output: Output('truncated', <type 'bool'>, None)
 command: group_mod
-args: 1,11,3
+args: 1,12,3
 arg: Str('cn', attribute=True, cli_name='group_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
 option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
 option: Int('gidnumber', attribute=True, autofill=False, cli_name='gid', minvalue=1, multivalue=False, required=False)
@@ -1274,6 +1276,7 @@ option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Str('delattr*', cli_name='delattr', exclude='webui')
 option: Flag('rights', autofill=True, default=False)
 option: Flag('posix', autofill=True, cli_name='posix', default=False)
+option: Flag('external', autofill=True, cli_name='external', default=False)
 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: Str('version?', exclude='webui')
@@ -1282,8 +1285,9 @@ output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('value', <type 'unicode'>, None)
 command: group_remove_member
-args: 1,5,3
+args: 1,6,3
 arg: Str('cn', attribute=True, cli_name='group_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
+option: Str('ipaexternalmember*', cli_name='external', csv=True)
 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: Str('version?', exclude='webui')
diff --git a/ipalib/errors.py b/ipalib/errors.py
index 407d9f7dbcf79c47193a3087fe6efbc50728c903..f4dc3c6084b0c36f2fd8556c1b725621bea29d1f 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -1277,6 +1277,23 @@ class SingleMatchExpected(ExecutionError):
     format = _('The search criteria was not specific enough. Expected 1 and found %(found)d.')
 
 
+class AlreadyExternalGroup(ExecutionError):
+    """
+    **4028** Raised when a group is already an external member group
+
+    For example:
+
+    >>> raise AlreadyExternalGroup
+    Traceback (most recent call last):
+      ...
+    AlreadyExternalGroup: This group already allows external members
+
+    """
+
+    errno = 4028
+    format = _('This group already allows external members')
+
+
 class BuiltinError(ExecutionError):
     """
     **4100** Base class for builtin execution errors (*4100 - 4199*).
diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py
index 475222a6a30863bcc536e1746bf5b338a4e42448..a1c8c2fbafec979c36978e04e752e8a20066e949 100644
--- a/ipalib/plugins/baseldap.py
+++ b/ipalib/plugins/baseldap.py
@@ -362,6 +362,9 @@ def add_external_post_callback(memberattr, membertype, externalattr, ldap, compl
     externalattr is one of externaluser,
     """
     completed_external = 0
+    normalize = True
+    if 'external_callback_normalize' in options:
+        normalize = options['external_callback_normalize']
     # Sift through the failures. We assume that these are all
     # entries that aren't stored in IPA, aka external entries.
     if memberattr in failed and membertype in failed[memberattr]:
@@ -373,9 +376,13 @@ def add_external_post_callback(memberattr, membertype, externalattr, ldap, compl
             membername = entry[0].lower()
             member_dn = api.Object[membertype].get_dn(membername)
             if membername not in external_entries and \
-              member_dn not in members:
+               entry[0] not in external_entries and \
+               member_dn not in members:
                 # Not an IPA entry, assume external
-                external_entries.append(membername)
+                if normalize:
+                    external_entries.append(membername)
+                else:
+                    external_entries.append(entry[0])
                 completed_external += 1
             elif membername in external_entries and \
               member_dn not in members:
@@ -409,8 +416,11 @@ def remove_external_post_callback(memberattr, membertype, externalattr, ldap, co
         completed_external = 0
         for entry in failed[memberattr][membertype]:
             membername = entry[0].lower()
-            if membername in external_entries:
-                external_entries.remove(membername)
+            if membername in external_entries or entry[0] in external_entries:
+                try:
+                    external_entries.remove(membername)
+                except ValueError:
+                    external_entries.remove(entry[0])
                 completed_external += 1
             else:
                 failed_entries.append(membername)
diff --git a/ipalib/plugins/group.py b/ipalib/plugins/group.py
index 65657363a463fb0ccb07133c9c84e17b15ffee42..83ec980aa6fed10f1a74e6c20736e130af012c54 100644
--- a/ipalib/plugins/group.py
+++ b/ipalib/plugins/group.py
@@ -22,6 +22,12 @@ from ipalib import api
 from ipalib import Int, Str
 from ipalib.plugins.baseldap import *
 from ipalib import _, ngettext
+if api.env.in_server and api.env.context in ['lite', 'server']:
+    try:
+        import ipaserver.dcerpc
+        _dcerpc_bindings_installed = True
+    except Exception, e:
+        _dcerpc_bindings_installed = False
 
 __doc__ = _("""
 Groups of users
@@ -83,19 +89,18 @@ class group(LDAPObject):
     object_name_plural = _('groups')
     object_class = ['ipausergroup']
     object_class_config = 'ipagroupobjectclasses'
-    possible_objectclasses = ['posixGroup', 'mepManagedEntry']
+    possible_objectclasses = ['posixGroup', 'mepManagedEntry', 'ipaExternalGroup']
     search_attributes_config = 'ipagroupsearchfields'
     default_attributes = [
         'cn', 'description', 'gidnumber', 'member', 'memberof',
-        'memberindirect', 'memberofindirect',
+        'memberindirect', 'memberofindirect', 'ipaexternalmember',
     ]
     uuid_attribute = 'ipauniqueid'
     attribute_members = {
         'member': ['user', 'group'],
         'memberof': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'],
         'memberindirect': ['user', 'group'],
-        'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule',
-        'sudorule'],
+        'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'],
     }
     rdn_is_primary_key = True
 
@@ -139,10 +144,19 @@ class group_add(LDAPCreate):
              doc=_('Create as a non-POSIX group'),
              default=False,
         ),
+        Flag('external',
+             cli_name='external',
+             doc=_('Allow adding external non-IPA members from trusted domains'),
+             default=False,
+        ),
     )
 
     def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
-        if not options['nonposix']:
+        if 'external' in options:
+            entry_attrs['objectclass'].append('ipaexternalgroup')
+            if 'gidnumber' in options:
+                raise errors.RequirementError(name='gid')
+        elif not 'nonposix' in options:
             entry_attrs['objectclass'].append('posixgroup')
             if not 'gidnumber' in options:
                 entry_attrs['gidnumber'] = 999
@@ -194,6 +208,11 @@ class group_mod(LDAPUpdate):
              cli_name='posix',
              doc=_('change to a POSIX group'),
         ),
+        Flag('external',
+             cli_name='external',
+             doc=_('change to support external non-IPA members from trusted domains'),
+             default=False,
+        ),
     )
 
     def pre_callback(self, ldap, dn, entry_attrs, *keys, **options):
@@ -207,6 +226,14 @@ class group_mod(LDAPUpdate):
                 entry_attrs['objectclass'] = old_entry_attrs['objectclass']
                 if not 'gidnumber' in options:
                     entry_attrs['gidnumber'] = 999
+        if options['external'] in options:
+            (dn, old_entry_attrs) = ldap.get_entry(dn, ['objectclass'])
+            if 'ipaexternalgroup' in old_entry_attrs['objectclass']:
+                if options['external']:
+                    raise errors.AlreadyExternalGroup()
+            else:
+                old_entry_attrs['objectclass'].append('ipaexternalgroup')
+                entry_attrs['objectclass'] = old_entry_attrs['objectclass']
         # Can't check for this in a validator because we lack context
         if 'gidnumber' in options and options['gidnumber'] is None:
             raise errors.RequirementError(name='gid')
@@ -274,12 +301,64 @@ api.register(group_show)
 class group_add_member(LDAPAddMember):
     __doc__ = _('Add members to a group.')
 
+    takes_options = (
+        Str('ipaexternalmember*',
+            cli_name='external',
+            label=_('External member'),
+            doc=_('comma-separated SIDs of members of a trusted domain'),
+            csv=True,
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+    )
+
+    def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
+        result = (completed, dn)
+        if 'ipaexternalmember' in options:
+            if not _dcerpc_bindings_installed:
+                raise errors.NotFound(name=_('AD Trust'),
+                      reason=_('''Cannot perform external member validation without Samba 4 support installed.
+                                  Make sure you have installed server-trust-ad sub-package of IPA on the server'''))
+            domain_validator = ipaserver.dcerpc.DomainValidator(self.api)
+            if not domain_validator.is_configured():
+                raise errors.NotFound(name=_('AD Trust setup'),
+                      reason=_('''Cannot perform join operation without own domain configured.
+                                  Make sure you have run ipa-adtrust-install on the IPA server first'''))
+            sids = []
+            failed_sids = []
+            for sid in options['ipaexternalmember']:
+                if domain_validator.is_trusted_sid_valid(sid):
+                    sids.append(sid)
+                else:
+                    failed_sids.append((sid, 'Not a trusted domain SID'))
+            if len(sids) == 0:
+                raise errors.ValidationError(name=_('external member'),
+                                             error=_('values are not recognized as valid SIDs from trusted domain'))
+            restore = []
+            if 'member' in failed and 'group' in failed['member']:
+                restore = failed['member']['group']
+            failed['member']['group'] = list((id,id) for id in sids)
+            result = add_external_post_callback('member', 'group', 'ipaexternalmember',
+                                                ldap, completed, failed, dn, entry_attrs,
+                                                keys, options, external_callback_normalize=False)
+            failed['member']['group'] = restore + failed_sids
+        return result
+
 api.register(group_add_member)
 
 
 class group_remove_member(LDAPRemoveMember):
     __doc__ = _('Remove members from a group.')
 
+    takes_options = (
+        Str('ipaexternalmember*',
+            cli_name='external',
+            label=_('External member'),
+            doc=_('comma-separated SIDs of members of a trusted domain'),
+            csv=True,
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+    )
+
     def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
         if keys[0] == protected_group_name:
             result = api.Command.group_show(protected_group_name)
@@ -290,6 +369,20 @@ class group_remove_member(LDAPRemoveMember):
                     label=_(u'group'), container=protected_group_name)
         return dn
 
+    def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
+        result = (completed, dn)
+        if 'ipaexternalmember' in options:
+            sids = options['ipaexternalmember']
+            restore = list()
+            if 'member' in failed and 'group' in failed['member']:
+                restore = failed['member']['group']
+            failed['member']['group'] = list((id,id) for id in sids)
+            result = remove_external_post_callback('member', 'group', 'ipaexternalmember',
+                                                ldap, completed, failed, dn, entry_attrs,
+                                                keys, options)
+            failed['member']['group'] = restore
+        return result
+
 api.register(group_remove_member)
 
 
diff --git a/ipalib/plugins/trust.py b/ipalib/plugins/trust.py
index 2fd949cd24145a28ebbe909543914b56027e1d45..b313b30d9b16911adea3c607dbff7e5fe30bda1f 100644
--- a/ipalib/plugins/trust.py
+++ b/ipalib/plugins/trust.py
@@ -154,6 +154,10 @@ class trust_add_ad(LDAPCreate):
             realm_server = options['realm_server']
 
         trustinstance = ipaserver.dcerpc.TrustDomainJoins(self.api)
+        if not trustinstance.configured:
+            raise errors.NotFound(name=_('AD Trust setup'),
+                  reason=_('''Cannot perform join operation without own domain configured.
+                              Make sure you have run ipa-adtrust-install on the IPA server first'''))
 
         # 1. Full access to the remote domain. Use admin credentials and
         # generate random trustdom password to do work on both sides
diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
index 3bc8b63af3f416cc45cb75c76fd7b9587f367e3e..ae5f4b632b514929a0cae35db14e0b5d6e484910 100644
--- a/ipaserver/dcerpc.py
+++ b/ipaserver/dcerpc.py
@@ -58,6 +58,79 @@ class ExtendedDNControl(_ldap.controls.RequestControl):
     def encodeControlValue(self):
         return '0\x03\x02\x01\x01'
 
+class DomainValidator(object):
+    ATTR_FLATNAME = 'ipantflatname'
+    ATTR_SID = 'ipantsecurityidentifier'
+    ATTR_TRUSTED_SID = 'ipanttrusteddomainsid'
+
+    def __init__(self, api):
+        self.api = api
+        self.ldap = self.api.Backend.ldap2
+        self.domain = None
+        self.flatname = None
+        self.dn = None
+        self.sid = None
+        self._domains = None
+
+    def is_configured(self):
+        cn_trust_local = DN(('cn', self.api.env.domain), self.api.env.container_cifsdomains, self.api.env.basedn)
+        try:
+            (dn, entry_attrs) = self.ldap.get_entry(unicode(cn_trust_local), [self.ATTR_FLATNAME, self.ATTR_SID])
+            self.flatname = entry_attrs[self.ATTR_FLATNAME][0]
+            self.sid = entry_attrs[self.ATTR_SID][0]
+            self.dn = dn
+            self.domain = self.api.env.domain
+        except errors.NotFound, e:
+            return False
+        return True
+
+    def get_trusted_domains(self):
+        cn_trust = DN(('cn', 'ad'), self.api.env.container_trusts, self.api.env.basedn)
+        try:
+            search_kw = {'objectClass': 'ipaNTTrustedDomain'}
+            filter = self.ldap.make_filter(search_kw, rules=self.ldap.MATCH_ALL)
+            (entries, truncated) = self.ldap.find_entries(filter=filter, base_dn=unicode(cn_trust),
+                                                          attrs_list=[self.ATTR_TRUSTED_SID, 'dn'])
+
+            return entries
+        except errors.NotFound, e:
+            return []
+
+    def is_trusted_sid_valid(self, sid):
+        if not self.domain:
+            # our domain is not configured or self.is_configured() never run
+            # reject SIDs as we can't check correctness of them
+            return False
+        # Parse sid string to see if it is really in a SID format
+        try:
+            test_sid = security.dom_sid(sid)
+        except TypeError:
+            return False
+        (dom, sid_rid) = test_sid.split()
+        sid_dom = str(dom)
+        # Now we have domain prefix of the sid as sid_dom string and can
+        # analyze it against known prefixes
+        if sid_dom.find(security.SID_NT_AUTHORITY) != 0:
+            # Ignore any potential SIDs that are not S-1-5-*
+            return False
+        if sid_dom.find(self.sid) == 0:
+            # A SID from our own domain cannot be treated as trusted domain's SID
+            return False
+        # At this point we have SID_NT_AUTHORITY family SID and really need to
+        # check it against prefixes of domain SIDs we trust to
+        if not self._domains:
+            self._domains = self.get_trusted_domains()
+        if len(self._domains) == 0:
+            # Our domain is configured but no trusted domains are configured
+            # This means we can't check the correctness of a trusted domain SIDs 
+            return False
+        # We have non-zero list of trusted domains and have to go through them
+        # one by one and check their sids as prefixes
+        for (dn, domaininfo) in self._domains:
+            if sid_dom.find(domaininfo[self.ATTR_TRUSTED_SID][0]) == 0:
+                return True
+        return False
+
 class TrustDomainInstance(object):
 
     def __init__(self, hostname, creds=None):
@@ -247,20 +320,18 @@ class TrustDomainInstance(object):
         self._pipe.CreateTrustedDomainEx2(self._policy_handle, info, self.auth_info, security.SEC_STD_DELETE)
 
 class TrustDomainJoins(object):
-    ATTR_FLATNAME = 'ipantflatname'
-
     def __init__(self, api):
         self.api = api
         self.local_domain = None
         self.remote_domain = None
 
-        self.ldap = self.api.Backend.ldap2
-        cn_trust_local = DN(('cn', self.api.env.domain), self.api.env.container_cifsdomains, self.api.env.basedn)
-        (dn, entry_attrs) = self.ldap.get_entry(unicode(cn_trust_local), [self.ATTR_FLATNAME])
-        self.local_flatname = entry_attrs[self.ATTR_FLATNAME][0]
-        self.local_dn = dn
+        domain_validator = DomainValidator(api)
+        self.configured = domain_validator.is_configured()
 
-        self.__populate_local_domain()
+        if self.configured:
+            self.local_flatname = domain_validator.flatname
+            self.local_dn = domain_validator.dn
+            self.__populate_local_domain()
 
     def __populate_local_domain(self):
         # Initialize local domain info using kerberos only
@@ -308,6 +379,9 @@ class TrustDomainJoins(object):
         self.remote_domain = rd
 
     def join_ad_full_credentials(self, realm, realm_server, realm_admin, realm_passwd):
+        if not self.configured:
+            return None
+
         self.__populate_remote_domain(realm, realm_server, realm_admin, realm_passwd)
         if not self.remote_domain.read_only:
             trustdom_pass = samba.generate_random_password(128, 128)
@@ -317,6 +391,9 @@ class TrustDomainJoins(object):
         return None
 
     def join_ad_ipa_half(self, realm, realm_server, trustdom_passwd):
+        if not self.configured:
+            return None
+
         self.__populate_remote_domain(realm, realm_server, realm_passwd=None)
         self.local_domain.establish_trust(self.remote_domain, trustdom_passwd)
         return dict(local=self.local_domain, remote=self.remote_domain)
-- 
1.7.10.2



More information about the Freeipa-devel mailing list