URL: https://github.com/freeipa/freeipa/pull/5471
Author: abbra
 Title: #5471: [Backport][ipa-4-9] Trust fixes
Action: opened

PR body:
"""
Manual backport of https://github.com/freeipa/freeipa/pull/5436
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/5471/head:pr5471
git checkout pr5471
From 755e524b861c94f62c40f60a56587dc711bd8058 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <aboko...@redhat.com>
Date: Thu, 25 Jul 2019 11:28:42 +0300
Subject: [PATCH 01/10] ipa-kdb: provide correct logon time in MS-PAC from
 authentication time

When MS-PAC structure is created, we get passed the time of
authentication from KDC. Use this to record logon time in MS-PAC
structure.

Set allow password change time to the last password change. We need to
refer to the actual password policy here in future.

Also use INT64_MAX to represent the resulting value for logoff
and kickoff times according to MS-PAC 2.6.

Fixes: https://pagure.io/freeipa/issue/8659
Signed-off-by: Alexander Bokovoy <aboko...@redhat.com>
Reviewed-By: Christian Heimes <chei...@redhat.com>
Reviewed-By: Rob Crittenden <rcrit...@redhat.com>
---
 daemons/ipa-kdb/ipa_kdb_mspac.c | 21 +++++++++++++--------
 1 file changed, 13 insertions(+), 8 deletions(-)

diff --git a/daemons/ipa-kdb/ipa_kdb_mspac.c b/daemons/ipa-kdb/ipa_kdb_mspac.c
index 6202d303125..31f61712942 100644
--- a/daemons/ipa-kdb/ipa_kdb_mspac.c
+++ b/daemons/ipa-kdb/ipa_kdb_mspac.c
@@ -415,6 +415,7 @@ static krb5_error_code ipadb_fill_info3(struct ipadb_context *ipactx,
                                         LDAPMessage *lentry,
                                         unsigned int flags,
                                         TALLOC_CTX *memctx,
+                                        krb5_timestamp authtime,
                                         struct netr_SamInfo3 *info3)
 {
     LDAP *lcontext = ipactx->lcontext;
@@ -518,9 +519,11 @@ static krb5_error_code ipadb_fill_info3(struct ipadb_context *ipactx,
         prigid = intres;
     }
 
+    /* krb5_timestamp must be converted to uint32_t to allow 64-bit time_t
+     * to handle time beyond year 2038. See <krb5.h> for details */
+    unix_to_nt_time(&info3->base.logon_time, (time_t)(uint32_t) authtime);
 
-    info3->base.logon_time = 0; /* do not have this info yet */
-    info3->base.logoff_time = -1; /* do not force logoff */
+    info3->base.logoff_time = INT64_MAX; /* do not force logoff */
 
 /* TODO: is krbPrinciplaExpiration what we want to use in kickoff_time ?
  * Needs more investigation */
@@ -538,7 +541,7 @@ static krb5_error_code ipadb_fill_info3(struct ipadb_context *ipactx,
         return ret;
     }
 #else
-    info3->base.kickoff_time = -1;
+    info3->base.kickoff_time = INT64_MAX;
 #endif
 
     ret = ipadb_ldap_attr_to_time_t(lcontext, lentry,
@@ -555,8 +558,9 @@ static krb5_error_code ipadb_fill_info3(struct ipadb_context *ipactx,
     }
 
     /* TODO: from pw policy (ied->pol) */
-    info3->base.allow_password_change = 0;
-    info3->base.force_password_change = -1;
+    /* AD DCs set allow_password_change to last_password_change, it seems */
+    info3->base.allow_password_change = info3->base.last_password_change;
+    info3->base.force_password_change = INT64_MAX;
 
     ret = ipadb_ldap_attr_to_str(lcontext, lentry, "cn", &strres);
     switch (ret) {
@@ -797,6 +801,7 @@ static krb5_error_code ipadb_fill_info3(struct ipadb_context *ipactx,
 static krb5_error_code ipadb_get_pac(krb5_context kcontext,
                                      krb5_db_entry *client,
                                      unsigned int flags,
+                                     krb5_timestamp authtime,
                                      krb5_pac *pac)
 {
     TALLOC_CTX *tmpctx;
@@ -859,7 +864,7 @@ static krb5_error_code ipadb_get_pac(krb5_context kcontext,
     }
 
     /* == Fill Info3 == */
-    kerr = ipadb_fill_info3(ipactx, lentry, flags, tmpctx,
+    kerr = ipadb_fill_info3(ipactx, lentry, flags, tmpctx, authtime,
                             &pac_info.logon_info.info->info3);
     if (kerr) {
         goto done;
@@ -2276,7 +2281,7 @@ krb5_error_code ipadb_sign_authdata(krb5_context context,
 
         (void)ipadb_reinit_mspac(ipactx, force_reinit_mspac);
 
-        kerr = ipadb_get_pac(context, client, flags, &pac);
+        kerr = ipadb_get_pac(context, client, flags, authtime, &pac);
         if (kerr != 0 && kerr != ENOENT) {
             goto done;
         }
@@ -2290,7 +2295,7 @@ krb5_error_code ipadb_sign_authdata(krb5_context context,
         /* check or generate pac data */
         if ((pac_auth_data == NULL) || (pac_auth_data[0] == NULL)) {
             if (flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION) {
-                kerr = ipadb_get_pac(context, client_entry, flags, &pac);
+                kerr = ipadb_get_pac(context, client_entry, flags, authtime, &pac);
                 if (kerr != 0 && kerr != ENOENT) {
                     goto done;
                 }

From 6f96916c797399f0a87b8936731c9e49d08b45b2 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <aboko...@redhat.com>
Date: Mon, 12 Oct 2020 12:42:25 +0300
Subject: [PATCH 02/10] ipasam: implement PASSDB getgrnam call

ipasam already implemented retrieval of groups for MS-SAMR calls.
However, it did not have implementation of a group retrieval for the
path of lookup_name() function in Samba. The lookup_name() is used in
many places in smbd and winbindd.

With this change it will be possible to resolve IPA groups in Windows UI
(Security tab) and console (net localgroup ...). When Global Catalog
service is enabled, it will be possible to search for those groups as
well.

In Active Directory, security groups can be domain, domain local, local
and so on. In IPA, only domain groups exposed through ipasam because
SID generation plugin only supports adding SIDs to POSIX groups and
users. Thus, non-POSIX groups are not going to have SIDs associated and
will not be visible in both UNIX and Windows environments.

Group retrieval in Samba is implemented as a mapping between NT and
POSIX groups. IPA doesn't have explicit mapping tables. Instead, any
POSIX group in IPA that has a SID associated with it is considered a
domain group for Samba.

Finally, additional ACI is required to ensure attributes looked up by
ipasam are always readable by the trust agents.

Fixes: https://pagure.io/freeipa/issue/8660
Signed-off-by: Alexander Bokovoy <aboko...@redhat.com>
Reviewed-By: Christian Heimes <chei...@redhat.com>
Reviewed-By: Rob Crittenden <rcrit...@redhat.com>
---
 daemons/ipa-sam/ipa_sam.c        | 146 +++++++++++++++++++++++++++++++
 install/updates/60-trusts.update |   5 ++
 2 files changed, 151 insertions(+)

diff --git a/daemons/ipa-sam/ipa_sam.c b/daemons/ipa-sam/ipa_sam.c
index 79f4e5a7724..3ec9f8a130d 100644
--- a/daemons/ipa-sam/ipa_sam.c
+++ b/daemons/ipa-sam/ipa_sam.c
@@ -147,6 +147,8 @@ bool E_md4hash(const char *passwd, uint8_t p16[16]); /* available in libcliauth-
 #define LDAP_ATTRIBUTE_UIDNUMBER "uidnumber"
 #define LDAP_ATTRIBUTE_GIDNUMBER "gidnumber"
 #define LDAP_ATTRIBUTE_ASSOCIATED_DOMAIN "associatedDomain"
+#define LDAP_ATTRIBUTE_DISPLAYNAME "displayName"
+#define LDAP_ATTRIBUTE_DESCRIPTION "description"
 
 #define LDAP_OBJ_KRB_PRINCIPAL "krbPrincipal"
 #define LDAP_OBJ_KRB_PRINCIPAL_AUX "krbPrincipalAux"
@@ -992,6 +994,49 @@ static bool ipasam_uid_to_sid(struct pdb_methods *methods, uid_t uid,
 	return ret;
 }
 
+typedef NTSTATUS (*process_entry_fn)(struct ipasam_private *priv, LDAPMessage *entry, TALLOC_CTX *ctx, void *state);
+static NTSTATUS ipasam_search_entries(struct pdb_methods *methods,
+				      struct ipasam_private *priv,
+				      TALLOC_CTX *ctx,
+				      char *filter,
+				      const char **attrs_list,
+				      process_entry_fn process_entry,
+				      void *state)
+{
+	LDAPMessage *result = NULL;
+	LDAPMessage *entry = NULL;
+	int rc;
+	NTSTATUS res;
+
+	if ((priv == NULL) || (ctx == NULL) || (process_entry == NULL)) {
+		return NT_STATUS_NO_MEMORY;
+	}
+
+	rc = smbldap_search_suffix(priv->ldap_state, filter, attrs_list, &result);
+	if (rc != LDAP_SUCCESS) {
+		return NT_STATUS_UNSUCCESSFUL;
+	}
+
+	smbldap_talloc_autofree_ldapmsg(ctx, result);
+
+	if (ldap_count_entries(priv2ld(priv), result) == 0) {
+		return NT_STATUS_UNSUCCESSFUL;
+	}
+
+	for (entry = ldap_first_entry(priv2ld(priv), result);
+		entry != NULL;
+		entry = ldap_next_entry(priv2ld(priv), entry)) {
+
+		res = process_entry(priv, entry, ctx, state);
+		if (!NT_STATUS_IS_OK(res)) {
+			return res;
+		}
+	}
+
+	return NT_STATUS_OK;
+}
+
+
 static bool ipasam_gid_to_sid(struct pdb_methods *methods, gid_t gid,
 			       struct dom_sid *sid)
 {
@@ -1660,6 +1705,106 @@ static bool ipasam_search_grouptype(struct pdb_methods *methods,
 	return ipasam_search_firstpage(search);
 }
 
+static NTSTATUS _ipasam_collect_map_entry(struct ipasam_private *ipasam_state,
+					  LDAPMessage *entry, TALLOC_CTX *ctx, void *state)
+{
+
+	GROUP_MAP *map = state;
+	char *value = NULL;
+	struct dom_sid *sid = NULL;
+	enum idmap_error_code err;
+	LDAP *ld = priv2ld(ipasam_state);
+
+	/* display name is the NT group name */
+	value = smbldap_talloc_single_attribute(ld, entry, LDAP_ATTRIBUTE_DISPLAYNAME, ctx);
+	if (!value) {
+		DBG_DEBUG("\"" LDAP_ATTRIBUTE_DISPLAYNAME "\" not found\n");
+
+		/* fallback to the 'cn' attribute */
+		value = smbldap_talloc_single_attribute(ld, entry, LDAP_ATTRIBUTE_CN, ctx);
+		if (!value) {
+			DBG_INFO("\"" LDAP_ATTRIBUTE_CN "\" not found\n");
+			return NT_STATUS_NO_SUCH_GROUP;
+		}
+	}
+
+	map->nt_name = talloc_steal(map, value);
+
+	value = smbldap_talloc_single_attribute(ld, entry, LDAP_ATTRIBUTE_DESCRIPTION, ctx);
+	if (!value) {
+		DBG_DEBUG("\"" LDAP_ATTRIBUTE_DESCRIPTION "\" not found\n");
+		value = talloc_strdup(ctx, "");
+	}
+	map->comment = talloc_steal(map, value);
+
+	value = smbldap_talloc_single_attribute(ld, entry, LDAP_ATTRIBUTE_SID, ctx);
+	if (!value) {
+		DBG_ERR("\"" LDAP_ATTRIBUTE_SID "\" not found\n");
+		return NT_STATUS_NO_SUCH_GROUP;
+	}
+
+	err = sss_idmap_sid_to_smb_sid(ipasam_state->idmap_ctx, value, &sid);
+	if (err != IDMAP_SUCCESS) {
+		DBG_ERR("Could not convert %s to SID\n", value);
+		return NT_STATUS_NO_SUCH_GROUP;
+	}
+
+	sid_copy(&map->sid, sid);
+	TALLOC_FREE(sid);
+	TALLOC_FREE(value);
+
+	/* all POSIX groups in FreeIPA are domain groups */
+	map->sid_name_use = SID_NAME_DOM_GRP;
+
+	return NT_STATUS_OK;
+
+}
+
+static NTSTATUS ipasam_getgrnam(struct pdb_methods *methods,
+				GROUP_MAP *map, const char *name)
+{
+	struct ipasam_private *ipasam_state =
+		talloc_get_type_abort(methods->private_data, struct ipasam_private);
+	TALLOC_CTX *tmp_ctx = NULL;
+	char *filter = NULL;
+	char *escaped = NULL;
+	const char *attrs_list[] = {LDAP_ATTRIBUTE_CN, LDAP_ATTRIBUTE_SECURITY_IDENTIFIER,
+				    LDAP_ATTRIBUTE_GIDNUMBER, LDAP_ATTRIBUTE_DISPLAYNAME,
+				    LDAP_ATTRIBUTE_DESCRIPTION, NULL};
+	NTSTATUS result;
+
+	if (map == NULL) {
+		return NT_STATUS_NO_MEMORY;
+	}
+
+	tmp_ctx = talloc_new(ipasam_state);
+	if (tmp_ctx == NULL) {
+		return NT_STATUS_NO_MEMORY;
+	}
+
+	escaped = escape_ldap_string(tmp_ctx, name);
+	if (escaped == NULL) {
+		TALLOC_FREE(tmp_ctx);
+		return NT_STATUS_NO_MEMORY;
+	}
+
+	filter = talloc_asprintf(tmp_ctx,
+				 "(&(objectclass=%s)(objectclass=%s)(%s=%s))",
+				 LDAP_OBJ_GROUPMAP, LDAP_OBJ_POSIXGROUP,
+				 LDAP_ATTRIBUTE_CN, escaped);
+	if (filter == NULL) {
+		TALLOC_FREE(tmp_ctx);
+		return NT_STATUS_NO_MEMORY;
+	}
+
+	result = ipasam_search_entries(methods, ipasam_state, tmp_ctx,
+				       filter, attrs_list,
+				       _ipasam_collect_map_entry, map);
+	talloc_free(tmp_ctx);
+	return result;
+}
+
+
 static bool ipasam_search_groups(struct pdb_methods *methods,
 				  struct pdb_search *search)
 {
@@ -5006,6 +5151,7 @@ static NTSTATUS pdb_init_ipasam(struct pdb_methods **pdb_method,
 
 	(*pdb_method)->getsampwnam = ipasam_getsampwnam;
 	(*pdb_method)->getsampwsid = ipasam_getsampwsid;
+	(*pdb_method)->getgrnam = ipasam_getgrnam;
 	(*pdb_method)->search_users = ipasam_search_users;
 	(*pdb_method)->search_groups = ipasam_search_groups;
 	(*pdb_method)->search_aliases = ipasam_search_aliases;
diff --git a/install/updates/60-trusts.update b/install/updates/60-trusts.update
index 6d8cccc3842..56e392044a2 100644
--- a/install/updates/60-trusts.update
+++ b/install/updates/60-trusts.update
@@ -41,6 +41,11 @@ dn: $SUFFIX
 add:aci: (targetattr = "ipaNTHash")(version 3.0; acl "Samba system principals can read and write NT passwords"; allow (read,write) groupdn="ldap:///cn=adtrust agents,cn=sysaccounts,cn=etc,$SUFFIX";)
 remove:aci: (targetattr = "ipaNTHash")(version 3.0; acl "Samba system principals can read NT passwords"; allow (read) groupdn="ldap:///cn=adtrust agents,cn=sysaccounts,cn=etc,$SUFFIX";)
 
+# Allow Samba to read POSIX information with an explicit ACI
+dn: cn=accounts,$SUFFIX
+add:aci: (targetattr = "cn || createtimestamp || description || displayname || entryusn || gecos || gidnumber || givenname || homedirectory || ipantsecurityidentifier || loginshell || modifytimestamp || objectclass || uid || uidnumber")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "Allow reading POSIX information about users and group objects";allow (compare,read,search) groupdn = "ldap:///cn=adtrust agents,cn=sysaccounts,cn=etc,$SUFFIX";)
+
+
 # For Samba as a domain member setup we need to allow synchronizing ipaNTHash value
 dn: cn=services,cn=accounts,$SUFFIX
 add:aci: (target="ldap:///krbprincipalname=cifs/($$dn),cn=services,cn=accounts,$SUFFIX")(targetattr="ipaNTHash")(version 3.0; acl "CIFS service can modify own ipaNTHash"; allow(write) userdn="ldap:///krbprincipalname=cifs/($$dn),cn=services,cn=accounts,$SUFFIX" or userattr="managedby#SELFDN";)

From 2ad53a9a4d3b1ee83d2de3cdcb342dd8af30e424 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <aboko...@redhat.com>
Date: Tue, 10 Nov 2020 09:37:19 +0200
Subject: [PATCH 03/10] ipasam: allow search of users by user principal name
 (UPN)

lookup_name() in Samba may call PASSDB API to search by a UPN (e.g.
username@suffix). Support this call by detecting '@' in the passed name
and setting up filter to be

  (&(objectClass=ipaNTUserAttrs)(objectClass=krbPrincipalAux)(krbPrincipalName:caseIgnoreIA5Match:=%s))

instead of

  (&(objectClass=ipaNTUserAttrs)(uid=%s))

The result of the search would still contain a proper user entry as we
always have krbPrincipalName in LDAP entries of IPA users. Note that the
match must be case-insensitive because otherwise krbPrincipalName is
matched with exact case in the schema. We use the same matching override
in KDB driver already.

Fixes: https://pagure.io/freeipa/issue/8661
Signed-of-by: Alexander Bokovoy <aboko...@redhat.com>
Reviewed-By: Christian Heimes <chei...@redhat.com>
Reviewed-By: Rob Crittenden <rcrit...@redhat.com>
---
 daemons/ipa-sam/ipa_sam.c | 29 +++++++++++++++++++++++++----
 1 file changed, 25 insertions(+), 4 deletions(-)

diff --git a/daemons/ipa-sam/ipa_sam.c b/daemons/ipa-sam/ipa_sam.c
index 3ec9f8a130d..46c28f6c757 100644
--- a/daemons/ipa-sam/ipa_sam.c
+++ b/daemons/ipa-sam/ipa_sam.c
@@ -3855,6 +3855,7 @@ static NTSTATUS ipasam_getsampwnam(struct pdb_methods *methods,
 	LDAPMessage *entry = NULL;
 	int ret;
 	int count;
+	bool search_for_upn = false;
 
 	lastidx = strlen(sname);
 	if (lastidx > 0) {
@@ -3877,16 +3878,36 @@ static NTSTATUS ipasam_getsampwnam(struct pdb_methods *methods,
 		return NT_STATUS_NO_MEMORY;
 	}
 
+	if (strchr(sname, '@') != NULL) {
+		search_for_upn = true;
+	}
+
 	escaped_user = escape_ldap_string(tmp_ctx, sname);
 	if (escaped_user == NULL) {
 		status = NT_STATUS_NO_MEMORY;
 		goto done;
 	}
 
-	filter = talloc_asprintf(tmp_ctx, "(&(%s=%s)(%s=%s))",
-					  LDAP_ATTRIBUTE_OBJECTCLASS,
-					  LDAP_OBJ_SAMBASAMACCOUNT,
-					  LDAP_ATTRIBUTE_UID, escaped_user);
+
+	if (search_for_upn) {
+		/* Search krbPrincipalName case-insensitive for UPN suffixes to
+		 * match because default matching rule is case-sensitive for
+		 * IA5String */
+		filter = talloc_asprintf(tmp_ctx,
+				"(&(%s=%s)(%s=%s)(%s:caseIgnoreIA5Match:=%s))",
+				LDAP_ATTRIBUTE_OBJECTCLASS,
+				LDAP_OBJ_SAMBASAMACCOUNT,
+				LDAP_ATTRIBUTE_OBJECTCLASS,
+				LDAP_OBJ_KRB_PRINCIPAL_AUX,
+				LDAP_ATTRIBUTE_KRB_PRINCIPAL,
+				escaped_user);
+	} else {
+		filter = talloc_asprintf(tmp_ctx, "(&(%s=%s)(%s=%s))",
+						  LDAP_ATTRIBUTE_OBJECTCLASS,
+						  LDAP_OBJ_SAMBASAMACCOUNT,
+						  LDAP_ATTRIBUTE_UID, escaped_user);
+	}
+
 	if (filter == NULL) {
 		status = NT_STATUS_NO_MEMORY;
 		goto done;

From 3a30a2cf5e9e4c36fd7965079e35d3884780c730 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <aboko...@redhat.com>
Date: Thu, 12 Nov 2020 15:33:09 +0200
Subject: [PATCH 04/10] ipasam: free trusted domain context on failure

The context is hanging off a talloc memory context passed into the
function so it will eventually be freed. It is better, though, to free
it immediately when we exit from the fill_pdb_trusted_domain() function.

Related: https://pagure.io/freeipa/issue/8576
Signed-off-by: Alexander Bokovoy <aboko...@redhat.com>
Reviewed-By: Christian Heimes <chei...@redhat.com>
Reviewed-By: Rob Crittenden <rcrit...@redhat.com>
---
 daemons/ipa-sam/ipa_sam.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/daemons/ipa-sam/ipa_sam.c b/daemons/ipa-sam/ipa_sam.c
index 46c28f6c757..c11e9917712 100644
--- a/daemons/ipa-sam/ipa_sam.c
+++ b/daemons/ipa-sam/ipa_sam.c
@@ -2490,6 +2490,7 @@ static bool fill_pdb_trusted_domain(TALLOC_CTX *mem_ctx,
 					 LDAP_ATTRIBUTE_TRUST_DIRECTION,
 					 &td->trust_direction);
 	if (!res) {
+		TALLOC_FREE(td);
 		return false;
 	}
 	if (td->trust_direction == 0) {
@@ -2501,6 +2502,7 @@ static bool fill_pdb_trusted_domain(TALLOC_CTX *mem_ctx,
 					 LDAP_ATTRIBUTE_TRUST_ATTRIBUTES,
 					 &td->trust_attributes);
 	if (!res) {
+		TALLOC_FREE(td);
 		return false;
 	}
 	if (td->trust_attributes == 0) {
@@ -2512,6 +2514,7 @@ static bool fill_pdb_trusted_domain(TALLOC_CTX *mem_ctx,
 					 LDAP_ATTRIBUTE_TRUST_TYPE,
 					 &td->trust_type);
 	if (!res) {
+		TALLOC_FREE(td);
 		return false;
 	}
 	if (td->trust_type == 0) {
@@ -2521,23 +2524,27 @@ static bool fill_pdb_trusted_domain(TALLOC_CTX *mem_ctx,
 
 	td->trust_posix_offset = talloc_zero(td, uint32_t);
 	if (td->trust_posix_offset == NULL) {
+		TALLOC_FREE(td);
 		return false;
 	}
 	res = get_uint32_t_from_ldap_msg(ipasam_state, entry,
 					 LDAP_ATTRIBUTE_TRUST_POSIX_OFFSET,
 					 td->trust_posix_offset);
 	if (!res) {
+		TALLOC_FREE(td);
 		return false;
 	}
 
 	td->supported_enc_type = talloc_zero(td, uint32_t);
 	if (td->supported_enc_type == NULL) {
+		TALLOC_FREE(td);
 		return false;
 	}
 	res = get_uint32_t_from_ldap_msg(ipasam_state, entry,
 					 LDAP_ATTRIBUTE_SUPPORTED_ENC_TYPE,
 					 td->supported_enc_type);
 	if (!res) {
+		TALLOC_FREE(td);
 		return false;
 	}
 	if (*td->supported_enc_type == 0) {

From fd1f808ad07151532ced2c6e72b1ae164fa4fb6f Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <aboko...@redhat.com>
Date: Mon, 18 Jan 2021 17:51:20 +0200
Subject: [PATCH 05/10] ipasam: derive parent domain for subdomains
 automatically

[MS-ADTS] 6.1.6.7.13 defines 'trustPartner' attribute as containing a
FQDN of the trusted domain. In practice, for a subdomain of a forest, it
would be FQDN of the subdomain itself in the trusted domain entry in the
parent domain. This is reflected as ipaNTTrustPartner attribute in
FreeIPA.

Remove ipaNTTrustPartner from the searches that use NetBIOS name. We
match cn of that entry already.

Use RDN value of the entry to derive DNS domain name in case
ipaNTTrustPartner is missing.

For subdomains, set trust attributes to 0 and trust flags to mark them
as being within the forest. This will trigger winbindd to not ask for
credentials to reach those domain controllers directly.

Fixes: https://pagure.io/freeipa/issue/8576
Signed-off-by: Alexander Bokovoy <aboko...@redhat.com>
Reviewed-By: Christian Heimes <chei...@redhat.com>
Reviewed-By: Rob Crittenden <rcrit...@redhat.com>
---
 daemons/ipa-sam/ipa_sam.c | 78 ++++++++++++++++++++++++++++++++++-----
 1 file changed, 68 insertions(+), 10 deletions(-)

diff --git a/daemons/ipa-sam/ipa_sam.c b/daemons/ipa-sam/ipa_sam.c
index c11e9917712..06b1b7af4a9 100644
--- a/daemons/ipa-sam/ipa_sam.c
+++ b/daemons/ipa-sam/ipa_sam.c
@@ -2358,10 +2358,10 @@ static bool get_trusted_domain_by_name_int(struct ipasam_private *ipasam_state,
 	bool ok;
 
 	filter = talloc_asprintf(mem_ctx,
-				 "(&(objectClass=%s)(|(%s=%s)(%s=%s)(cn=%s)))",
+				 "(&(objectClass=%s)(|(%s=%s)(cn=%s)))",
 				 LDAP_OBJ_TRUSTED_DOMAIN,
 				 LDAP_ATTRIBUTE_FLAT_NAME, domain,
-				 LDAP_ATTRIBUTE_TRUST_PARTNER, domain, domain);
+				 domain);
 	if (filter == NULL) {
 		return false;
 	}
@@ -2431,16 +2431,74 @@ static bool fill_pdb_trusted_domain(TALLOC_CTX *mem_ctx,
 	struct pdb_trusted_domain *td;
 	struct dom_sid *sid = NULL;
 	enum idmap_error_code err;
+	char *strdn = NULL;
+	char *dnl = NULL;
+	int rc = 0;
+	int count = 0;
+	char *dns_domain = NULL;
+	LDAPDN dn = NULL;
 
 	if (entry == NULL) {
 		return false;
 	}
 
+	strdn = ldap_get_dn(priv2ld(ipasam_state), entry);
+	if (strdn == NULL) {
+		DEBUG(1, ("Couldn't retrieve DN of the trusted domain entry\n"));
+		return false;
+	}
+
+	dnl = strcasestr(strdn, ipasam_state->trust_dn);
+	if (dnl == NULL) {
+		DEBUG(1, ("DN %s of trusted domain entry is not under %s\n",
+			  strdn,
+			  ipasam_state->trust_dn));
+		free(strdn);
+		return false;
+	}
+
 	td = talloc_zero(mem_ctx, struct pdb_trusted_domain);
 	if (td == NULL) {
+		free(strdn);
 		return false;
 	}
 
+	/* dnl points to the begining of cn=ad,cn=trusts,... and we need
+	 * to go one character back and turn ',' into end of string. */
+	dnl--;
+	dnl[0] = '\0';
+
+	/* Now strdn has at most two RDNs:
+	 * - there is one RDN for the directly trusted one
+	 * - there are two RDNs for a subdomain
+	 */
+	rc = ldap_str2dn(strdn, &dn, LDAP_DN_FORMAT_LDAPV3);
+	if (rc) {
+		free(strdn);
+		return false;
+	}
+
+	for (count = 0; dn[count] != NULL; count++);
+
+	/* For subdomains, we must set parent domain */
+	if (count < 1 || count > 2) {
+		DEBUG(1, ("LDAP object with DN %s,%s "
+			  "cannot be used as a trusted domain\n",
+			  strdn, ipasam_state->trust_dn));
+		ldap_dnfree(dn);
+		free(strdn);
+		TALLOC_FREE(td);
+		return false;
+
+	}
+
+	dns_domain = talloc_asprintf(td, "%*s",
+				     (int)dn[0][0]->la_value.bv_len,
+				     dn[0][0]->la_value.bv_val);
+
+	ldap_dnfree(dn);
+	free(strdn);
+
 	/* All attributes are MAY */
 
 	dummy = get_single_attribute(NULL, priv2ld(ipasam_state), entry,
@@ -2479,13 +2537,16 @@ static bool fill_pdb_trusted_domain(TALLOC_CTX *mem_ctx,
 			  LDAP_ATTRIBUTE_FLAT_NAME));
 	}
 
+	/* If ipaNTTrustPartner is missing, it should be the same as the domain
+	 * itself. */
 	td->domain_name = get_single_attribute(td, priv2ld(ipasam_state), entry,
 					       LDAP_ATTRIBUTE_TRUST_PARTNER);
 	if (td->domain_name == NULL) {
-		DEBUG(9, ("Attribute %s not present.\n",
-			  LDAP_ATTRIBUTE_TRUST_PARTNER));
+		td->domain_name = dns_domain;
 	}
 
+	/* For subdomains ipaNTTrustDirection is missing which is OK, we
+	 * default to 0 */
 	res = get_uint32_t_from_ldap_msg(ipasam_state, entry,
 					 LDAP_ATTRIBUTE_TRUST_DIRECTION,
 					 &td->trust_direction);
@@ -2493,10 +2554,6 @@ static bool fill_pdb_trusted_domain(TALLOC_CTX *mem_ctx,
 		TALLOC_FREE(td);
 		return false;
 	}
-	if (td->trust_direction == 0) {
-		/* attribute wasn't present, set default value */
-		td->trust_direction = LSA_TRUST_DIRECTION_INBOUND | LSA_TRUST_DIRECTION_OUTBOUND;
-	}
 
 	res = get_uint32_t_from_ldap_msg(ipasam_state, entry,
 					 LDAP_ATTRIBUTE_TRUST_ATTRIBUTES,
@@ -2506,8 +2563,9 @@ static bool fill_pdb_trusted_domain(TALLOC_CTX *mem_ctx,
 		return false;
 	}
 	if (td->trust_attributes == 0) {
-		/* attribute wasn't present, set default value */
-		td->trust_attributes = LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE;
+		/* attribute wasn't present, this is a subdomain within the
+		 * parent forest */
+		td->trust_attributes = LSA_TRUST_ATTRIBUTE_WITHIN_FOREST;
 	}
 
 	res = get_uint32_t_from_ldap_msg(ipasam_state, entry,

From 7efd33c1d862c780dd4eef0e6bca73eb4bc9f9f4 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <aboko...@redhat.com>
Date: Mon, 18 Jan 2021 17:51:43 +0200
Subject: [PATCH 06/10] ipaserver/dcerpc: store forest topology as a blob in
 ipasam

winbindd expects forest topology information blob to find out child
domains. We should store it in LDAP and let ipasam to retrieve it.
In fact, ipasam already supports updating and loading this information
but during 'ipa trust-fetch-domains' we didn't provide it.

Make sure the blob is preserved after it was retrieved and also updated
when we fetch forest topology information.

Fixes: https://pagure.io/freeipa/issue/8576
Signed-off-by: Alexander Bokovoy <aboko...@redhat.com>
Reviewed-By: Christian Heimes <chei...@redhat.com>
Reviewed-By: Rob Crittenden <rcrit...@redhat.com>
---
 ipaserver/dcerpc.py        | 120 ++++++++++++++++++++++++++++---------
 ipaserver/plugins/trust.py |   3 +
 2 files changed, 94 insertions(+), 29 deletions(-)

diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
index beb17f4a588..6f4defde2d4 100644
--- a/ipaserver/dcerpc.py
+++ b/ipaserver/dcerpc.py
@@ -24,6 +24,7 @@
 
 from __future__ import absolute_import
 
+from contextlib import contextmanager
 import logging
 import re
 import time
@@ -852,6 +853,7 @@ def __init__(self, hostname, creds=None):
         self._policy_handle = None
         self.read_only = False
         self.ftinfo_records = None
+        self.ftinfo_data = None
         self.validation_attempts = 0
 
     def __gen_lsa_connection(self, binding):
@@ -1011,6 +1013,16 @@ def retrieve(self, remote_host):
 
         self.info['is_pdc'] = (result.role == lsa.LSA_ROLE_PRIMARY)
 
+        if all([self.info['is_pdc'],
+                self.info['dns_domain'] == self.info['dns_forest']]):
+            try:
+                netr_pipe = netlogon.netlogon(self.binding,
+                                              self.parm, self.creds)
+                self.ftinfo_data = netr_pipe.netr_DsRGetForestTrustInformation(
+                    self.info['dc'], None, 0)
+            except RuntimeError as e:
+                raise assess_dcerpc_error(e)
+
     def generate_auth(self, trustdom_secret):
         password_blob = string_to_array(trustdom_secret.encode('utf-16-le'))
 
@@ -1067,6 +1079,9 @@ def generate_ftinfo(self, another_domain):
 
         Only top level name and top level name exclusions are handled here.
         """
+        if another_domain.ftinfo_data is not None:
+            return another_domain.ftinfo_data
+
         if not another_domain.ftinfo_records:
             return None
 
@@ -1501,21 +1516,14 @@ def retrieve_netlogon_info_2(logon_server, domain, function_code, data):
         return False
 
 
-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)
-        # 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'], None, 0)
-        return domains
-
-    domains = None
+@contextmanager
+def discover_trust_instance(api, mydomain, trustdomain,
+                            creds=None, server=None):
     domain_validator = DomainValidator(api)
     configured = domain_validator.is_configured()
     if not configured:
-        return None
+        yield None
+        return
 
     td = TrustDomainInstance('')
     td.parm.set('workgroup', mydomain)
@@ -1541,9 +1549,11 @@ def communicate(td):
         # Rely on existing Kerberos credentials in the environment
         td.creds = credentials.Credentials()
         td.creds.set_kerberos_state(credentials.MUST_USE_KERBEROS)
+        enforce_smb_encryption(td.creds)
         td.creds.guess(td.parm)
         td.creds.set_workstation(domain_validator.flatname)
-        domains = communicate(td)
+        logger.error('environment: %s', str(os.environ))
+        yield td
     else:
         # Attempt to authenticate as HTTP/ipa.master and use cross-forest trust
         # or as passed-in user in case of a one-way trust
@@ -1559,14 +1569,37 @@ def communicate(td):
                                                  'cross-forest communication'))
         td.creds = credentials.Credentials()
         td.creds.set_kerberos_state(credentials.MUST_USE_KERBEROS)
+        enforce_smb_encryption(td.creds)
         if ccache_name:
             with ipautil.private_ccache(path=ccache_name):
                 td.creds.guess(td.parm)
                 td.creds.set_workstation(domain_validator.flatname)
-                domains = communicate(td)
+                yield td
 
-    if domains is None:
-        return None
+
+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)
+        # 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'],
+                                                              None, 0)
+        return domains
+
+    domains = None
+    with discover_trust_instance(api, mydomain, trustdomain,
+                                 creds=creds, server=server) as td:
+        if td is None:
+            return None
+        if td.ftinfo_data is not None:
+            domains = td.ftinfo_data
+        else:
+            domains = communicate(td)
+
+        if domains is None:
+            return None
 
     result = {'domains': {}, 'suffixes': {}}
     # netr_DsRGetForestTrustInformation returns two types of entries:
@@ -1574,24 +1607,48 @@ def communicate(td):
     # 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
+    ftinfo_records = []
+    ftinfo = drsblobs.ForestTrustInfo()
     for t in domains.entries:
+        record = drsblobs.ForestTrustInfoRecord()
+        record.flags = t.flags
+        record.timestamp = t.time
+        record.type = t.type
+
         if t.type == lsa.LSA_FOREST_TRUST_DOMAIN_INFO:
+            record.data.sid = t.forest_trust_data.domain_sid
+            record.data.dns_name.string = \
+                t.forest_trust_data.dns_domain_name.string
+            record.data.netbios_name.string = \
+                t.forest_trust_data.netbios_domain_name.string
+
             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)
-            }
+            if tname != trustdomain:
+                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:
+            record.data.string = t.forest_trust_data.string
+
             tname = unicode(t.forest_trust_data.string)
             if tname == trustdomain:
                 continue
 
             result['suffixes'][tname] = {'cn': tname}
+        elif t.type == lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+            record.data.string = t.forest_trust_data.string
+
+        rc = drsblobs.ForestTrustInfoRecordArmor()
+        rc.record = record
+        ftinfo_records.append(rc)
+
+    ftinfo.count = len(ftinfo_records)
+    ftinfo.records = ftinfo_records
+    result['ftinfo_data'] = ndr_pack(ftinfo)
     return result
 
 
@@ -1785,10 +1842,15 @@ def join_ad_full_credentials(self, realm, realm_server, realm_admin,
                                                    trustdom_pass,
                                                    trust_type, trust_external)
 
-            # For local domain we don't set topology information
-            self.local_domain.establish_trust(self.remote_domain,
-                                              trustdom_pass,
-                                              trust_type, trust_external)
+            try:
+                self.local_domain.establish_trust(self.remote_domain,
+                                                  trustdom_pass,
+                                                  trust_type, trust_external)
+            except TrustTopologyConflictSolved:
+                self.local_domain.establish_trust(self.remote_domain,
+                                                  trustdom_pass,
+                                                  trust_type, trust_external)
+
             # if trust is inbound, we don't need to verify it because
             # AD DC will respond with WERR_NO_SUCH_DOMAIN --
             # it only does verification for outbound trusts.
diff --git a/ipaserver/plugins/trust.py b/ipaserver/plugins/trust.py
index b25f5b5e711..8c9c674f639 100644
--- a/ipaserver/plugins/trust.py
+++ b/ipaserver/plugins/trust.py
@@ -1744,6 +1744,9 @@ def add_new_domains_from_trust(myapi, trustinstance, trust_entry,
         tlns = entry.get('ipantadditionalsuffixes', [])
         tlns.extend(x for x in suffixes if x not in tlns)
         entry['ipantadditionalsuffixes'] = tlns
+        ftidata = domains.get('ftinfo_data', None)
+        if ftidata is not None:
+            entry['ipanttrustforesttrustinfo'] = [ftidata]
         ldap.update_entry(entry)
     except errors.EmptyModlist:
         pass

From e097cc3eaaa083269b863201916c6d3bbad6c865 Mon Sep 17 00:00:00 2001
From: Sergey Orlov <sor...@redhat.com>
Date: Fri, 15 Jan 2021 14:51:48 +0100
Subject: [PATCH 07/10] ipatests: use fully qualified name for AD admin when
 establishing trust

Changes in https://pagure.io/freeipa/issue/8655 made it impossible
to use AD admin name without domain part in "ipa trust-add" command to
establish external trust with an AD tree domain.
Also use fully qualified admin name by default in all trust related tests
to reduce abiguity

Reviewed-By: Christian Heimes <chei...@redhat.com>
Reviewed-By: Rob Crittenden <rcrit...@redhat.com>
---
 ipatests/pytest_ipa/integration/tasks.py |  9 +++++++--
 ipatests/test_integration/test_trust.py  | 15 ++++++++-------
 2 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py
index 9a9ce725bab..6046869752a 100755
--- a/ipatests/pytest_ipa/integration/tasks.py
+++ b/ipatests/pytest_ipa/integration/tasks.py
@@ -661,7 +661,7 @@ def unconfigure_windows_dns_for_trust(ad, master):
     ad.run_command(['dnscmd', '/zonedelete', master.domain.name, '/f'])
 
 
-def establish_trust_with_ad(master, ad_domain, extra_args=(),
+def establish_trust_with_ad(master, ad_domain, ad_admin=None, extra_args=(),
                             shared_secret=None):
     """
     Establishes trust with Active Directory. Trust type is detected depending
@@ -669,6 +669,9 @@ def establish_trust_with_ad(master, ad_domain, extra_args=(),
 
     Use extra arguments to pass extra arguments to the trust-add command, such
     as --range-type="ipa-ad-trust" to enforce a particular range type.
+
+    If ad_admin is not provided, name will be constructed as
+    "Administrator@<ad_domain>".
     """
 
     # Force KDC to reload MS-PAC info by trying to get TGT for HTTP
@@ -686,7 +689,9 @@ def establish_trust_with_ad(master, ad_domain, extra_args=(),
         extra_args += ['--trust-secret']
         stdin_text = shared_secret
     else:
-        extra_args += ['--admin', 'Administrator', '--password']
+        if ad_admin is None:
+            ad_admin = 'Administrator@{}'.format(ad_domain)
+        extra_args += ['--admin', ad_admin, '--password']
         stdin_text = master.config.ad_admin_password
     run_repeatedly(
         master, ['ipa', 'trust-add', '--type', 'ad', ad_domain] + extra_args,
diff --git a/ipatests/test_integration/test_trust.py b/ipatests/test_integration/test_trust.py
index a335c780eff..dc23977cba1 100644
--- a/ipatests/test_integration/test_trust.py
+++ b/ipatests/test_integration/test_trust.py
@@ -462,7 +462,7 @@ def test_invalid_range_types(self):
 
                 result = self.master.run_command(
                     ['ipa', 'trust-add', '--type', 'ad', self.ad_domain,
-                     '--admin', 'Administrator',
+                     '--admin', 'Administrator@' + self.ad_domain,
                      '--range-type', range_type, '--password'],
                     raiseonerr=False,
                     stdin_text=self.master.config.ad_admin_password)
@@ -513,8 +513,8 @@ def test_establish_nonexternal_subdomain_trust(self):
 
             result = self.master.run_command([
                 'ipa', 'trust-add', '--type', 'ad', self.ad_subdomain,
-                '--admin',
-                'Administrator', '--password', '--range-type', 'ipa-ad-trust'
+                '--admin', 'Administrator@' + self.ad_subdomain,
+                '--password', '--range-type', 'ipa-ad-trust'
             ], stdin_text=self.master.config.ad_admin_password,
                 raiseonerr=False)
 
@@ -565,8 +565,8 @@ def test_establish_nonexternal_treedomain_trust(self):
 
             result = self.master.run_command([
                 'ipa', 'trust-add', '--type', 'ad', self.ad_treedomain,
-                '--admin',
-                'Administrator', '--password', '--range-type', 'ipa-ad-trust'
+                '--admin', 'Administrator@' + self.ad_treedomain,
+                '--password', '--range-type', 'ipa-ad-trust'
             ], stdin_text=self.master.config.ad_admin_password,
                 raiseonerr=False)
 
@@ -775,8 +775,9 @@ def test_server_option_with_unreachable_ad(self):
             # Check that trust can not be established without --server option
             # This checks that our setup is correct
             result = self.master.run_command(
-                ['ipa', 'trust-add', self.ad.domain.name,
-                 '--admin', 'Administrator', '--password'], raiseonerr=False,
+                ['ipa', 'trust-add', self.ad_domain,
+                 '--admin', 'Administrator@' + self.ad_domain, '--password'],
+                raiseonerr=False,
                 stdin_text=self.master.config.ad_admin_password)
             assert result.returncode == 1
             assert 'CIFS server communication error: code "3221225653", ' \

From ffb8c16230d631a2b65e35b94728a1500450bc80 Mon Sep 17 00:00:00 2001
From: JoeDrane <j...@drane.io>
Date: Mon, 18 Jan 2021 17:02:56 +0000
Subject: [PATCH 08/10] Update ipa_sam.c

fixed typo in debug message on line 4040.

Signed-off-by: JoeDrane <j...@drane.io>
Reviewed-by: Alexander Bokovoy <aboko...@redhat.com>
Reviewed-By: Christian Heimes <chei...@redhat.com>
Reviewed-By: Rob Crittenden <rcrit...@redhat.com>
---
 daemons/ipa-sam/ipa_sam.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/daemons/ipa-sam/ipa_sam.c b/daemons/ipa-sam/ipa_sam.c
index 06b1b7af4a9..cfcf15f6e45 100644
--- a/daemons/ipa-sam/ipa_sam.c
+++ b/daemons/ipa-sam/ipa_sam.c
@@ -4268,7 +4268,7 @@ static NTSTATUS ipasam_search_domain_info(struct smbldap_state *ldap_state,
 		return NT_STATUS_OK;
 	}
 
-	DEBUG(0, ("iapsam_search_domain_info: Got [%d] domain info entries, "
+	DEBUG(0, ("ipasam_search_domain_info: Got [%d] domain info entries, "
 		  "but expected only 1.\n", count));
 
 	return NT_STATUS_UNSUCCESSFUL;

From 6190e623f667888d456a68d76a4d8f32135591fc Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <aboko...@redhat.com>
Date: Mon, 18 Jan 2021 09:44:32 +0200
Subject: [PATCH 09/10] trust-fetch-domains: use custom krb5.conf overlay for
 all trust operations

Operations in FIPS mode make impossible use of NTLMSSP when
authenticating to trusted Active Directory domain controllers because
RC4 cipher is not allowed. Instead, Kerberos authentication have to be
used. We switched to enforce Kerberos authentication when communicating
with trusted domains' domain controllers everywhere.

Kerberos library uses system wide configuration which in IPA defaults to
resolving location of KDCs via DNS SRV records. Once trust is
established, SSSD will populate a list of closest DCs and provide them
through the KDC locator plugin. But at the time the trust is established
performing DNS SRV-based discovery of Kerberos KDCs might fail due to
multiple reasons. It might also succeed but point to a DC that doesn't
know about the account we have to use to establish trust.

One edge case is when DNS SRV record points to an unreachable DC,
whether due to a firewall or a network topology limitations. In such
case an administrator would pass --server <server> option to
'ipa trust-add' or 'ipa trust-fetch-domains' commands.

'ipa trust-fetch-domains' runs a helper via oddjobd. This helper was
already modified to support --server option and generated custom
krb5.conf overlay to pin to a specific AD DC. However, this
configuration was removed as soon as we finished talking to AD DCs.

With switch to always use Kebreros to authenticate in retrieval of the
topology information, we have to use the overlay everywhere as well.

Convert the code that generated the overlay file into a context that
generates the overlay and sets environment. Reuse it in other
trust-related places where this matters.

Oddjob helper runs as root and can write to /run/ipa for the krb5.conf
overlay.

Server side of 'ipa trust-add' code calls into ipaserver/dcerpc.py and
runs under ipaapi so can only write to /tmp.  Since it is a part of the
Apache instance, it uses private /tmp mounted on tmpfs.

Fixes: https://pagure.io/freeipa/issue/8664
Related: https://pagure.io/freeipa/issue/8655
Signed-off-by: Alexander Bokovoy <aboko...@redhat.com>
Reviewed-By: Christian Heimes <chei...@redhat.com>
Reviewed-By: Rob Crittenden <rcrit...@redhat.com>
---
 .../com.redhat.idm.trust-fetch-domains.in     | 179 +++++++-----------
 ipapython/ipautil.py                          |  50 +++++
 ipaserver/dcerpc.py                           |  52 ++---
 3 files changed, 148 insertions(+), 133 deletions(-)

diff --git a/install/oddjob/com.redhat.idm.trust-fetch-domains.in b/install/oddjob/com.redhat.idm.trust-fetch-domains.in
index 92fb4ce9b7a..22af701a184 100644
--- a/install/oddjob/com.redhat.idm.trust-fetch-domains.in
+++ b/install/oddjob/com.redhat.idm.trust-fetch-domains.in
@@ -4,16 +4,12 @@ from ipaserver import dcerpc
 from ipaserver.install.installutils import ScriptError
 from ipapython import config, ipautil
 from ipalib import api
-from ipalib.facts import is_ipa_configured
 from ipapython.dn import DN
 from ipapython.dnsutil import DNSName
 from ipaplatform.constants import constants
 from ipaplatform.paths import paths
-import io
 import sys
 import os
-import tempfile
-import textwrap
 
 import six
 import gssapi
@@ -121,43 +117,6 @@ def get_forest_root_domain(api_instance, trusted_domain, server=None):
     return remote_domain.info["dns_forest"]
 
 
-def generate_krb5_config(realm, server):
-    """Generate override krb5 config file for trusted domain DC access
-
-    :param realm: realm of the trusted AD domain
-    :param server: server to override KDC to
-
-    :returns: tuple (temporary config file name, KRB5_CONFIG string)
-    """
-    cfg = paths.KRB5_CONF
-    tcfg = None
-    if server:
-        content = textwrap.dedent(u"""
-            [realms]
-               %s = {
-                   kdc = %s
-               }
-            """) % (
-            realm.upper(),
-            server,
-        )
-
-        (fd, tcfg) = tempfile.mkstemp(dir="/run/ipa",
-                prefix="krb5conf", text=True)
-        with io.open(fd, mode='w', encoding='utf-8') as o:
-            o.write(content)
-        cfg = ":".join([tcfg, cfg])
-    return (tcfg, cfg)
-
-
-if not is_ipa_configured():
-    # LSB status code 6: program is not configured
-    raise ScriptError(
-        "IPA is not configured "
-        + "(see man pages of ipa-server-install for help)",
-        6,
-    )
-
 if not os.getegid() == 0:
     # LSB status code 4: user had insufficient privilege
     raise ScriptError("You must be root to run ipactl.", 4)
@@ -234,97 +193,95 @@ trusted_domain = trusted_domain_entry.single_value.get("cn").lower()
 # At this point if we didn't find trusted forest name, an exception will be raised
 # and script will quit. This is actually intended.
 
+rc = 0
+
 # Generate MIT Kerberos configuration file that potentially overlays
 # the KDC to connect to for a trusted domain to allow --server option
 # to take precedence.
-cfg_file, cfg = generate_krb5_config(trusted_domain, options.server)
+with ipautil.private_krb5_config(trusted_domain, options.server) as cfg_file:
+    if not (options.admin and options.password):
+        oneway_keytab_name = os.path.join("/var/lib/sss/keytabs/",
+                                          trusted_domain + ".keytab")
 
-if not (options.admin and options.password):
-    oneway_keytab_name = "/var/lib/sss/keytabs/" + trusted_domain + ".keytab"
-    oneway_principal = str(
-        "%s$@%s" % (own_trust_flatname, trusted_domain.upper())
-    )
+        oneway_principal = str(
+            "%s$@%s" % (own_trust_flatname, trusted_domain.upper())
+        )
 
-    # If keytab does not exist, retrieve it
-    if not os.path.isfile(oneway_keytab_name):
-        retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal)
+        # If keytab does not exist, retrieve it
+        if not os.path.isfile(oneway_keytab_name):
+            retrieve_keytab(api, ccache_name,
+                            oneway_keytab_name, oneway_principal)
 
-    try:
-        have_ccache = False
         try:
-            # The keytab may have stale key material (from older trust-add run)
-            cred = kinit_keytab(
-                oneway_principal,
-                oneway_keytab_name,
-                oneway_ccache_name,
-                config=cfg,
-            )
-            if cred.lifetime > 0:
-                have_ccache = True
-        except (gssapi.exceptions.ExpiredCredentialsError, gssapi.raw.misc.GSSError):
-            pass
-        if not have_ccache:
+            have_ccache = False
+            try:
+                # The keytab may have stale key material (from older trust-add run)
+                cred = kinit_keytab(
+                    oneway_principal,
+                    oneway_keytab_name,
+                    oneway_ccache_name,
+                )
+                if cred.lifetime > 0:
+                    have_ccache = True
+            except (gssapi.exceptions.ExpiredCredentialsError, gssapi.raw.misc.GSSError):
+                pass
+            if not have_ccache:
+                if os.path.exists(oneway_ccache_name):
+                    os.unlink(oneway_ccache_name)
+                kinit_keytab(
+                    oneway_principal,
+                    oneway_keytab_name,
+                    oneway_ccache_name,
+                )
+        except (gssapi.exceptions.GSSError, gssapi.raw.misc.GSSError):
+            # If there was failure on using keytab, assume it is stale and retrieve again
+            retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal)
             if os.path.exists(oneway_ccache_name):
                 os.unlink(oneway_ccache_name)
-            kinit_keytab(
+            cred = kinit_keytab(
                 oneway_principal,
                 oneway_keytab_name,
                 oneway_ccache_name,
-                config=cfg,
             )
-    except (gssapi.exceptions.GSSError, gssapi.raw.misc.GSSError):
-        # If there was failure on using keytab, assume it is stale and retrieve again
-        retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal)
-        if os.path.exists(oneway_ccache_name):
-            os.unlink(oneway_ccache_name)
-        cred = kinit_keytab(
-            oneway_principal,
-            oneway_keytab_name,
+    else:
+        cred = kinit_password(
+            options.admin,
+            options.password,
             oneway_ccache_name,
-            config=cfg,
+            canonicalize=True,
+            enterprise=True,
         )
-else:
-    cred = kinit_password(
-        options.admin,
-        options.password,
-        oneway_ccache_name,
-        canonicalize=True,
-        enterprise=True,
-        config=cfg,
-    )
 
-if cred and cred.lifetime > 0:
-    have_ccache = True
+    if cred and cred.lifetime > 0:
+        have_ccache = True
 
-if not have_ccache:
-    sys.exit(1)
+    if not have_ccache:
+        rc = 1
+        raise GeneratorExit
 
-# We are done: we have ccache with TDO credentials and can fetch domains
-ipa_domain = api.env.domain
-os.environ["KRB5CCNAME"] = oneway_ccache_name
-os.environ["KRB5_CONFIG"] = cfg
+    # We are done: we have ccache with TDO credentials and can fetch domains
+    ipa_domain = api.env.domain
+    os.environ["KRB5CCNAME"] = oneway_ccache_name
 
-# retrieve the forest root domain name and contact it to retrieve trust
-# topology info
-forest_root = get_forest_root_domain(
-    api, trusted_domain, server=options.server
-)
-domains = dcerpc.fetch_domains(
-    api, ipa_domain, forest_root, creds=True, server=options.server
-)
+    # retrieve the forest root domain name and contact it to retrieve trust
+    # topology info
+    forest_root = get_forest_root_domain(
+        api, trusted_domain, server=options.server
+    )
+    domains = dcerpc.fetch_domains(
+        api, ipa_domain, forest_root, creds=True, server=options.server
+    )
 
-if old_ccache:
-    os.environ["KRB5CCNAME"] = old_ccache
+    # We still need to use the override for KDC configuration in case the --server
+    # was forced, thus only switch to the old ccache.
+    if old_ccache:
+        os.environ["KRB5CCNAME"] = old_ccache
 
-if old_config:
-    os.environ["KRB5_CONFIG"] = old_config
 
-if cfg_file:
-    os.remove(cfg_file)
+    trust_domain_object = api.Command.trust_show(trusted_domain, raw=True)[
+        "result"
+    ]
 
-trust_domain_object = api.Command.trust_show(trusted_domain, raw=True)[
-    "result"
-]
-trust.add_new_domains_from_trust(api, None, trust_domain_object, domains)
+    trust.add_new_domains_from_trust(api, None, trust_domain_object, domains)
 
-sys.exit(0)
+sys.exit(rc)
diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py
index fcb7f4d9731..b5685f1e6b7 100644
--- a/ipapython/ipautil.py
+++ b/ipapython/ipautil.py
@@ -36,6 +36,8 @@
 import datetime
 import netaddr
 import time
+import textwrap
+import io
 from contextlib import contextmanager
 import locale
 import collections
@@ -1447,6 +1449,54 @@ def private_ccache(path=None):
                 pass
 
 
+@contextmanager
+def private_krb5_config(realm, server, dir="/run/ipa"):
+    """Generate override krb5 config file for a trusted domain DC access
+    Provide a context where environment variable KRB5_CONFIG is set
+    with the overlay on top of paths.KRB5_CONF. Overlay's file path
+    is passed to the context in case it is needed for something else
+
+    :param realm: realm of the trusted AD domain
+    :param server: server to override KDC to
+    :param dir: path where to create a temporary krb5.conf overlay
+    """
+    cfg = paths.KRB5_CONF
+    tcfg = None
+    if server:
+        content = textwrap.dedent(u"""
+            [realms]
+               %s = {
+                   kdc = %s
+               }
+            """) % (
+            realm.upper(),
+            server,
+        )
+
+        (fd, tcfg) = tempfile.mkstemp(dir=dir, prefix="krb5conf", text=True)
+
+        with io.open(fd, mode='w', encoding='utf-8') as o:
+            o.write(content)
+        cfg = ":".join([tcfg, cfg])
+
+    original_value = os.environ.get('KRB5_CONFIG', None)
+
+    os.environ['KRB5_CONFIG'] = cfg
+
+    try:
+        yield tcfg
+    except GeneratorExit:
+        pass
+    finally:
+        if original_value is not None:
+            os.environ['KRB5_CONFIG'] = original_value
+        else:
+            os.environ.pop('KRB5_CONFIG', None)
+
+        if tcfg is not None and os.path.exists(tcfg):
+            os.remove(tcfg)
+
+
 if six.PY2:
     def fsdecode(value):
         """
diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
index 6f4defde2d4..2e716570d1e 100644
--- a/ipaserver/dcerpc.py
+++ b/ipaserver/dcerpc.py
@@ -1698,20 +1698,23 @@ def get_instance(local_flatname):
                         error=_('Non-Kerberos user name was specified, '
                                 'please provide user@REALM variant instead'))
                 realm_admin = r"%s@%s" % (
-                    realm_admin, rd.info['dns_forest'].upper())
+                    realm_admin, rd.info['dns_domain'].upper())
+                realm = rd.info['dns_domain'].upper()
             auth_string = r"%s%%%s" \
                           % (realm_admin, realm_passwd)
-            td = get_instance(local_flatname)
-            td.creds.set_kerberos_state(credentials.MUST_USE_KERBEROS)
-            enforce_smb_encryption(td.creds)
-            td.creds.parse_string(auth_string)
-            td.creds.set_workstation(hostname)
-            if realm_server is None:
-                # we must have rd.info['dns_hostname'] then
-                # as it is part of the anonymous discovery
-                td.retrieve(rd.info['dns_hostname'])
-            else:
-                td.retrieve(realm_server)
+            with ipautil.private_krb5_config(realm, realm_server, dir='/tmp'):
+                with ipautil.private_ccache():
+                    td = get_instance(local_flatname)
+                    td.creds.set_kerberos_state(credentials.MUST_USE_KERBEROS)
+                    enforce_smb_encryption(td.creds)
+                    td.creds.parse_string(auth_string)
+                    td.creds.set_workstation(hostname)
+                    if realm_server is None:
+                        # we must have rd.info['dns_hostname'] then
+                        # as it is part of the anonymous discovery
+                        td.retrieve(rd.info['dns_hostname'])
+                    else:
+                        td.retrieve(realm_server)
             td.read_only = False
             return td
 
@@ -1832,15 +1835,18 @@ def join_ad_full_credentials(self, realm, realm_server, realm_admin,
             # Establishing trust may throw an exception for topology
             # conflict. If it was solved, re-establish the trust again
             # Otherwise let the CLI to display a message about the conflict
-            try:
-                self.remote_domain.establish_trust(self.local_domain,
-                                                   trustdom_pass,
-                                                   trust_type, trust_external)
-            except TrustTopologyConflictSolved:
-                # we solved topology conflict, retry again
-                self.remote_domain.establish_trust(self.local_domain,
-                                                   trustdom_pass,
-                                                   trust_type, trust_external)
+            with ipautil.private_krb5_config(realm, realm_server, dir='/tmp'):
+                try:
+                    self.remote_domain.establish_trust(self.local_domain,
+                                                       trustdom_pass,
+                                                       trust_type,
+                                                       trust_external)
+                except TrustTopologyConflictSolved:
+                    # we solved topology conflict, retry again
+                    self.remote_domain.establish_trust(self.local_domain,
+                                                       trustdom_pass,
+                                                       trust_type,
+                                                       trust_external)
 
             try:
                 self.local_domain.establish_trust(self.remote_domain,
@@ -1856,7 +1862,9 @@ def join_ad_full_credentials(self, realm, realm_server, realm_admin,
             # it only does verification for outbound trusts.
             result = True
             if trust_type == TRUST_BIDIRECTIONAL:
-                result = self.remote_domain.verify_trust(self.local_domain)
+                with ipautil.private_krb5_config(realm,
+                                                 realm_server, dir='/tmp'):
+                    result = self.remote_domain.verify_trust(self.local_domain)
             return dict(
                         local=self.local_domain,
                         remote=self.remote_domain,

From 863324cec77e89b3bee1e40dd274caa5147e17ea Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <aboko...@redhat.com>
Date: Fri, 22 Jan 2021 17:29:15 +0200
Subject: [PATCH 10/10] use a constant instead of /var/lib/sss/keytabs

Signed-off-by: Alexander Bokovoy <aboko...@redhat.com>
Reviewed-By: Christian Heimes <chei...@redhat.com>
Reviewed-By: Rob Crittenden <rcrit...@redhat.com>
---
 install/oddjob/com.redhat.idm.trust-fetch-domains.in | 6 ++++--
 ipaplatform/base/paths.py                            | 1 +
 ipatests/pytest_ipa/integration/tasks.py             | 4 ++--
 3 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/install/oddjob/com.redhat.idm.trust-fetch-domains.in b/install/oddjob/com.redhat.idm.trust-fetch-domains.in
index 22af701a184..45c1f1463d5 100644
--- a/install/oddjob/com.redhat.idm.trust-fetch-domains.in
+++ b/install/oddjob/com.redhat.idm.trust-fetch-domains.in
@@ -178,7 +178,9 @@ api.Backend.ldap2.connect(ccache_name)
 
 # Retrieve own NetBIOS name and trusted forest's name.
 # We use script's input to retrieve the trusted forest's name to sanitize input
-# for file-level access as we might need to wipe out keytab in /var/lib/sss/keytabs
+# for file-level access as we might need to wipe out keytab in
+# paths.SSSD_KEYTABS_DIR
+
 own_trust_dn = DN(
     ("cn", api.env.domain), ("cn", "ad"), ("cn", "etc"), api.env.basedn
 )
@@ -200,7 +202,7 @@ rc = 0
 # to take precedence.
 with ipautil.private_krb5_config(trusted_domain, options.server) as cfg_file:
     if not (options.admin and options.password):
-        oneway_keytab_name = os.path.join("/var/lib/sss/keytabs/",
+        oneway_keytab_name = os.path.join(paths.SSSD_KEYTABS_DIR,
                                           trusted_domain + ".keytab")
 
         oneway_principal = str(
diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 99e75f23587..e26225140b7 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -346,6 +346,7 @@ class BasePathNamespace:
     SSSD_PUBCONF_DIR = "/var/lib/sss/pubconf"
     SSSD_PUBCONF_KNOWN_HOSTS = "/var/lib/sss/pubconf/known_hosts"
     SSSD_PUBCONF_KRB5_INCLUDE_D_DIR = "/var/lib/sss/pubconf/krb5.include.d/"
+    SSSD_KEYTABS_DIR = "/var/lib/sss/keytabs"
     VAR_LOG_AUDIT = "/var/log/audit/audit.log"
     VAR_LOG_HTTPD_DIR = "/var/log/httpd"
     VAR_LOG_HTTPD_ERROR = "/var/log/httpd/error_log"
diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py
index 6046869752a..92a4f7dcde4 100755
--- a/ipatests/pytest_ipa/integration/tasks.py
+++ b/ipatests/pytest_ipa/integration/tasks.py
@@ -1018,8 +1018,8 @@ def uninstall_master(host, ignore_topology_disconnect=True,
                       paths.IPA_RENEWAL_LOCK,
                       paths.REPLICA_INFO_GPG_TEMPLATE % host.hostname],
                      raiseonerr=False)
-    host.run_command("find /var/lib/sss/keytabs -name '*.keytab' | "
-                     "xargs rm -fv", raiseonerr=False)
+    host.run_command("find %s -name '*.keytab' | "
+                     "xargs rm -fv" % paths.SSSD_KEYTABS_DIR, raiseonerr=False)
     host.run_command("find /run/ipa -name 'krb5*' | xargs rm -fv",
                      raiseonerr=False)
     if clean:
_______________________________________________
FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org
To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org
Fedora Code of Conduct: 
https://docs.fedoraproject.org/en-US/project/code-of-conduct/
List Guidelines: https://fedoraproject.org/wiki/Mailing_list_guidelines
List Archives: 
https://lists.fedorahosted.org/archives/list/freeipa-devel@lists.fedorahosted.org

Reply via email to