[Freeipa-devel] [PATCH] 0202 support UPNs for trusted domain users

Alexander Bokovoy abokovoy at redhat.com
Thu Jun 9 16:46:42 UTC 2016


On Thu, 09 Jun 2016, Martin Babinsky wrote:
>On 06/07/2016 07:35 PM, Alexander Bokovoy wrote:
>>On Tue, 07 Jun 2016, Martin Babinsky wrote:
>>>On 06/07/2016 06:38 PM, Alexander Bokovoy wrote:
>>>>On Tue, 07 Jun 2016, Martin Babinsky wrote:
>>>>>On 06/06/2016 12:34 PM, Alexander Bokovoy wrote:
>>>>>>Hi,
>>>>>>
>>>>>>Add support for additional user name principal suffixes from
>>>>>>trusted Active Directory forests. UPN suffixes are property
>>>>>>of the forest and as such are associated with the forest root
>>>>>>domain.
>>>>>>
>>>>>>FreeIPA stores UPN suffixes as ipaNTAdditionalSuffixes multi-valued
>>>>>>attribute of ipaNTTrustedDomain object class.
>>>>>>
>>>>>>In order to look up UPN suffixes, netr_DsRGetForestTrustInformation
>>>>>>LSA RPC call is used instead of netr_DsrEnumerateDomainTrusts.
>>>>>>
>>>>>>For more details on UPN and naming in Active Directory see
>>>>>>https://technet.microsoft.com/en-us/library/cc739093%28v=ws.10%29.aspx
>>>>>>
>>>>>>https://fedorahosted.org/freeipa/ticket/5354
>>>>>>
>>>>>>
>>>>>>
>>>>>
>>>>>Hi Alexander,
>>>>>
>>>>>a few comments:
>>>>>
>>>>>1.)
>>>>>
>>>>>there are some PEP8 violations in the patch. Not all of them need to
>>>>>be fixed, though (line overhangs < 5 characters are OK by me).
>>>>>
>>>>>"""
>>>>>./ipaserver/dcerpc.py:1199:80: E501 line too long (80 > 79 characters)
>>>>>./ipaserver/dcerpc.py:1200:80: E501 line too long (83 > 79 characters)
>>>>>./ipaserver/dcerpc.py:1258:40: E203 whitespace before ':'
>>>>>./ipaserver/dcerpc.py:1263:80: E501 line too long (81 > 79 characters)
>>>>>./ipaserver/dcerpc.py:1271:80: E501 line too long (90 > 79 characters)
>>>>>./ipaserver/dcerpc.py:1272:80: E501 line too long (82 > 79 characters)
>>>>>./ipaserver/plugins/trust.py:490:9: E128 continuation line
>>>>>under-indented for visual indent
>>>>>./ipaserver/plugins/trust.py:490:80: E501 line too long (93 > 79
>>>>>characters)
>>>>>./ipaserver/plugins/trust.py:490:92: E202 whitespace before ']'
>>>>>./ipaserver/plugins/trust.py:493:80: E501 line too long (83 > 79
>>>>>characters)
>>>>>./ipaserver/plugins/trust.py:1461:80: E501 line too long (80 > 79
>>>>>characters)
>>>>>./ipaserver/plugins/trust.py:1462:59: E202 whitespace before ']'
>>>>>./ipaserver/plugins/trust.py:1544:1: E302 expected 2 blank lines,
>>>>>found 1
>>>>>./ipaserver/plugins/trust.py:1638:1: E302 expected 2 blank lines,
>>>>>found 1
>>>>>"""
>>>>I've fixed trust.py part, the dcerpc.py fixes in 0201 should be enough
>>>>now -- breaking following line is not giving any reasonable benefit:
>>>>
>>>>          res['ipantflatname'] =
>>>>unicode(t.forest_trust_data.netbios_domain_name.string)
>>>>
>>>
>>>Looking at the code, it would be IMHO more readable to directly append
>>>dict literals to the result like so:
>>>
>>>"""
>>>--- a/ipaserver/dcerpc.py
>>>+++ b/ipaserver/dcerpc.py
>>>@@ -1269,18 +1269,20 @@ def fetch_domains(api, mydomain, trustdomain,
>>>creds=None, server=None):
>>>            tname = unicode(t.forest_trust_data.dns_domain_name.string)
>>>            if tname == trustdomain:
>>>                continue
>>>-            res = dict()
>>>-            res['cn'] = tname
>>>-            res['ipantflatname'] =
>>>unicode(t.forest_trust_data.netbios_domain_name.string)
>>>-            res['ipanttrusteddomainsid'] =
>>>unicode(t.forest_trust_data.domain_sid)
>>>-            result['domains'][tname] = res
>>>+
>>>+            result['domains'][tname] = {
>>>+                'cn': tname,
>>>+                'ipantflatname': unicode(
>>>+                    t.forest_trust_data.netbios_domain_name.string),
>>>+                'ipanttrusteddomainsid': unicode(
>>>+                    t.forest_trust_data.domain_sid)
>>>+            }
>>>        elif t.type == lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME:
>>>            tname = unicode(t.forest_trust_data.string)
>>>            if tname == trustdomain:
>>>                continue
>>>-            res = dict()
>>>-            res['cn'] = tname
>>>-            result['suffixes'][tname] = res
>>>+
>>>+            result['suffixes'][tname] = {'cn': tname}
>>>    return result
>>>"""
>>Makes sense. Fixed.
>>
>>>
>>>Also there is a whitespace before colon here:
>>>"""
>>>+    result = {'domains': {}, 'suffixes' : {}}
>>>                                       ^
>>>"""
>>>Please fix that and I will be a happy engineer.
>>Fixed.
>>
>>>
>>>>>2.)
>>>>>
>>>>>Should the ipaNTAdditionalSuffixes attribute be case insensitive? It
>>>>>makes sense but I'm just asking so that we don't end changing the
>>>>>schema later.
>>>>ipaNTAdditionalSuffixes is defined as
>>>>
>>>>attributeTypes: ( 2.16.840.1.113730.3.8.11.75 NAME
>>>>'ipaNTAdditionalSuffixes' DESC 'Suffix for the user principal name
>>>>associated with the domain' EQUALITY caseIgnoreMatch SYNTAX
>>>>1.3.6.1.4.1.1466.115.121.1.15)
>>>>
>>>>There should be no problem with case sensitivity already.
>>>>
>>>OK I presume that the UPN suffixes on AD are also case-insensitive so
>>>everything should be alright.
>>Yes, as everything realm-related on AD side, they are case-insensitive.
>>
>>Updated patch attached.
>
>1.) there is one unused import that makes pylint choke:
>
>"""
>************* Module com.redhat.idm
>install/oddjob/com.redhat.idm.trust-fetch-domains:6: 
>[W0611(unused-import), ] Unused errors imported from ipalib)
>Makefile:137: recipe for target 'lint' failed
>make: *** [lint] Error 1
>"""
Fixed, thanks. I wasn't attentive to why it considered that error.

>2.) The UPN suffixes are added during trust establishment and I can 
>also kinit as the enterprise principal using one of UPNs, but only 
>when using two-way trust. Is this expected? I was not able to find any 
>clue in the design page but maybe I was just not looking hard enough.
No, it works just fine for one-way trust as well. The way UPNs used in
AD is a bit awkward -- you define them globally for the whole forest
first and then assign specific UPN suffix for the user account in the
account properties. Only then you can use the suffix for this user:

[root at f24-master ~]# net ads search -S w12.ad.test -k cn=grumpy |grep userPrincipalName
userPrincipalName: grumpy at torchwood

[root at f24-master ~]# KRB5_TRACE=/dev/stderr kinit -E -C grumpy at torchwood
[9135] 1465490159.655327: Resolving unique ccache of type KEYRING
[9135] 1465490159.655408: Getting initial credentials for grumpy\@torchwood at IPA.AD.TEST
[9135] 1465490159.656798: Sending request (176 bytes) to IPA.AD.TEST
[9135] 1465490159.656994: Initiating TCP connection to stream 192.168.5.117:88
[9135] 1465490159.657122: Sending TCP request to stream 192.168.5.117:88
[9135] 1465490159.657655: Received answer (169 bytes) from stream 192.168.5.117:88
[9135] 1465490159.657679: Terminating TCP connection to stream 192.168.5.117:88
[9135] 1465490159.657715: Response was from master KDC
[9135] 1465490159.657741: Received error from KDC: -1765328316/Realm not local to KDC
[9135] 1465490159.657756: Following referral to realm AD.TEST
[9135] 1465490159.657783: Sending request (168 bytes) to AD.TEST
[9135] 1465490159.674764: Resolving hostname w12.ad.test.
[9135] 1465490159.679514: Sending initial UDP request to dgram 192.168.5.111:88
[9135] 1465490159.680158: Received answer (171 bytes) from dgram 192.168.5.111:88
[9135] 1465490159.688085: Response was not from master KDC
[9135] 1465490159.688141: Received error from KDC: -1765328359/Additional pre-authentication required
[9135] 1465490159.688203: Processing preauth types: 16, 15, 19, 2
[9135] 1465490159.688252: Selected etype info: etype aes256-cts, salt "AD.TESTgrumpy", params ""
[9135] 1465490159.688285: PKINIT client has no configured identity; giving up
[9135] 1465490159.688315: PKINIT client has no configured identity; giving up
[9135] 1465490159.688342: Preauth module pkinit (16) (real) returned: 22/Invalid argument
[9135] 1465490159.688368: PKINIT client has no configured identity; giving up
[9135] 1465490159.688394: Preauth module pkinit (14) (real) returned: 22/Invalid argument
Password for grumpy\@torchwood at AD.TEST: 
[9135] 1465490162.317092: AS key obtained for encrypted timestamp: aes256-cts/C61F
[9135] 1465490162.317176: Encrypted timestamp (for 1465490162.505923): plain 301AA011180F32303136303630393136333630325AA105020307B843, encrypted ADD327ACC03C99C821D9FBBE280866D6342D3CE1BBA2C720CF5A5BEBA586ECD0034CD2C3448F5FD6D66219DB77DC51D81C8C1D6D01A87AC9
[9135] 1465490162.317228: Preauth module encrypted_timestamp (2) (real) returned: 0/Success
[9135] 1465490162.317272: Produced preauth for next request: 2
[9135] 1465490162.317328: Sending request (246 bytes) to AD.TEST
[9135] 1465490162.346793: Resolving hostname w12.ad.test.
[9135] 1465490162.352714: Sending initial UDP request to dgram 192.168.5.111:88
[9135] 1465490162.353537: Received answer (1442 bytes) from dgram 192.168.5.111:88
[9135] 1465490162.366042: Response was not from master KDC
[9135] 1465490162.366094: Processing preauth types: 19
[9135] 1465490162.366139: Selected etype info: etype aes256-cts, salt "AD.TESTgrumpy", params ""
[9135] 1465490162.366190: Produced preauth for next request: (empty)
[9135] 1465490162.366235: AS key determined by preauth: aes256-cts/C61F
[9135] 1465490162.366328: Decrypted AS reply; session key is: aes256-cts/BD45
[9135] 1465490162.366382: FAST negotiation: unavailable
[9135] 1465490162.366416: Initializing KEYRING:persistent:0:krb_ccache_dSjqZkD with default princ grumpy at AD.TEST
[9135] 1465490162.366488: Storing grumpy at AD.TEST -> krbtgt/AD.TEST at AD.TEST in KEYRING:persistent:0:krb_ccache_dSjqZkD
[9135] 1465490162.366550: Storing config in KEYRING:persistent:0:krb_ccache_dSjqZkD for krbtgt/AD.TEST at AD.TEST: pa_type: 2
[9135] 1465490162.366585: Storing grumpy at AD.TEST -> krb5_ccache_conf_data/pa_type/krbtgt\/AD.TEST\@AD.TEST at X-CACHECONF: in KEYRING:persistent:0:krb_ccache_dSjqZkD

As you can see, the realm was considered non-local and a referral to the
correct realm was issued, then kinit followed the referral and finally
we've got the ticket for grumpy at AD.TEST because canonicalization was
asked. If no canonicalization would be asked, the principal would be
enterprise one but, again, from the other realm:

[root at f24-master ~]# klist
Ticket cache: KEYRING:persistent:0:krb_ccache_lIqrVtl
Default principal: grumpy\@torchwood at AD.TEST

Valid starting       Expires              Service principal
06/09/2016 19:45:16  06/10/2016 05:45:16  krbtgt/AD.TEST at AD.TEST
	renew until 06/10/2016 19:45:13



-- 
/ Alexander Bokovoy
-------------- next part --------------
From 366242a58d476ae34d46629b779a8e9d7df29195 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy at redhat.com>
Date: Mon, 6 Jun 2016 11:41:46 +0300
Subject: [PATCH 2/8] adtrust: support UPNs for trusted domain users

Add support for additional user name principal suffixes from
trusted Active Directory forests. UPN suffixes are property
of the forest and as such are associated with the forest root
domain.

FreeIPA stores UPN suffixes as ipaNTAdditionalSuffixes multi-valued
attribute of ipaNTTrustedDomain object class.

In order to look up UPN suffixes, netr_DsRGetForestTrustInformation
LSA RPC call is used instead of netr_DsrEnumerateDomainTrusts.

For more details on UPN and naming in Active Directory see
https://technet.microsoft.com/en-us/library/cc739093%28v=ws.10%29.aspx

https://fedorahosted.org/freeipa/ticket/5354
---
 ACI.txt                                            |  2 +-
 daemons/ipa-kdb/ipa_kdb_mspac.c                    | 60 +++++++++++++++----
 daemons/ipa-kdb/ipa_kdb_mspac_private.h            |  1 +
 .../ipa-extdom-extop/ipa_extdom_common.c           |  9 ++-
 install/oddjob/com.redhat.idm.trust-fetch-domains  | 31 +---------
 install/share/60basev3.ldif                        |  3 +-
 ipaserver/dcerpc.py                                | 40 +++++++++----
 ipaserver/plugins/trust.py                         | 69 ++++++++++++++++------
 8 files changed, 142 insertions(+), 73 deletions(-)

diff --git a/ACI.txt b/ACI.txt
index a09495e..6f691f2 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -309,7 +309,7 @@ aci: (targetattr = "cmdcategory || cn || createtimestamp || description || entry
 dn: dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || description || entryusn || modifytimestamp || objectclass || ou || sudocommand || sudohost || sudonotafter || sudonotbefore || sudooption || sudoorder || sudorunas || sudorunasgroup || sudorunasuser || sudouser")(target = "ldap:///ou=sudoers,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read Sudoers compat tree";allow (compare,read,search) userdn = "ldap:///anyone";)
 dn: cn=trusts,dc=ipa,dc=example
-aci: (targetattr = "cn || createtimestamp || entryusn || ipantflatname || ipantsecurityidentifier || ipantsidblacklistincoming || ipantsidblacklistoutgoing || ipanttrustdirection || ipanttrusteddomainsid || ipanttrustpartner || modifytimestamp || objectclass")(version 3.0;acl "permission:System: Read Trust Information";allow (compare,read,search) userdn = "ldap:///all";)
+aci: (targetattr = "cn || createtimestamp || entryusn || ipantadditionalsuffixes || ipantflatname || ipantsecurityidentifier || ipantsidblacklistincoming || ipantsidblacklistoutgoing || ipanttrustdirection || ipanttrusteddomainsid || ipanttrustpartner || modifytimestamp || objectclass")(version 3.0;acl "permission:System: Read Trust Information";allow (compare,read,search) userdn = "ldap:///all";)
 dn: cn=trusts,dc=ipa,dc=example
 aci: (targetattr = "gidnumber || krbprincipalname || uidnumber")(version 3.0;acl "permission:System: Read system trust accounts";allow (compare,read,search) groupdn = "ldap:///cn=System: Read system trust accounts,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=groups,cn=accounts,dc=ipa,dc=example
diff --git a/daemons/ipa-kdb/ipa_kdb_mspac.c b/daemons/ipa-kdb/ipa_kdb_mspac.c
index d54ca71..80e7055 100644
--- a/daemons/ipa-kdb/ipa_kdb_mspac.c
+++ b/daemons/ipa-kdb/ipa_kdb_mspac.c
@@ -2273,7 +2273,7 @@ static char *get_server_netbios_name(struct ipadb_context *ipactx)
 
 void ipadb_mspac_struct_free(struct ipadb_mspac **mspac)
 {
-    int i;
+    int i, j;
 
     if (!*mspac) return;
 
@@ -2290,6 +2290,12 @@ void ipadb_mspac_struct_free(struct ipadb_mspac **mspac)
             free((*mspac)->trusts[i].sid_blacklist_outgoing);
             free((*mspac)->trusts[i].parent_name);
             (*mspac)->trusts[i].parent = NULL;
+            if ((*mspac)->trusts[i].upn_suffixes) {
+                for (j = 0; (*mspac)->trusts[i].upn_suffixes[j]; j++) {
+                    free((*mspac)->trusts[i].upn_suffixes[j]);
+                }
+                free((*mspac)->trusts[i].upn_suffixes);
+            }
         }
         free((*mspac)->trusts);
     }
@@ -2405,7 +2411,7 @@ krb5_error_code ipadb_mspac_get_trusted_domains(struct ipadb_context *ipactx)
     LDAP *lc = ipactx->lcontext;
     char *attrs[] = { "cn", "ipaNTTrustPartner", "ipaNTFlatName",
                       "ipaNTTrustedDomainSID", "ipaNTSIDBlacklistIncoming",
-                      "ipaNTSIDBlacklistOutgoing", NULL };
+                      "ipaNTSIDBlacklistOutgoing", "ipaNTAdditionalSuffixes", NULL };
     char *filter = "(objectclass=ipaNTTrustedDomain)";
     krb5_error_code kerr;
     LDAPMessage *res = NULL;
@@ -2462,26 +2468,42 @@ krb5_error_code ipadb_mspac_get_trusted_domains(struct ipadb_context *ipactx)
             goto done;
         }
 
+        t[n].flat_name = NULL;
         ret = ipadb_ldap_attr_to_str(lc, le, "ipaNTFlatName",
                                      &t[n].flat_name);
-        if (ret) {
+        if (ret && ret != ENOENT) {
             ret = EINVAL;
             goto done;
         }
 
+        t[n].domain_sid = NULL;
         ret = ipadb_ldap_attr_to_str(lc, le, "ipaNTTrustedDomainSID",
                                      &t[n].domain_sid);
-        if (ret) {
+        if (ret && ret != ENOENT) {
             ret = EINVAL;
             goto done;
         }
 
         ret = string_to_sid(t[n].domain_sid, &t[n].domsid);
-        if (ret) {
+        if (ret && t[n].domain_sid != NULL) {
             ret = EINVAL;
             goto done;
         }
 
+        ret = ipadb_ldap_attr_to_strlist(lc, le, "ipaNTAdditionalSuffixes",
+                                         &t[n].upn_suffixes);
+
+        if (ret) {
+            if (ret == ENOENT) {
+                /* This attribute is optional */
+                ret = 0;
+                t[n].upn_suffixes = NULL;
+            } else {
+                ret = EINVAL;
+                goto done;
+            }
+        }
+
         ret = ipadb_ldap_attr_to_strlist(lc, le, "ipaNTSIDBlacklistIncoming",
                                          &sid_blacklist_incoming);
 
@@ -2808,6 +2830,7 @@ krb5_error_code ipadb_is_princ_from_trusted_realm(krb5_context kcontext,
 	struct ipadb_context *ipactx;
 	int i, j, length;
 	const char *name;
+	bool result = false;
 
 	if (test_realm == NULL || test_realm[0] == '\0') {
 		return KRB5_KDB_NOENTRY;
@@ -2829,12 +2852,27 @@ krb5_error_code ipadb_is_princ_from_trusted_realm(krb5_context kcontext,
 
 	/* Iterate through list of trusts and check if input realm belongs to any of the trust */
 	for(i = 0 ; i < ipactx->mspac->num_trusts ; i++) {
-		if ((strncasecmp(test_realm,
-				 ipactx->mspac->trusts[i].domain_name,
-				 size) == 0) ||
-		    (strncasecmp(test_realm,
-				 ipactx->mspac->trusts[i].flat_name,
-				 size) == 0)) {
+		result = strncasecmp(test_realm,
+				     ipactx->mspac->trusts[i].domain_name,
+				     size) == 0;
+
+                if (!result && (ipactx->mspac->trusts[i].flat_name != NULL)) {
+			result = strncasecmp(test_realm,
+					     ipactx->mspac->trusts[i].flat_name,
+					     size) == 0;
+		}
+
+		if (!result && (ipactx->mspac->trusts[i].upn_suffixes != NULL)) {
+			for (j = 0; ipactx->mspac->trusts[i].upn_suffixes[j]; j++) {
+				result = strncasecmp(test_realm,
+						     ipactx->mspac->trusts[i].upn_suffixes[j],
+						     size) == 0;
+				if (result)
+					break;
+			}
+		}
+
+		if (result) {
 			/* return the realm if caller supplied a place for it */
 			if (trusted_realm != NULL) {
 				name = (ipactx->mspac->trusts[i].parent_name != NULL) ?
diff --git a/daemons/ipa-kdb/ipa_kdb_mspac_private.h b/daemons/ipa-kdb/ipa_kdb_mspac_private.h
index 1052bb8..30382d2 100644
--- a/daemons/ipa-kdb/ipa_kdb_mspac_private.h
+++ b/daemons/ipa-kdb/ipa_kdb_mspac_private.h
@@ -47,6 +47,7 @@ struct ipadb_adtrusts {
     int len_sid_blacklist_outgoing;
     struct ipadb_adtrusts *parent;
     char *parent_name;
+    char **upn_suffixes;
 };
 
 int string_to_sid(const char *str, struct dom_sid *sid);
diff --git a/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_common.c b/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_common.c
index 445624f..823c05c 100644
--- a/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_common.c
+++ b/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_common.c
@@ -1011,8 +1011,13 @@ static int handle_name_request(struct ipa_extdom_ctx *ctx,
     char *buf = NULL;
     struct sss_nss_kv *kv_list = NULL;
 
-    ret = asprintf(&fq_name, "%s%c%s", name, SSSD_DOMAIN_SEPARATOR,
-                                       domain_name);
+    if (strchr(name, SSSD_DOMAIN_SEPARATOR) == NULL) {
+        ret = asprintf(&fq_name, "%s%c%s", name, SSSD_DOMAIN_SEPARATOR,
+                                           domain_name);
+    } else {
+        /* SSSD_DOMAIN_SEPARATOR already present, assume UPN */
+        ret = asprintf(&fq_name, "%s", name);
+    }
     if (ret == -1) {
         ret = LDAP_OPERATIONS_ERROR;
         set_err_msg(req, "Failed to create fully qualified name");
diff --git a/install/oddjob/com.redhat.idm.trust-fetch-domains b/install/oddjob/com.redhat.idm.trust-fetch-domains
index ea00f30..57b15f7 100755
--- a/install/oddjob/com.redhat.idm.trust-fetch-domains
+++ b/install/oddjob/com.redhat.idm.trust-fetch-domains
@@ -3,7 +3,7 @@
 from ipaserver import dcerpc
 from ipaserver.install.installutils import is_ipa_configured, ScriptError
 from ipapython import config, ipautil
-from ipalib import api, errors
+from ipalib import api
 from ipapython.dn import DN
 from ipalib.config import Env
 from ipalib.constants import DEFAULT_CONFIG
@@ -170,33 +170,8 @@ except gssapi.exceptions.GSSError:
 ipa_domain = api.env.domain
 os.environ['KRB5CCNAME'] = oneway_ccache_name
 domains = dcerpc.fetch_domains(api, ipa_domain, trusted_domain, creds=True)
-
-if domains:
-    # trust range must exist by the time fetch_domains_from_trust is called
-    range_name = unicode(trusted_domain.upper() + '_id_range')
-    old_range = api.Command.idrange_show(range_name, raw=True)['result']
-    idrange_type = old_range['iparangetype'][0]
-
-    result = []
-    for dom in domains:
-        dom['trust_type'] = u'ad'
-        try:
-            name = dom['cn']
-            del dom['cn']
-
-            res = api.Command.trustdomain_add(trusted_domain, name, **dom)
-            result.append(res['result'])
-
-            if idrange_type != u'ipa-ad-trust-posix':
-                range_name = name.upper() + '_id_range'
-                dom['range_type'] = u'ipa-ad-trust'
-                # Do not pass ipaserver.dcerpc.TrustInstance to trust.add_range
-                # to force it using existing credentials cache
-                trust.add_range(api, None, range_name, dom['ipanttrusteddomainsid'],
-                                trusted_domain, name, **dom)
-        except errors.DuplicateEntry:
-            # Ignore updating duplicate entries
-            pass
+trust_domain_object = api.Command.trust_show(trusted_domain, raw=True)['result']
+trust.add_new_domains_from_trust(api, None, trust_domain_object, domains)
 
 if old_ccache:
    os.environ['KRB5CCNAME'] = old_ccache
diff --git a/install/share/60basev3.ldif b/install/share/60basev3.ldif
index 5ebe335..059174b 100644
--- a/install/share/60basev3.ldif
+++ b/install/share/60basev3.ldif
@@ -56,6 +56,7 @@ attributeTypes: (2.16.840.1.113730.3.8.11.64 NAME 'ipaSecretKeyRef' DESC 'DN of
 attributeTypes: (2.16.840.1.113730.3.8.11.65 NAME 'ipaWrappingMech' DESC 'PKCS#11 wrapping mechanism equivalent to CK_MECHANISM_TYPE' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v4.1')
 attributeTypes: (2.16.840.1.113730.3.8.11.70 NAME 'ipaPermTargetTo' DESC 'Destination location to move an entry IPA permission ACI' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.0' )
 attributeTypes: (2.16.840.1.113730.3.8.11.71 NAME 'ipaPermTargetFrom' DESC 'Source location from where moving an entry IPA permission ACI' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.0' )
+attributeTypes: ( 2.16.840.1.113730.3.8.11.75 NAME 'ipaNTAdditionalSuffixes' DESC 'Suffix for the user principal name associated with the domain' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15)
 attributeTypes: (2.16.840.1.113730.3.8.18.2.1 NAME 'ipaVaultType' DESC 'IPA vault type' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.2')
 attributeTypes: (2.16.840.1.113730.3.8.18.2.2 NAME 'ipaVaultSalt' DESC 'IPA vault salt' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'IPA v4.2' )
 # FIXME: https://bugzilla.redhat.com/show_bug.cgi?id=1267782
@@ -64,7 +65,7 @@ objectClasses: (2.16.840.1.113730.3.8.12.1 NAME 'ipaExternalGroup' SUP top STRUC
 objectClasses: (2.16.840.1.113730.3.8.12.2 NAME 'ipaNTUserAttrs' SUP top AUXILIARY MUST ( ipaNTSecurityIdentifier ) MAY ( ipaNTHash $ ipaNTLogonScript $ ipaNTProfilePath $ ipaNTHomeDirectory $ ipaNTHomeDirectoryDrive ) X-ORIGIN 'IPA v3' )
 objectClasses: (2.16.840.1.113730.3.8.12.3 NAME 'ipaNTGroupAttrs' SUP top AUXILIARY MUST ( ipaNTSecurityIdentifier ) X-ORIGIN 'IPA v3' )
 objectClasses: (2.16.840.1.113730.3.8.12.4 NAME 'ipaNTDomainAttrs' SUP top AUXILIARY MUST ( ipaNTSecurityIdentifier $ ipaNTFlatName $ ipaNTDomainGUID ) MAY ( ipaNTFallbackPrimaryGroup ) X-ORIGIN 'IPA v3' )
-objectClasses: (2.16.840.1.113730.3.8.12.5 NAME 'ipaNTTrustedDomain' SUP top STRUCTURAL DESC 'Trusted Domain Object' MUST ( cn ) MAY ( ipaNTTrustType $ ipaNTTrustAttributes $ ipaNTTrustDirection $ ipaNTTrustPartner $ ipaNTFlatName $ ipaNTTrustAuthOutgoing $ ipaNTTrustAuthIncoming $ ipaNTTrustedDomainSID $ ipaNTTrustForestTrustInfo $ ipaNTTrustPosixOffset $ ipaNTSupportedEncryptionTypes $ ipaNTSIDBlacklistIncoming $ ipaNTSIDBlacklistOutgoing) )
+objectClasses: (2.16.840.1.113730.3.8.12.5 NAME 'ipaNTTrustedDomain' SUP top STRUCTURAL DESC 'Trusted Domain Object' MUST ( cn ) MAY ( ipaNTTrustType $ ipaNTTrustAttributes $ ipaNTTrustDirection $ ipaNTTrustPartner $ ipaNTFlatName $ ipaNTTrustAuthOutgoing $ ipaNTTrustAuthIncoming $ ipaNTTrustedDomainSID $ ipaNTTrustForestTrustInfo $ ipaNTTrustPosixOffset $ ipaNTSupportedEncryptionTypes $ ipaNTSIDBlacklistIncoming $ ipaNTSIDBlacklistOutgoing $ ipaNTAdditionalSuffixes) )
 objectClasses: (2.16.840.1.113730.3.8.12.6 NAME 'groupOfPrincipals' SUP top AUXILIARY MUST ( cn ) MAY ( memberPrincipal ) X-ORIGIN 'IPA v3' )
 objectClasses: (2.16.840.1.113730.3.8.12.7 NAME 'ipaKrb5DelegationACL' SUP groupOfPrincipals STRUCTURAL MAY ( ipaAllowToImpersonate $ ipaAllowedTarget ) X-ORIGIN 'IPA v3' )
 objectClasses: (2.16.840.1.113730.3.8.12.10 NAME 'ipaSELinuxUserMap' SUP ipaAssociation STRUCTURAL MUST ipaSELinuxUser MAY ( accessTime $ seeAlso ) X-ORIGIN 'IPA v3')
diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
index 5f56643..dc25ab7 100644
--- a/ipaserver/dcerpc.py
+++ b/ipaserver/dcerpc.py
@@ -1197,7 +1197,10 @@ def fetch_domains(api, mydomain, trustdomain, creds=None, server=None):
     def communicate(td):
         td.init_lsa_pipe(td.info['dc'])
         netr_pipe = netlogon.netlogon(td.binding, td.parm, td.creds)
-        domains = netr_pipe.netr_DsrEnumerateDomainTrusts(td.binding, 1)
+        # Older FreeIPA versions used netr_DsrEnumerateDomainTrusts call
+        # but it doesn't provide information about non-domain UPNs associated
+        # with the forest, thus we have to use netr_DsRGetForestTrustInformation
+        domains = netr_pipe.netr_DsRGetForestTrustInformation(td.info['dc'], '', 0)
         return domains
 
     domains = None
@@ -1225,6 +1228,7 @@ def fetch_domains(api, mydomain, trustdomain, creds=None, server=None):
         raise assess_dcerpc_exception(message=str(e))
 
     td.info['dc'] = unicode(result.pdc_dns_name)
+    td.info['name'] = unicode(result.dns_domain)
     if type(creds) is bool:
         # Rely on existing Kerberos credentials in the environment
         td.creds = credentials.Credentials()
@@ -1254,16 +1258,30 @@ def fetch_domains(api, mydomain, trustdomain, creds=None, server=None):
     if domains is None:
         return None
 
-    result = []
-    for t in domains.array:
-        if (not (t.trust_flags & trust_flags['NETR_TRUST_FLAG_PRIMARY']) and
-            (t.trust_flags & trust_flags['NETR_TRUST_FLAG_IN_FOREST'])):
-            res = dict()
-            res['cn'] = unicode(t.dns_name)
-            res['ipantflatname'] = unicode(t.netbios_name)
-            res['ipanttrusteddomainsid'] = unicode(t.sid)
-            res['ipanttrustpartner'] = res['cn']
-            result.append(res)
+    result = {'domains': {}, 'suffixes': {}}
+    # netr_DsRGetForestTrustInformation returns two types of entries:
+    # domain information  -- name, NetBIOS name, SID of the domain
+    # top level name info -- a name suffix associated with the forest
+    # We should ignore forest root name/name suffix as it is already part
+    # of trust information for IPA purposes and only add what's inside the forest
+    for t in domains.entries:
+        if t.type == lsa.LSA_FOREST_TRUST_DOMAIN_INFO:
+            tname = unicode(t.forest_trust_data.dns_domain_name.string)
+            if tname == trustdomain:
+                continue
+            result['domains'][tname] = {
+                'cn': tname,
+                'ipantflatname': unicode(
+                    t.forest_trust_data.netbios_domain_name.string),
+                'ipanttrusteddomainsid': unicode(
+                    t.forest_trust_data.domain_sid)
+            }
+        elif t.type == lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+            tname = unicode(t.forest_trust_data.string)
+            if tname == trustdomain:
+                continue
+
+            result['suffixes'][tname] = {'cn': tname}
     return result
 
 
diff --git a/ipaserver/plugins/trust.py b/ipaserver/plugins/trust.py
index 744be93..0f1bb00 100644
--- a/ipaserver/plugins/trust.py
+++ b/ipaserver/plugins/trust.py
@@ -487,11 +487,16 @@ class trust(LDAPObject):
     object_name_plural = _('trusts')
     object_class = ['ipaNTTrustedDomain']
     default_attributes = ['cn', 'ipantflatname', 'ipanttrusteddomainsid',
-        'ipanttrusttype', 'ipanttrustattributes', 'ipanttrustdirection',
-        'ipanttrustpartner', 'ipanttrustforesttrustinfo',
-        'ipanttrustposixoffset', 'ipantsupportedencryptiontypes' ]
+                          'ipanttrusttype', 'ipanttrustattributes',
+                          'ipanttrustdirection', 'ipanttrustpartner',
+                          'ipanttrustforesttrustinfo',
+                          'ipanttrustposixoffset',
+                          'ipantsupportedencryptiontypes',
+                          'ipantadditionalsuffixes']
     search_display_attributes = ['cn', 'ipantflatname',
-                                 'ipanttrusteddomainsid', 'ipanttrusttype']
+                                 'ipanttrusteddomainsid', 'ipanttrusttype',
+                                 'ipanttrustattributes',
+                                 'ipantadditionalsuffixes']
     managed_permissions = {
         'System: Read Trust Information': {
             # Allow reading of attributes needed for SSSD subdomains support
@@ -505,7 +510,7 @@ class trust(LDAPObject):
                 'ipantflatname', 'ipantsecurityidentifier',
                 'ipanttrusteddomainsid', 'ipanttrustpartner',
                 'ipantsidblacklistincoming', 'ipantsidblacklistoutgoing',
-                'ipanttrustdirection'
+                'ipanttrustdirection', 'ipantadditionalsuffixes',
             },
         },
 
@@ -1457,8 +1462,11 @@ class trustdomain(LDAPObject):
     object_name = _('trust domain')
     object_name_plural = _('trust domains')
     object_class = ['ipaNTTrustedDomain']
-    default_attributes = ['cn', 'ipantflatname', 'ipanttrusteddomainsid', 'ipanttrustpartner']
-    search_display_attributes = ['cn', 'ipantflatname', 'ipanttrusteddomainsid', ]
+    default_attributes = ['cn', 'ipantflatname', 'ipanttrusteddomainsid',
+                          'ipanttrustpartner', 'ipantadditionalsuffixes']
+    search_display_attributes = ['cn', 'ipantflatname',
+                                 'ipanttrusteddomainsid',
+                                 'ipantadditionalsuffixes']
 
     label = _('Trusted domains')
     label_singular = _('Trusted domain')
@@ -1500,9 +1508,10 @@ class trustdomain(LDAPObject):
 class trustdomain_find(LDAPSearch):
     __doc__ = _('Search domains of the trust')
 
-    has_output_params = LDAPSearch.has_output_params + (
+    has_output_params = LDAPSearch.has_output_params + trust_output_params + (
         Flag('domain_enabled', label= _('Domain enabled')),
     )
+
     def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options):
         return (filters, base_dn, ldap.SCOPE_SUBTREE)
 
@@ -1511,11 +1520,10 @@ class trustdomain_find(LDAPSearch):
             return truncated
         trust_dn = self.obj.get_dn(args[0], trust_type=u'ad')
         trust_entry = ldap.get_entry(trust_dn)
+        blacklist = trust_entry.get('ipantsidblacklistincoming')
         for entry in entries:
-            sid = entry['ipanttrusteddomainsid'][0]
-
-            blacklist = trust_entry.get('ipantsidblacklistincoming')
-            if blacklist is None:
+            sid = entry.get('ipanttrusteddomainsid', [None])[0]
+            if sid is None:
                 continue
 
             if sid in blacklist:
@@ -1540,10 +1548,12 @@ class trustdomain_add(LDAPCreate):
 
     takes_options = LDAPCreate.takes_options + (_trust_type_option,)
     def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
-        if 'ipanttrustpartner' in options:
-            entry_attrs['ipanttrustpartner'] = [options['ipanttrustpartner']]
+        # ipaNTTrustPartner must always be set to the name of the trusted domain
+        # See MS-ADTS 6.1.6.7.13
+        entry_attrs['ipanttrustpartner'] = [dn[0]['cn']]
         return dn
 
+
 @register()
 class trustdomain_del(LDAPDelete):
     __doc__ = _('Remove infromation about the domain associated with the trust.')
@@ -1586,9 +1596,11 @@ def fetch_domains_from_trust(myapi, trustinstance, trust_entry, **options):
     server = options.get('realm_server', None)
     domains = ipaserver.dcerpc.fetch_domains(myapi,
                                              trustinstance.local_flatname,
-                                             trust_name, creds=creds, server=server)
+                                             trust_name, creds=creds,
+                                             server=server)
     return domains
 
+
 def add_new_domains_from_trust(myapi, trustinstance, trust_entry, domains, **options):
     result = []
     if not domains:
@@ -1600,7 +1612,12 @@ def add_new_domains_from_trust(myapi, trustinstance, trust_entry, domains, **opt
     old_range = myapi.Command.idrange_show(range_name, raw=True)['result']
     idrange_type = old_range['iparangetype'][0]
 
-    for dom in domains:
+    suffixes = list()
+    suffixes.extend(y['cn']
+                    for x, y in six.iteritems(domains['suffixes'])
+                    if x not in domains['domains'])
+
+    for dom in six.itervalues(domains['domains']):
         dom['trust_type'] = u'ad'
         try:
             name = dom['cn']
@@ -1616,13 +1633,27 @@ def add_new_domains_from_trust(myapi, trustinstance, trust_entry, domains, **opt
             if idrange_type != u'ipa-ad-trust-posix':
                 range_name = name.upper() + '_id_range'
                 dom['range_type'] = u'ipa-ad-trust'
-                add_range(myapi, trustinstance, range_name, dom['ipanttrusteddomainsid'],
+                add_range(myapi, trustinstance,
+                          range_name, dom['ipanttrusteddomainsid'],
                           trust_name, name, **dom)
         except errors.DuplicateEntry:
             # Ignore updating duplicate entries
             pass
+
+    try:
+        dn = myapi.Object.trust.get_dn(trust_name, trust_type=u'ad')
+        ldap = myapi.Backend.ldap2
+        entry = ldap.get_entry(dn)
+        tlns = entry.get('ipantadditionalsuffixes', [])
+        tlns.extend(x for x in suffixes if x not in tlns)
+        entry['ipantadditionalsuffixes'] = tlns
+        ldap.update_entry(entry)
+    except errors.EmptyModlist:
+        pass
+
     return result
 
+
 @register()
 class trust_fetch_domains(LDAPRetrieve):
     __doc__ = _('Refresh list of the domains associated with the trust')
@@ -1698,7 +1729,7 @@ class trustdomain_enable(LDAPQuery):
         dn = self.obj.get_dn(keys[0], keys[1], trust_type=u'ad')
         try:
             entry = ldap.get_entry(dn)
-            sid = entry['ipanttrusteddomainsid'][0]
+            sid = entry.single_value.get('ipanttrusteddomainsid', None)
             if sid in trust_entry['ipantsidblacklistincoming']:
                 trust_entry['ipantsidblacklistincoming'].remove(sid)
                 ldap.update_entry(trust_entry)
@@ -1739,7 +1770,7 @@ class trustdomain_disable(LDAPQuery):
         dn = self.obj.get_dn(keys[0], keys[1], trust_type=u'ad')
         try:
             entry = ldap.get_entry(dn)
-            sid = entry['ipanttrusteddomainsid'][0]
+            sid = entry.single_value.get('ipanttrusteddomainsid', None)
             if not (sid in trust_entry['ipantsidblacklistincoming']):
                 trust_entry['ipantsidblacklistincoming'].append(sid)
                 ldap.update_entry(trust_entry)
-- 
2.7.4



More information about the Freeipa-devel mailing list