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