diff --git a/SOURCES/0027-kdb-fix-vulnerability-in-GCD-rules-handling.patch b/SOURCES/0027-kdb-fix-vulnerability-in-GCD-rules-handling.patch new file mode 100644 index 0000000..028fc04 --- /dev/null +++ b/SOURCES/0027-kdb-fix-vulnerability-in-GCD-rules-handling.patch @@ -0,0 +1,341 @@ +From 0a48726e104282fb40d8f471ebb306bc9134cb0c Mon Sep 17 00:00:00 2001 +From: Julien Rische +Date: Tue, 19 Mar 2024 12:24:40 +0100 +Subject: [PATCH] kdb: fix vulnerability in GCD rules handling + +The initial implementation of MS-SFU by MIT Kerberos was missing some +a condition for granting the "forwardable" flag on S4U2Self tickets. +Fixing this mistake required to add a special case for the +check_allowed_to_delegate() function: if the target service argument is +NULL, then it means the KDC is probing for general constrained +delegation rules, not actually checking a specific S4U2Proxy request. + +In commit e86807b5, the behavior of ipadb_match_acl() was modified to +match the changes from upstream MIT Kerberos a441fbe3. However, a +mistake resulted in this mechanism to apply in cases where target +service argument is set AND unset. This results in S4U2Proxy requests to +be accepted regardless of the fact there is a matching service +delegation rule or not. + +This vulnerability does not affect services having RBCD (resource-based +constrained delegation) rules. + +This fixes CVE-2024-2698 + +Signed-off-by: Julien Rische +--- + daemons/ipa-kdb/README.s4u2proxy.txt | 19 ++- + daemons/ipa-kdb/ipa_kdb_delegation.c | 191 +++++++++++++++------------ + 2 files changed, 118 insertions(+), 92 deletions(-) + +diff --git a/daemons/ipa-kdb/README.s4u2proxy.txt b/daemons/ipa-kdb/README.s4u2proxy.txt +index 254fcc4d1..ab34aff36 100644 +--- a/daemons/ipa-kdb/README.s4u2proxy.txt ++++ b/daemons/ipa-kdb/README.s4u2proxy.txt +@@ -12,9 +12,7 @@ much more easily managed. + + The grouping mechanism has been built so that lookup is highly optimized + and is basically reduced to a single search that uses the derefernce +-control. Speed is very important in this case because KDC operations +-time out very quickly and unless we add a caching layer in ipa-kdb we +-must keep the number of searches down to avoid client timeouts. ++control. + + The grouping mechanism is very simple a groupOfPrincipals object is + introduced, this Auxiliary class have a single optional attribute called +@@ -112,8 +110,7 @@ kinit -kt /etc/httpd/conf/ipa.keytab HTTP/ipaserver.example.com + kvno -U admin HTTP/ipaserver.example.com + + # Perform S4U2Proxy +-kvno -k /etc/httpd/conf/ipa.keytab -U admin -P HTTP/ipaserver.example.com +-ldap/ipaserver.example.com ++kvno -U admin -P ldap/ipaserver.example.com + + + If this works it means you successfully impersonated the admin user with +@@ -125,6 +122,18 @@ modprinc -ok_to_auth_as_delegate HTTP/ipaserver.example.com + Simo. + + ++If IPA is compiled with krb5 1.20 and newer (KDB DAL >= 9), then the ++behavior of S4U2Self changes: S4U2Self TGS-REQs produce forwardable ++tickets for all requesters, except if the requester principal is set as ++the proxy (impersonating service) in at least one `servicedelegation` ++rule. In this case, even if the rule has no target, the KDC will ++response to S4U2Self requests with a non-forwardable ticket. Hence, ++granting the `ok_to_auth_as_delegate` permission to the proxy service ++remains the only way for this service to obtain the evidence ticket ++required for general constrained delegation requests if this ticket is ++not provided by the client. ++ ++ + [1] + Note that here I use the term proxy in a different way than it is used in + the krb interfaces. It may seem a bit confusing but I think people will +diff --git a/daemons/ipa-kdb/ipa_kdb_delegation.c b/daemons/ipa-kdb/ipa_kdb_delegation.c +index de82174ad..3581f3c79 100644 +--- a/daemons/ipa-kdb/ipa_kdb_delegation.c ++++ b/daemons/ipa-kdb/ipa_kdb_delegation.c +@@ -91,120 +91,110 @@ static bool ipadb_match_member(char *princ, LDAPDerefRes *dres) + return false; + } + +-static krb5_error_code ipadb_match_acl(krb5_context kcontext, +- LDAPMessage *results, +- krb5_const_principal client, +- krb5_const_principal target) ++#if KRB5_KDB_DAL_MAJOR_VERSION >= 9 ++static krb5_error_code ++ipadb_has_acl(krb5_context kcontext, LDAPMessage *ldap_acl, bool *res) + { + struct ipadb_context *ipactx; +- krb5_error_code kerr; +- LDAPMessage *lentry; +- LDAPDerefRes *deref_results; +- LDAPDerefRes *dres; +- char *client_princ = NULL; +- char *target_princ = NULL; +- bool client_missing; +- bool client_found; +- bool target_found; +- bool is_constraint_delegation = false; +- size_t nrules = 0; +- int ret; ++ bool in_res = false; ++ krb5_error_code kerr = 0; + + ipactx = ipadb_get_context(kcontext); +- if (!ipactx) { ++ if (!ipactx) + return KRB5_KDB_DBNOTINITED; +- } + +- if ((client != NULL) && (target != NULL)) { +- kerr = krb5_unparse_name(kcontext, client, &client_princ); +- if (kerr != 0) { +- goto done; +- } +- kerr = krb5_unparse_name(kcontext, target, &target_princ); +- if (kerr != 0) { +- goto done; +- } +- } else { +- is_constraint_delegation = true; ++ switch (ldap_count_entries(ipactx->lcontext, ldap_acl)) { ++ case 0: ++ break; ++ case -1: ++ kerr = EINVAL; ++ goto end; ++ default: ++ in_res = true; ++ goto end; + } + +- lentry = ldap_first_entry(ipactx->lcontext, results); +- if (!lentry) { +- kerr = ENOENT; +- goto done; +- } ++end: ++ if (res) ++ *res = in_res; ++ ++ return kerr; ++} ++#endif ++ ++static krb5_error_code ++ipadb_match_acl(krb5_context kcontext, LDAPMessage *ldap_acl, ++ krb5_const_principal client, krb5_const_principal target) ++{ ++ struct ipadb_context *ipactx; ++ LDAPMessage *rule; ++ LDAPDerefRes *acis, *aci; ++ char *client_princ = NULL, *target_princ= NULL; ++ bool client_missing, client_found, target_found; ++ int lerr; ++ krb5_error_code kerr; ++ ++ ipactx = ipadb_get_context(kcontext); ++ if (!ipactx) ++ return KRB5_KDB_DBNOTINITED; ++ ++ kerr = krb5_unparse_name(kcontext, client, &client_princ); ++ if (kerr) ++ goto end; ++ ++ kerr = krb5_unparse_name(kcontext, target, &target_princ); ++ if (kerr) ++ goto end; + + /* the default is that we fail */ +- kerr = ENOENT; ++ kerr = KRB5KDC_ERR_BADOPTION; + +- while (lentry) { ++ for (rule = ldap_first_entry(ipactx->lcontext, ldap_acl); ++ rule; ++ rule = ldap_next_entry(ipactx->lcontext, rule)) ++ { + /* both client and target must be found in the same ACI */ + client_missing = true; + client_found = false; + target_found = false; + +- ret = ipadb_ldap_deref_results(ipactx->lcontext, lentry, +- &deref_results); +- switch (ret) { ++ lerr = ipadb_ldap_deref_results(ipactx->lcontext, rule, &acis); ++ switch (lerr) { + case 0: +- for (dres = deref_results; dres; dres = dres->next) { +- nrules++; +- if (is_constraint_delegation) { +- /* +- Microsoft revised the S4U2Proxy rules for forwardable +- tickets. All S4U2Proxy operations require forwardable +- evidence tickets, but S4U2Self should issue a +- forwardable ticket if the requesting service has no +- ok-to-auth-as-delegate bit but also no constrained +- delegation privileges for traditional S4U2Proxy. +- Implement these rules, extending the +- check_allowed_to_delegate() DAL method so that the KDC +- can ask if a principal has any delegation privileges. +- +- Since target principal is NULL and client principal is +- NULL in this case, we simply calculate number of rules associated +- with the server principal to decide whether to deny forwardable bit +- */ +- continue; +- } +- if (client_found == false && +- strcasecmp(dres->derefAttr, "ipaAllowToImpersonate") == 0) { ++ for (aci = acis; aci; aci = aci->next) { ++ if (!client_found && ++ 0 == strcasecmp(aci->derefAttr, "ipaAllowToImpersonate")) ++ { + /* NOTE: client_missing is used to signal that the + * attribute was completely missing. This signals that + * ANY client is allowed to be impersonated. + * This logic is valid only for clients, not for targets */ + client_missing = false; +- client_found = ipadb_match_member(client_princ, dres); ++ client_found = ipadb_match_member(client_princ, aci); + } +- if (target_found == false && +- strcasecmp(dres->derefAttr, "ipaAllowedTarget") == 0) { +- target_found = ipadb_match_member(target_princ, dres); ++ if (!target_found && ++ 0 == strcasecmp(aci->derefAttr, "ipaAllowedTarget")) ++ { ++ target_found = ipadb_match_member(target_princ, aci); + } + } + +- ldap_derefresponse_free(deref_results); ++ ldap_derefresponse_free(acis); + break; + case ENOENT: + break; + default: +- kerr = ret; +- goto done; ++ kerr = lerr; ++ goto end; + } + +- if ((client_found == true || client_missing == true) && +- target_found == true) { ++ if ((client_found || client_missing) && target_found) { + kerr = 0; +- goto done; ++ goto end; + } +- +- lentry = ldap_next_entry(ipactx->lcontext, lentry); +- } +- +- if (nrules > 0) { +- kerr = 0; + } + +-done: ++end: + krb5_free_unparsed_name(kcontext, client_princ); + krb5_free_unparsed_name(kcontext, target_princ); + return kerr; +@@ -223,7 +213,7 @@ krb5_error_code ipadb_check_allowed_to_delegate(krb5_context kcontext, + char *srv_principal = NULL; + krb5_db_entry *proxy_entry = NULL; + struct ipadb_e_data *ied_server, *ied_proxy; +- LDAPMessage *res = NULL; ++ LDAPMessage *ldap_gcd_acl = NULL; + + if (proxy != NULL) { + /* Handle the case where server == proxy, this is allowed in S4U */ +@@ -261,26 +251,53 @@ krb5_error_code ipadb_check_allowed_to_delegate(krb5_context kcontext, + goto done; + } + +- kerr = ipadb_get_delegation_acl(kcontext, srv_principal, &res); ++ /* Load general constrained delegation rules */ ++ kerr = ipadb_get_delegation_acl(kcontext, srv_principal, &ldap_gcd_acl); + if (kerr) { + goto done; + } + +- kerr = ipadb_match_acl(kcontext, res, client, proxy); +- if (kerr) { +- goto done; ++#if KRB5_KDB_DAL_MAJOR_VERSION >= 9 ++ /* ++ * Microsoft revised the S4U2Proxy rules for forwardable tickets. All ++ * S4U2Proxy operations require forwardable evidence tickets, but ++ * S4U2Self should issue a forwardable ticket if the requesting service ++ * has no ok-to-auth-as-delegate bit but also no constrained delegation ++ * privileges for traditional S4U2Proxy. Implement these rules, ++ * extending the check_allowed_to_delegate() DAL method so that the KDC ++ * can ask if a principal has any delegation privileges. ++ * ++ * If target service principal is NULL, and the impersonating service has ++ * at least one GCD rule, then succeed. ++ */ ++ if (!proxy) { ++ bool has_gcd_rules; ++ ++ kerr = ipadb_has_acl(kcontext, ldap_gcd_acl, &has_gcd_rules); ++ if (!kerr) ++ kerr = has_gcd_rules ? 0 : KRB5KDC_ERR_BADOPTION; ++ } else if (client) { ++#else ++ if (client && proxy) { ++#endif ++ kerr = ipadb_match_acl(kcontext, ldap_gcd_acl, client, proxy); ++ } else { ++ /* client and/or proxy is missing */ ++ kerr = KRB5KDC_ERR_BADOPTION; + } ++ if (kerr) ++ goto done; + + done: + if (kerr) { +-#if KRB5_KDB_DAL_MAJOR_VERSION < 9 +- kerr = KRB5KDC_ERR_POLICY; +-#else ++#if KRB5_KDB_DAL_MAJOR_VERSION >= 9 + kerr = KRB5KDC_ERR_BADOPTION; ++#else ++ kerr = KRB5KDC_ERR_POLICY; + #endif + } + ipadb_free_principal(kcontext, proxy_entry); + krb5_free_unparsed_name(kcontext, srv_principal); +- ldap_msgfree(res); ++ ldap_msgfree(ldap_gcd_acl); + return kerr; + } +-- +2.44.0 + diff --git a/SOURCES/0028-kdb-apply-combinatorial-logic-for-ticket-flags.patch b/SOURCES/0028-kdb-apply-combinatorial-logic-for-ticket-flags.patch new file mode 100644 index 0000000..d4c98a6 --- /dev/null +++ b/SOURCES/0028-kdb-apply-combinatorial-logic-for-ticket-flags.patch @@ -0,0 +1,615 @@ +From 542e12325afc2f64298f90296760235bfdcef04a Mon Sep 17 00:00:00 2001 +From: Julien Rische +Date: Mon, 25 Mar 2024 18:25:52 +0200 +Subject: [PATCH] kdb: apply combinatorial logic for ticket flags + +The initial design for ticket flags was implementing this logic: +* If a ticket policy is defined for the principal entry, use flags from + this policy if they are set. Otherwise, use default ticket flags. +* If no ticket policy is defined for the principal entry, but there is a + global one, use flags from the global ticket policy if they are set. + Otherwise, use default ticket flags. +* If no policy (principal nor global) is defined, use default ticket + flags. + +However, this logic was broken by a1165ffb which introduced creation of +a principal-level ticket policy in case the ticket flag set is modified. +This was typically the case for the -allow_tix flag, which was set +virtually by the KDB driver when a user was locked until they initialize +their password on first kinit pre-authentication. + +This was causing multiple issues, which are mitigated by the new +approach: + +Now flags from each level are combined together. There flags like ++requires_preauth which are set systematically by the KDB diver, as +well as -allow_tix which is set based on the value of "nsAccountLock". +This commit also adds the implicit -allow_svr ticket flag for user +principals to protect users against Kerberoast-type attacks. None of +these flags are stored in the LDAP database, they are hard-coded in the +KDB driver. + +In addition to these "virtual" ticket flags, flags from both global and +principal ticket policies are applied (if these policies exist). + +Principal ticket policies are not supported for hosts and services, but +this is only an HTTP API limitation. The "krbTicketPolicyAux" object +class is supported for all account types. This is required for ticket +flags like +ok_to_auth_as_delegate. Such flags can be set using "ipa +host-mod" and "ipa serivce-mod", or using kadmin's "modprinc". + +It is possible to ignore flags from the global ticket policy or default +flags like -allow_svr for a user principal by setting the +"final_user_tkt_flags" string attribute to "true" in kadmin. In this +case, any ticket flag can be configured in the principal ticket policy, +except requires_preauth and allow_tix. + +When in IPA setup mode (using the "ipa-setup-override-restrictions" KDB +argument), all the system described above is disabled and ticket flags +are written in the principal ticket policy as they are provided. This is +required to initialize the Kerberos LDAP container during IPA server +installation. + +This fixes CVE-2024-3183 + +Signed-off-by: Julien Rische +--- + daemons/ipa-kdb/ipa_kdb.h | 43 ++++ + daemons/ipa-kdb/ipa_kdb_principals.c | 353 +++++++++++++++++++++++---- + util/ipa_krb5.c | 18 ++ + util/ipa_krb5.h | 4 + + 4 files changed, 365 insertions(+), 53 deletions(-) + +diff --git a/daemons/ipa-kdb/ipa_kdb.h b/daemons/ipa-kdb/ipa_kdb.h +index 7baf4697f..85cabe142 100644 +--- a/daemons/ipa-kdb/ipa_kdb.h ++++ b/daemons/ipa-kdb/ipa_kdb.h +@@ -94,6 +94,34 @@ + #define IPA_KRB_AUTHZ_DATA_ATTR "ipaKrbAuthzData" + #define IPA_USER_AUTH_TYPE "ipaUserAuthType" + ++/* Virtual managed ticket flags like "-allow_tix", are always controlled by the ++ * "nsAccountLock" attribute, such flags should never be set in the database. ++ * The following expression combine all of them, and is used to filter them ++ * out. */ ++#define IPA_KDB_TKTFLAGS_VIRTUAL_MANAGED_ALL (KRB5_KDB_DISALLOW_ALL_TIX) ++ ++/* Virtual static ticket flags are hard-coded in the KDB driver. */ ++/* Virtual static mandatory flags are set systematically and implicitly for all ++ * principals. They are filtered out from database ticket flags updates. ++ * (However, "KRB5_KDB_REQUIRES_PRE_AUTH" can still be unset by the ++ * "KDC:Disable Default Preauth for SPNs" global setting) */ ++#define IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_MANDATORY (KRB5_KDB_REQUIRES_PRE_AUTH) ++/* Virtual static default ticket flags are implicitly set for user and non-user ++ * (SPN) principals, and not stored in the database. ++ * (Except if the "IPA_KDB_STRATTR_FINAL_TKTFLAGS" string attribute is "true" ++ * the principal) */ ++/* Virtual static default user ticket flags are set for users only. The ++ * "-allow_svr" flag is set to protect them from CVE-2024-3183. */ ++#define IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_DEFAULTS_USER (KRB5_KDB_DISALLOW_SVR) ++#define IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_DEFAULTS_SPN (0) ++ ++/* If this string attribute is set to "true", then only the virtual managed and ++ * virtual static mandatory ticket flags are applied and filtered out from ++ * database read and write operations for the concerned user principal. ++ * Configurable principal ticket flags are applied, but not the configurable ++ * global ticket policy flags. */ ++#define IPA_KDB_STRATTR_FINAL_USER_TKTFLAGS "final_user_tkt_flags" ++ + struct ipadb_mspac; + struct dom_sid; + +@@ -178,6 +206,21 @@ struct ipadb_e_data { + struct dom_sid *sid; + }; + ++inline static krb5_error_code ++ipadb_get_edata(krb5_db_entry *entry, struct ipadb_e_data **ied) ++{ ++ struct ipadb_e_data *in_ied; ++ ++ in_ied = (struct ipadb_e_data *)entry->e_data; ++ if (!in_ied || in_ied->magic != IPA_E_DATA_MAGIC) ++ return EINVAL; ++ ++ if (ied) ++ *ied = in_ied; ++ ++ return 0; ++} ++ + struct ipadb_context *ipadb_get_context(krb5_context kcontext); + int ipadb_get_connection(struct ipadb_context *ipactx); + +diff --git a/daemons/ipa-kdb/ipa_kdb_principals.c b/daemons/ipa-kdb/ipa_kdb_principals.c +index 07cc87746..6eb542d4f 100644 +--- a/daemons/ipa-kdb/ipa_kdb_principals.c ++++ b/daemons/ipa-kdb/ipa_kdb_principals.c +@@ -706,9 +706,12 @@ static krb5_error_code ipadb_parse_ldap_entry(krb5_context kcontext, + "krbTicketFlags", &result); + if (ret == 0) { + entry->attributes = result; +- } else { +- *polmask |= TKTFLAGS_BIT; + } ++ /* Since principal, global policy, and virtual ticket flags are combined, ++ * they must always be resolved, except if we are in IPA setup mode (because ++ * ticket policies and virtual ticket flags are irrelevant in this case). */ ++ if (!ipactx->override_restrictions) ++ *polmask |= TKTFLAGS_BIT; + + ret = ipadb_ldap_attr_to_int(lcontext, lentry, + "krbMaxTicketLife", &result); +@@ -912,7 +915,12 @@ static krb5_error_code ipadb_parse_ldap_entry(krb5_context kcontext, + goto done; + } + if (ret == 0) { +- ied->ipa_user = true; ++ if (1 == krb5_princ_size(kcontext, entry->princ)) { ++ /* A principal must be a POSIX account AND have only one element to ++ * be considered a user (this is to filter out CIFS principals). */ ++ ied->ipa_user = true; ++ } ++ + ret = ipadb_ldap_attr_to_str(lcontext, lentry, + "uid", &uidstring); + if (ret != 0 && ret != ENOENT) { +@@ -1251,23 +1259,150 @@ done: + return ret; + } + +-static krb5_flags maybe_require_preauth(struct ipadb_context *ipactx, +- krb5_db_entry *entry) ++static krb5_error_code ++are_final_tktflags(struct ipadb_context *ipactx, krb5_db_entry *entry, ++ bool *final_tktflags) + { +- const struct ipadb_global_config *config; ++ krb5_error_code kerr; + struct ipadb_e_data *ied; ++ char *str = NULL; ++ bool in_final_tktflags = false; + +- config = ipadb_get_global_config(ipactx); +- if (config && config->disable_preauth_for_spns) { +- ied = (struct ipadb_e_data *)entry->e_data; +- if (ied && ied->ipa_user != true) { +- /* not a user, assume SPN */ +- return 0; +- } ++ kerr = ipadb_get_edata(entry, &ied); ++ if (kerr) ++ goto end; ++ ++ if (!ied->ipa_user) { ++ kerr = 0; ++ goto end; + } + +- /* By default require preauth for all principals */ +- return KRB5_KDB_REQUIRES_PRE_AUTH; ++ kerr = krb5_dbe_get_string(ipactx->kcontext, entry, ++ IPA_KDB_STRATTR_FINAL_USER_TKTFLAGS, &str); ++ if (kerr) ++ goto end; ++ ++ in_final_tktflags = str && ipa_krb5_parse_bool(str); ++ ++end: ++ if (final_tktflags) ++ *final_tktflags = in_final_tktflags; ++ ++ krb5_dbe_free_string(ipactx->kcontext, str); ++ return kerr; ++} ++ ++static krb5_error_code ++add_virtual_static_tktflags(struct ipadb_context *ipactx, krb5_db_entry *entry, ++ krb5_flags *tktflags) ++{ ++ krb5_error_code kerr; ++ krb5_flags vsflg; ++ bool final_tktflags; ++ const struct ipadb_global_config *gcfg; ++ struct ipadb_e_data *ied; ++ ++ vsflg = IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_MANDATORY; ++ ++ kerr = ipadb_get_edata(entry, &ied); ++ if (kerr) ++ goto end; ++ ++ kerr = are_final_tktflags(ipactx, entry, &final_tktflags); ++ if (kerr) ++ goto end; ++ ++ /* In practice, principal ticket flags cannot be final for SPNs. */ ++ if (!final_tktflags) ++ vsflg |= ied->ipa_user ? IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_DEFAULTS_USER ++ : IPA_KDB_TKTFLAGS_VIRTUAL_STATIC_DEFAULTS_SPN; ++ ++ if (!ied->ipa_user) { ++ gcfg = ipadb_get_global_config(ipactx); ++ if (gcfg && gcfg->disable_preauth_for_spns) ++ vsflg &= ~KRB5_KDB_REQUIRES_PRE_AUTH; ++ } ++ ++ if (tktflags) ++ *tktflags |= vsflg; ++ ++end: ++ return kerr; ++} ++ ++static krb5_error_code ++get_virtual_static_tktflags_mask(struct ipadb_context *ipactx, ++ krb5_db_entry *entry, krb5_flags *mask) ++{ ++ krb5_error_code kerr; ++ krb5_flags flags = IPA_KDB_TKTFLAGS_VIRTUAL_MANAGED_ALL; ++ ++ kerr = add_virtual_static_tktflags(ipactx, entry, &flags); ++ if (kerr) ++ goto end; ++ ++ if (mask) ++ *mask = ~flags; ++ ++ kerr = 0; ++ ++end: ++ return kerr; ++} ++ ++/* Add ticket flags from the global ticket policy if it exists, otherwise ++ * succeed. If the global ticket policy is set, the "exists" parameter is set to ++ * true. */ ++static krb5_error_code ++add_global_ticket_policy_flags(struct ipadb_context *ipactx, ++ bool *gtpol_exists, krb5_flags *tktflags) ++{ ++ krb5_error_code kerr; ++ char *policy_dn; ++ char *tktflags_attr[] = { "krbticketflags", NULL }; ++ LDAPMessage *res = NULL, *first; ++ int ec, ldap_tktflags; ++ bool in_gtpol_exists = false; ++ ++ ec = asprintf(&policy_dn, "cn=%s,cn=kerberos,%s", ipactx->realm, ++ ipactx->base); ++ if (-1 == ec) { ++ kerr = ENOMEM; ++ goto end; ++ } ++ ++ kerr = ipadb_simple_search(ipactx, policy_dn, LDAP_SCOPE_BASE, ++ "(objectclass=krbticketpolicyaux)", ++ tktflags_attr, &res); ++ if (kerr) { ++ if (KRB5_KDB_NOENTRY == kerr) ++ kerr = 0; ++ goto end; ++ } ++ ++ first = ldap_first_entry(ipactx->lcontext, res); ++ if (!first) { ++ kerr = 0; ++ goto end; ++ } ++ ++ in_gtpol_exists = true; ++ ++ ec = ipadb_ldap_attr_to_int(ipactx->lcontext, first, "krbticketflags", ++ &ldap_tktflags); ++ if (0 == ec && tktflags) { ++ *tktflags |= (krb5_flags)ldap_tktflags; ++ } ++ ++ kerr = 0; ++ ++end: ++ if (gtpol_exists) ++ *gtpol_exists = in_gtpol_exists; ++ ++ ldap_msgfree(res); ++ free(policy_dn); ++ return kerr; + } + + static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, +@@ -1280,6 +1415,7 @@ static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, + char *policy_dn = NULL; + LDAPMessage *res = NULL; + LDAPMessage *first; ++ bool final_tktflags, has_local_tktpolicy = true; + int result; + int ret; + +@@ -1288,12 +1424,18 @@ static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, + return KRB5_KDB_DBNOTINITED; + } + ++ kerr = are_final_tktflags(ipactx, entry, &final_tktflags); ++ if (kerr) ++ goto done; ++ + ret = ipadb_ldap_attr_to_str(ipactx->lcontext, lentry, + "krbticketpolicyreference", &policy_dn); + switch (ret) { + case 0: + break; + case ENOENT: ++ /* If no principal ticket policy, fallback to the global one. */ ++ has_local_tktpolicy = false; + ret = asprintf(&policy_dn, "cn=%s,cn=kerberos,%s", + ipactx->realm, ipactx->base); + if (ret == -1) { +@@ -1337,12 +1479,13 @@ static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, + } + } + if (polmask & TKTFLAGS_BIT) { +- ret = ipadb_ldap_attr_to_int(ipactx->lcontext, first, +- "krbticketflags", &result); +- if (ret == 0) { +- entry->attributes |= result; +- } else { +- entry->attributes |= maybe_require_preauth(ipactx, entry); ++ /* If global ticket policy is being applied, set flags only if ++ * user principal ticket flags are not final. */ ++ if (has_local_tktpolicy || !final_tktflags) { ++ ret = ipadb_ldap_attr_to_int(ipactx->lcontext, first, ++ "krbticketflags", &result); ++ if (ret == 0) ++ entry->attributes |= result; + } + } + +@@ -1366,13 +1509,27 @@ static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, + if (polmask & MAXRENEWABLEAGE_BIT) { + entry->max_renewable_life = 604800; + } +- if (polmask & TKTFLAGS_BIT) { +- entry->attributes |= maybe_require_preauth(ipactx, entry); +- } + + kerr = 0; + } + ++ if (polmask & TKTFLAGS_BIT) { ++ /* If the principal ticket flags were applied, then flags from the ++ * global ticket policy has to be applied atop of them if user principal ++ * ticket flags are not final. */ ++ if (has_local_tktpolicy && !final_tktflags) { ++ kerr = add_global_ticket_policy_flags(ipactx, NULL, ++ &entry->attributes); ++ if (kerr) ++ goto done; ++ } ++ ++ /* Virtual static ticket flags are set regardless of database content */ ++ kerr = add_virtual_static_tktflags(ipactx, entry, &entry->attributes); ++ if (kerr) ++ goto done; ++ } ++ + done: + ldap_msgfree(res); + free(policy_dn); +@@ -1864,6 +2021,36 @@ static void ipadb_mods_free_tip(struct ipadb_mods *imods) + imods->tip--; + } + ++/* Use LDAP REPLACE operation to remove an attribute. ++ * Contrary to the DELETE operation, it will not fail if the attribute does not ++ * exist. */ ++static krb5_error_code ++ipadb_ldap_replace_remove(struct ipadb_mods *imods, char *attribute) ++{ ++ krb5_error_code kerr; ++ LDAPMod *m = NULL; ++ ++ kerr = ipadb_mods_new(imods, &m); ++ if (kerr) ++ return kerr; ++ ++ m->mod_op = LDAP_MOD_REPLACE; ++ m->mod_type = strdup(attribute); ++ if (!m->mod_type) { ++ kerr = ENOMEM; ++ goto end; ++ } ++ ++ m->mod_values = NULL; ++ ++ kerr = 0; ++ ++end: ++ if (kerr) ++ ipadb_mods_free_tip(imods); ++ return kerr; ++} ++ + static krb5_error_code ipadb_get_ldap_mod_str(struct ipadb_mods *imods, + char *attribute, char *value, + int mod_op) +@@ -2275,6 +2462,93 @@ static krb5_error_code ipadb_get_ldap_mod_auth_ind(krb5_context kcontext, + return ret; + } + ++static krb5_error_code ++update_tktflags(krb5_context kcontext, struct ipadb_mods *imods, ++ krb5_db_entry *entry, int mod_op) ++{ ++ krb5_error_code kerr; ++ struct ipadb_context *ipactx; ++ struct ipadb_e_data *ied; ++ bool final_tktflags; ++ krb5_flags tktflags_mask; ++ int tktflags; ++ ++ ipactx = ipadb_get_context(kcontext); ++ if (!ipactx) { ++ kerr = KRB5_KDB_DBNOTINITED; ++ goto end; ++ } ++ ++ if (ipactx->override_restrictions) { ++ /* In IPA setup mode, IPA edata might not be available. In this mode, ++ * ticket flags are written as they are provided. */ ++ tktflags = (int)entry->attributes; ++ } else { ++ kerr = ipadb_get_edata(entry, &ied); ++ if (kerr) ++ goto end; ++ ++ kerr = get_virtual_static_tktflags_mask(ipactx, entry, &tktflags_mask); ++ if (kerr) ++ goto end; ++ ++ kerr = are_final_tktflags(ipactx, entry, &final_tktflags); ++ if (kerr) ++ goto end; ++ ++ /* Flags from the global ticket policy are filtered out only if the user ++ * principal flags are not final. */ ++ if (!final_tktflags) { ++ krb5_flags gbl_tktflags = 0; ++ ++ kerr = add_global_ticket_policy_flags(ipactx, NULL, &gbl_tktflags); ++ if (kerr) ++ goto end; ++ ++ tktflags_mask &= ~gbl_tktflags; ++ } ++ ++ tktflags = (int)(entry->attributes & tktflags_mask); ++ ++ if (LDAP_MOD_REPLACE == mod_op && ied && !ied->has_tktpolaux) { ++ if (0 == tktflags) { ++ /* No point initializing principal ticket policy if there are no ++ * flags left after filtering out virtual and global ticket ++ * policy ones. */ ++ kerr = 0; ++ goto end; ++ } ++ ++ /* if the object does not have the krbTicketPolicyAux class ++ * we need to add it or this will fail, only for modifications. ++ * We always add this objectclass by default when doing an add ++ * from scratch. */ ++ kerr = ipadb_get_ldap_mod_str(imods, "objectclass", ++ "krbTicketPolicyAux", LDAP_MOD_ADD); ++ if (kerr) ++ goto end; ++ } ++ } ++ ++ if (tktflags != 0) { ++ kerr = ipadb_get_ldap_mod_int(imods, "krbTicketFlags", tktflags, ++ mod_op); ++ if (kerr) ++ goto end; ++ } else if (LDAP_MOD_REPLACE == mod_op) { ++ /* If the principal is not being created, and there are no custom ticket ++ * flags to be set, remove the "krbTicketFlags" attribute. */ ++ kerr = ipadb_ldap_replace_remove(imods, "krbTicketFlags"); ++ if (kerr) ++ goto end; ++ } ++ ++ kerr = 0; ++ ++end: ++ return kerr; ++} ++ + static krb5_error_code ipadb_entry_to_mods(krb5_context kcontext, + struct ipadb_mods *imods, + krb5_db_entry *entry, +@@ -2350,36 +2624,9 @@ static krb5_error_code ipadb_entry_to_mods(krb5_context kcontext, + + /* KADM5_ATTRIBUTES */ + if (entry->mask & KMASK_ATTRIBUTES) { +- /* if the object does not have the krbTicketPolicyAux class +- * we need to add it or this will fail, only for modifications. +- * We always add this objectclass by default when doing an add +- * from scratch. */ +- if ((mod_op == LDAP_MOD_REPLACE) && entry->e_data) { +- struct ipadb_e_data *ied; +- +- ied = (struct ipadb_e_data *)entry->e_data; +- if (ied->magic != IPA_E_DATA_MAGIC) { +- kerr = EINVAL; +- goto done; +- } +- +- if (!ied->has_tktpolaux) { +- kerr = ipadb_get_ldap_mod_str(imods, "objectclass", +- "krbTicketPolicyAux", +- LDAP_MOD_ADD); +- if (kerr) { +- goto done; +- } +- } +- } +- +- kerr = ipadb_get_ldap_mod_int(imods, +- "krbTicketFlags", +- (int)entry->attributes, +- mod_op); +- if (kerr) { ++ kerr = update_tktflags(kcontext, imods, entry, mod_op); ++ if (kerr) + goto done; +- } + } + + /* KADM5_MAX_LIFE */ +diff --git a/util/ipa_krb5.c b/util/ipa_krb5.c +index 1ba6d25ee..2e663c506 100644 +--- a/util/ipa_krb5.c ++++ b/util/ipa_krb5.c +@@ -38,6 +38,12 @@ const char *ipapwd_password_max_len_errmsg = \ + TOSTR(IPAPWD_PASSWORD_MAX_LEN) \ + " chars)!"; + ++/* Case-insensitive string values to by parsed as boolean true */ ++static const char *const conf_yes[] = { ++ "y", "yes", "true", "t", "1", "on", ++ NULL, ++}; ++ + /* Salt types */ + #define KRB5P_SALT_SIZE 16 + +@@ -1237,3 +1243,15 @@ done: + } + return ret; + } ++ ++bool ipa_krb5_parse_bool(const char *str) ++{ ++ const char *const *p; ++ ++ for (p = conf_yes; *p; p++) { ++ if (!strcasecmp(*p, str)) ++ return true; ++ } ++ ++ return false; ++} +diff --git a/util/ipa_krb5.h b/util/ipa_krb5.h +index 7d2ebae98..d0280940a 100644 +--- a/util/ipa_krb5.h ++++ b/util/ipa_krb5.h +@@ -174,3 +174,7 @@ static inline bool + krb5_ts_after(krb5_timestamp a, krb5_timestamp b) { + return (uint32_t)a > (uint32_t)b; + } ++ ++/* Implement boolean string parsing function from MIT krb5: ++ * src/lib/krb5/krb/libdef_parse.c:_krb5_conf_boolean() */ ++bool ipa_krb5_parse_bool(const char *str); +-- +2.45.1 + diff --git a/SOURCES/0029-Allow_the_admin_user_to_be_disabled_rhel#34756.patch b/SOURCES/0029-Allow_the_admin_user_to_be_disabled_rhel#34756.patch new file mode 100644 index 0000000..1b5c49f --- /dev/null +++ b/SOURCES/0029-Allow_the_admin_user_to_be_disabled_rhel#34756.patch @@ -0,0 +1,127 @@ +diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py +index 6f5e349..febc22f 100644 +--- a/ipaserver/plugins/user.py ++++ b/ipaserver/plugins/user.py +@@ -144,8 +144,7 @@ PROTECTED_USERS = ('admin',) + def check_protected_member(user, protected_group_name=u'admins'): + ''' + Ensure admin and the last enabled member of a protected group cannot +- be deleted or disabled by raising ProtectedEntryError or +- LastMemberError as appropriate. ++ be deleted. + ''' + + if user in PROTECTED_USERS: +@@ -155,6 +154,12 @@ def check_protected_member(user, protected_group_name=u'admins'): + reason=_("privileged user"), + ) + ++ ++def check_last_member(user, protected_group_name=u'admins'): ++ ''' ++ Ensure the last enabled member of a protected group cannot ++ be disabled. ++ ''' + # Get all users in the protected group + result = api.Command.user_find(in_group=protected_group_name) + +@@ -796,6 +801,7 @@ class user_del(baseuser_del): + # If the target entry is a Delete entry, skip the orphaning/removal + # of OTP tokens. + check_protected_member(keys[-1]) ++ check_last_member(keys[-1]) + + preserve = options.get('preserve', False) + +@@ -1128,7 +1134,7 @@ class user_disable(LDAPQuery): + def execute(self, *keys, **options): + ldap = self.obj.backend + +- check_protected_member(keys[-1]) ++ check_last_member(keys[-1]) + + dn, _oc = self.obj.get_either_dn(*keys, **options) + ldap.deactivate_entry(dn) +diff --git a/ipatests/test_integration/test_commands.py b/ipatests/test_integration/test_commands.py +index c0cb4d0..c2a55b8 100644 +--- a/ipatests/test_integration/test_commands.py ++++ b/ipatests/test_integration/test_commands.py +@@ -1530,6 +1530,30 @@ class TestIPACommand(IntegrationTest): + + assert 'Discovered server %s' % self.master.hostname in result + ++ def test_delete_last_enabled_admin(self): ++ """ ++ The admin user may be disabled. Don't allow all other ++ members of admins to be removed if the admin user is ++ disabled which would leave the install with no ++ usable admins users ++ """ ++ user = 'adminuser2' ++ passwd = 'Secret123' ++ tasks.create_active_user(self.master, user, passwd) ++ tasks.kinit_admin(self.master) ++ self.master.run_command(['ipa', 'group-add-member', 'admins', ++ '--users', user]) ++ tasks.kinit_user(self.master, user, passwd) ++ self.master.run_command(['ipa', 'user-disable', 'admin']) ++ result = self.master.run_command( ++ ['ipa', 'user-del', user], ++ raiseonerr=False ++ ) ++ self.master.run_command(['ipa', 'user-enable', 'admin']) ++ tasks.kdestroy_all(self.master) ++ assert result.returncode == 1 ++ assert 'cannot be deleted or disabled' in result.stderr_text ++ + + class TestIPACommandWithoutReplica(IntegrationTest): + """ +diff --git a/ipatests/test_xmlrpc/test_user_plugin.py b/ipatests/test_xmlrpc/test_user_plugin.py +index 3c58845..68c6c48 100644 +--- a/ipatests/test_xmlrpc/test_user_plugin.py ++++ b/ipatests/test_xmlrpc/test_user_plugin.py +@@ -1045,8 +1045,8 @@ class TestAdmins(XMLRPC_test): + tracker = Tracker() + command = tracker.make_command('user_disable', admin1) + +- with raises_exact(errors.ProtectedEntryError(label=u'user', +- key=admin1, reason='privileged user')): ++ with raises_exact(errors.LastMemberError(label=u'group', ++ key=admin1, container=admin_group)): + command() + + def test_create_admin2(self, admin2): +@@ -1064,8 +1064,8 @@ class TestAdmins(XMLRPC_test): + admin2.disable() + tracker = Tracker() + +- with raises_exact(errors.ProtectedEntryError(label=u'user', +- key=admin1, reason='privileged user')): ++ with raises_exact(errors.LastMemberError(label=u'group', ++ key=admin1, container=admin_group)): + tracker.run_command('user_disable', admin1) + admin2.delete() + +diff --git a/ipatests/test_webui/test_user.py b/ipatests/test_webui/test_user.py +index a8a92d0..9083e50 100644 +--- a/ipatests/test_webui/test_user.py ++++ b/ipatests/test_webui/test_user.py +@@ -50,6 +50,8 @@ INV_FIRSTNAME = ("invalid 'first': Leading and trailing spaces are " + FIELD_REQ = 'Required field' + ERR_INCLUDE = 'may only include letters, numbers, _, -, . and $' + ERR_MISMATCH = 'Passwords must match' ++ERR_ADMIN_DISABLE = ('admin cannot be deleted or disabled because ' ++ 'it is the last member of group admins') + ERR_ADMIN_DEL = ('user admin cannot be deleted/modified: privileged user') + USR_EXIST = 'user with name "{}" already exists' + ENTRY_EXIST = 'This entry already exists' +@@ -546,7 +548,7 @@ class test_user(user_tasks): + self.select_record('admin') + self.facet_button_click('disable') + self.dialog_button_click('ok') +- self.assert_last_error_dialog(ERR_ADMIN_DEL, details=True) ++ self.assert_last_error_dialog(ERR_ADMIN_DISABLE, details=True) + self.dialog_button_click('ok') + self.assert_record('admin') + diff --git a/SOURCES/0030-ipa-otptoken-import-open-the-key-file-in-binary-mode_rhel#39616.patch b/SOURCES/0030-ipa-otptoken-import-open-the-key-file-in-binary-mode_rhel#39616.patch new file mode 100644 index 0000000..76a85d5 --- /dev/null +++ b/SOURCES/0030-ipa-otptoken-import-open-the-key-file-in-binary-mode_rhel#39616.patch @@ -0,0 +1,13 @@ +diff --git a/ipaserver/install/ipa_otptoken_import.py b/ipaserver/install/ipa_otptoken_import.py +index b3f9347..75e8680 100644 +--- a/ipaserver/install/ipa_otptoken_import.py ++++ b/ipaserver/install/ipa_otptoken_import.py +@@ -539,7 +539,7 @@ class OTPTokenImport(admintool.AdminTool): + + # Load the keyfile. + keyfile = self.safe_options.keyfile +- with open(keyfile) as f: ++ with open(keyfile, "rb") as f: + self.doc.setKey(f.read()) + + def run(self): diff --git a/SOURCES/0031-ipa-crlgen-manage-manage-the-cert-status-task-execution-time_rhel#30280.patch b/SOURCES/0031-ipa-crlgen-manage-manage-the-cert-status-task-execution-time_rhel#30280.patch new file mode 100644 index 0000000..addb493 --- /dev/null +++ b/SOURCES/0031-ipa-crlgen-manage-manage-the-cert-status-task-execution-time_rhel#30280.patch @@ -0,0 +1,114 @@ +diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py +index 38693c9..35cec89 100644 +--- a/ipaserver/install/cainstance.py ++++ b/ipaserver/install/cainstance.py +@@ -1327,6 +1327,8 @@ class CAInstance(DogtagInstance): + generation master: + - in CS.cfg ca.crl.MasterCRL.enableCRLCache=true + - in CS.cfg ca.crl.MasterCRL.enableCRLUpdates=true ++ - in CS.cfg ca.listenToCloneModifications=true ++ - in CS.cfg ca.certStatusUpdateInterval != 0 + - in /etc/httpd/conf.d/ipa-pki-proxy.conf the RewriteRule + ^/ipa/crl/MasterCRL.bin is disabled (commented or removed) + +@@ -1342,15 +1344,30 @@ class CAInstance(DogtagInstance): + updates = directivesetter.get_directive( + self.config, 'ca.crl.MasterCRL.enableCRLUpdates', '=') + enableCRLUpdates = updates.lower() == 'true' ++ listen = directivesetter.get_directive( ++ self.config, 'ca.listenToCloneModifications', '=') ++ enableToClone = listen.lower() == 'true' ++ updateinterval = directivesetter.get_directive( ++ self.config, 'ca.certStatusUpdateInterval', '=') + + # If the values are different, the config is inconsistent +- if enableCRLCache != enableCRLUpdates: ++ if not (enableCRLCache == enableCRLUpdates == enableToClone): + raise InconsistentCRLGenConfigException( + "Configuration is inconsistent, please check " +- "ca.crl.MasterCRL.enableCRLCache and " +- "ca.crl.MasterCRL.enableCRLUpdates in {} and " ++ "ca.crl.MasterCRL.enableCRLCache, " ++ "ca.crl.MasterCRL.enableCRLUpdates and " ++ "ca.listenToCloneModifications in {} and " + "run ipa-crlgen-manage [enable|disable] to repair".format( + self.config)) ++ # If they are the same then we are the CRL renewal master. Ensure ++ # the update task is configured. ++ if enableCRLCache and updateinterval == '0': ++ raise InconsistentCRLGenConfigException( ++ "Configuration is inconsistent, please check " ++ "ca.certStatusUpdateInterval in {}. It should " ++ "be either not present or not zero. Run " ++ "ipa-crlgen-manage [enable|disable] to repair".format( ++ self.config)) + except IOError: + raise RuntimeError( + "Unable to read {}".format(self.config)) +@@ -1407,6 +1424,11 @@ class CAInstance(DogtagInstance): + str_value = str(setup_crlgen).lower() + ds.set('ca.crl.MasterCRL.enableCRLCache', str_value) + ds.set('ca.crl.MasterCRL.enableCRLUpdates', str_value) ++ ds.set('ca.listenToCloneModifications', str_value) ++ if setup_crlgen: ++ ds.set('ca.certStatusUpdateInterval', None) ++ else: ++ ds.set('ca.certStatusUpdateInterval', '0') + + # Start pki-tomcat + logger.info("Starting %s", self.service_name) +diff --git a/ipatests/test_integration/test_crlgen_manage.py b/ipatests/test_integration/test_crlgen_manage.py +index 2a733bd..c6f41eb 100644 +--- a/ipatests/test_integration/test_crlgen_manage.py ++++ b/ipatests/test_integration/test_crlgen_manage.py +@@ -61,6 +61,16 @@ def check_crlgen_status(host, rc=0, msg=None, enabled=True, check_crl=False): + ext.value.crl_number) + assert number_msg in result.stdout_text + ++ try: ++ value = get_CS_cfg_value(host, 'ca.certStatusUpdateInterval') ++ except IOError: ++ return ++ ++ if enabled: ++ assert value is None ++ else: ++ assert value == '0' ++ + + def check_crlgen_enable(host, rc=0, msg=None, check_crl=False): + """Check ipa-crlgen-manage enable command +@@ -125,6 +135,23 @@ def break_crlgen_with_CS_cfg(host): + check_crlgen_status(host, rc=1, msg="Configuration is inconsistent") + + ++def get_CS_cfg_value(host, directive): ++ """Retrieve and return the a directive from the CA CS.cfg ++ ++ This returns None if the directives is not found. ++ """ ++ content = host.get_file_contents(paths.CA_CS_CFG_PATH, ++ encoding='utf-8') ++ value = None ++ for line in content.split('\n'): ++ l = line.lower() ++ ++ if l.startswith(directive.lower()): ++ value = line.split('=', 1)[1] ++ ++ return value ++ ++ + class TestCRLGenManage(IntegrationTest): + """Tests the ipa-crlgen-manage command. + +@@ -196,6 +223,9 @@ class TestCRLGenManage(IntegrationTest): + + Install a CA clone and enable CRLgen""" + tasks.install_ca(self.replicas[0]) ++ value = get_CS_cfg_value(self.replicas[0], ++ 'ca.certStatusUpdateInterval') ++ assert value == '0' + check_crlgen_enable( + self.replicas[0], rc=0, + msg="make sure to have only a single CRL generation master", diff --git a/SOURCES/0032-idrange-add-add-a-warning-because-389ds-restart-is-required_rhel#28996.patch b/SOURCES/0032-idrange-add-add-a-warning-because-389ds-restart-is-required_rhel#28996.patch new file mode 100644 index 0000000..50a891b --- /dev/null +++ b/SOURCES/0032-idrange-add-add-a-warning-because-389ds-restart-is-required_rhel#28996.patch @@ -0,0 +1,337 @@ +diff --git a/ipaserver/plugins/idrange.py b/ipaserver/plugins/idrange.py +index d5b184f..b38ea73 100644 +--- a/ipaserver/plugins/idrange.py ++++ b/ipaserver/plugins/idrange.py +@@ -549,6 +549,12 @@ class idrange_add(LDAPCreate): + self.obj.handle_ipabaserid(entry_attrs, options) + self.obj.handle_iparangetype(entry_attrs, options, + keep_objectclass=True) ++ self.add_message( ++ messages.ServiceRestartRequired( ++ service=services.knownservices.dirsrv.service_instance(""), ++ server=_('') ++ ) ++ ) + return dn + + +diff --git a/ipatests/test_xmlrpc/test_range_plugin.py b/ipatests/test_xmlrpc/test_range_plugin.py +index f912e04..e3f4c23 100644 +--- a/ipatests/test_xmlrpc/test_range_plugin.py ++++ b/ipatests/test_xmlrpc/test_range_plugin.py +@@ -372,6 +372,8 @@ IPA_LOCAL_RANGE_MOD_ERR = ( + "domain. Run `ipa help idrange` for more information" + ) + ++dirsrv_instance = services.knownservices.dirsrv.service_instance("") ++ + + @pytest.mark.tier1 + class test_range(Declarative): +@@ -464,6 +466,11 @@ class test_range(Declarative): + ), + value=testrange1, + summary=u'Added ID range "%s"' % (testrange1), ++ messages=( ++ messages.ServiceRestartRequired( ++ service=dirsrv_instance, ++ server='').to_dict(), ++ ), + ), + ), + +@@ -633,6 +640,11 @@ class test_range(Declarative): + ), + value=testrange2, + summary=u'Added ID range "%s"' % (testrange2), ++ messages=( ++ messages.ServiceRestartRequired( ++ service=dirsrv_instance, ++ server='').to_dict(), ++ ), + ), + ), + +@@ -792,6 +804,11 @@ class test_range(Declarative): + ), + value=unicode(domain7range1), + summary=u'Added ID range "%s"' % (domain7range1), ++ messages=( ++ messages.ServiceRestartRequired( ++ service=dirsrv_instance, ++ server='').to_dict(), ++ ), + ), + ), + +@@ -1079,6 +1096,11 @@ class test_range(Declarative): + ), + value=testrange9, + summary=u'Added ID range "%s"' % (testrange9), ++ messages=( ++ messages.ServiceRestartRequired( ++ service=dirsrv_instance, ++ server='').to_dict(), ++ ), + ), + ), + +diff --git a/ipaserver/plugins/idrange.py b/ipaserver/plugins/idrange.py +index b38ea73..b12e1b8 100644 +--- a/ipaserver/plugins/idrange.py ++++ b/ipaserver/plugins/idrange.py +@@ -549,12 +549,15 @@ class idrange_add(LDAPCreate): + self.obj.handle_ipabaserid(entry_attrs, options) + self.obj.handle_iparangetype(entry_attrs, options, + keep_objectclass=True) +- self.add_message( +- messages.ServiceRestartRequired( +- service=services.knownservices.dirsrv.service_instance(""), +- server=_('') ++ ++ if entry_attrs.single_value.get('iparangetype') in ( ++ 'ipa-local', self.obj.range_types.get('ipa-local', None)): ++ self.add_message( ++ messages.ServiceRestartRequired( ++ service=services.knownservices.dirsrv.service_instance(""), ++ server=_('') ++ ) + ) +- ) + return dn + + +@@ -568,7 +571,8 @@ class idrange_del(LDAPDelete): + try: + old_attrs = ldap.get_entry(dn, ['ipabaseid', + 'ipaidrangesize', +- 'ipanttrusteddomainsid']) ++ 'ipanttrusteddomainsid', ++ 'iparangetype']) + except errors.NotFound: + raise self.obj.handle_not_found(*keys) + +@@ -602,6 +606,20 @@ class idrange_del(LDAPDelete): + key=keys[0], + dependent=trust_domains[0].dn[0].value) + ++ self.add_message( ++ messages.ServiceRestartRequired( ++ service=services.knownservices['sssd'].systemd_name, ++ server=_('') ++ ) ++ ) ++ ++ if old_attrs.single_value.get('iparangetype') == 'ipa-local': ++ self.add_message( ++ messages.ServiceRestartRequired( ++ service=services.knownservices.dirsrv.service_instance(""), ++ server=_('') ++ ) ++ ) + + return dn + +@@ -804,10 +822,20 @@ class idrange_mod(LDAPUpdate): + assert isinstance(dn, DN) + self.obj.handle_ipabaserid(entry_attrs, options) + self.obj.handle_iparangetype(entry_attrs, options) ++ ++ if entry_attrs.single_value.get('iparangetype') in ( ++ 'ipa-local', self.obj.range_types.get('ipa-local', None)): ++ self.add_message( ++ messages.ServiceRestartRequired( ++ service=services.knownservices.dirsrv.service_instance(""), ++ server=_('') ++ ) ++ ) ++ + self.add_message( + messages.ServiceRestartRequired( + service=services.knownservices['sssd'].systemd_name, +- server=keys[0] ++ server=_('') + ) + ) + return dn +diff --git a/ipatests/test_xmlrpc/test_range_plugin.py b/ipatests/test_xmlrpc/test_range_plugin.py +index e3f4c23..531fe4a 100644 +--- a/ipatests/test_xmlrpc/test_range_plugin.py ++++ b/ipatests/test_xmlrpc/test_range_plugin.py +@@ -26,7 +26,8 @@ import six + from ipalib import api, errors, messages + from ipalib import constants + from ipaplatform import services +-from ipatests.test_xmlrpc.xmlrpc_test import Declarative, fuzzy_uuid ++from ipatests.test_xmlrpc.xmlrpc_test import ( ++ Declarative, fuzzy_uuid, Fuzzy, fuzzy_sequence_of) + from ipatests.test_xmlrpc import objectclasses + from ipatests.util import MockLDAP + from ipapython.dn import DN +@@ -374,6 +375,8 @@ IPA_LOCAL_RANGE_MOD_ERR = ( + + dirsrv_instance = services.knownservices.dirsrv.service_instance("") + ++fuzzy_restart_messages = fuzzy_sequence_of(Fuzzy(type=dict)) ++ + + @pytest.mark.tier1 + class test_range(Declarative): +@@ -610,7 +613,8 @@ class test_range(Declarative): + desc='Delete ID range %r' % testrange1, + command=('idrange_del', [testrange1], {}), + expected=dict( +- result=dict(failed=[]), ++ result=dict(failed=[], ++ messages=fuzzy_restart_messages), + value=[testrange1], + summary=u'Deleted ID range "%s"' % testrange1, + ), +@@ -714,7 +718,8 @@ class test_range(Declarative): + desc='Delete ID range %r' % testrange2, + command=('idrange_del', [testrange2], {}), + expected=dict( +- result=dict(failed=[]), ++ result=dict(failed=[], ++ messages=fuzzy_restart_messages), + value=[testrange2], + summary=u'Deleted ID range "%s"' % testrange2, + ), +diff --git a/ipatests/test_xmlrpc/test_range_plugin.py b/ipatests/test_xmlrpc/test_range_plugin.py +index 531fe4a..3646952 100644 +--- a/ipatests/test_xmlrpc/test_range_plugin.py ++++ b/ipatests/test_xmlrpc/test_range_plugin.py +@@ -613,8 +613,8 @@ class test_range(Declarative): + desc='Delete ID range %r' % testrange1, + command=('idrange_del', [testrange1], {}), + expected=dict( +- result=dict(failed=[], +- messages=fuzzy_restart_messages), ++ result=dict(failed=[]), ++ messages=fuzzy_restart_messages, + value=[testrange1], + summary=u'Deleted ID range "%s"' % testrange1, + ), +@@ -718,8 +718,8 @@ class test_range(Declarative): + desc='Delete ID range %r' % testrange2, + command=('idrange_del', [testrange2], {}), + expected=dict( +- result=dict(failed=[], +- messages=fuzzy_restart_messages), ++ result=dict(failed=[]), ++ messages=fuzzy_restart_messages, + value=[testrange2], + summary=u'Deleted ID range "%s"' % testrange2, + ), +@@ -809,11 +809,6 @@ class test_range(Declarative): + ), + value=unicode(domain7range1), + summary=u'Added ID range "%s"' % (domain7range1), +- messages=( +- messages.ServiceRestartRequired( +- service=dirsrv_instance, +- server='').to_dict(), +- ), + ), + ), + +@@ -836,6 +831,7 @@ class test_range(Declarative): + result=dict(failed=[]), + value=[domain1range1], + summary=u'Deleted ID range "%s"' % domain1range1, ++ messages=fuzzy_restart_messages, + ), + ), + +@@ -862,12 +858,7 @@ class test_range(Declarative): + command=('idrange_mod', [domain3range2], + dict(ipabaseid=domain3range1_base_id)), + expected=dict( +- messages=( +- messages.ServiceRestartRequired( +- service=services.knownservices['sssd'].systemd_name, +- server=domain3range2 +- ).to_dict(), +- ), ++ messages=fuzzy_restart_messages, + result=dict( + cn=[domain3range2], + ipabaseid=[unicode(domain3range1_base_id)], +@@ -933,12 +924,7 @@ class test_range(Declarative): + command=('idrange_mod', [domain2range1], + dict(ipabaserid=domain5range1_base_rid)), + expected=dict( +- messages=( +- messages.ServiceRestartRequired( +- service=services.knownservices['sssd'].systemd_name, +- server=domain2range1 +- ).to_dict(), +- ), ++ messages=fuzzy_restart_messages, + result=dict( + cn=[domain2range1], + ipabaseid=[unicode(domain2range1_base_id)], +@@ -973,12 +959,7 @@ class test_range(Declarative): + command=('idrange_mod', [domain2range1], + dict(ipaautoprivategroups='true')), + expected=dict( +- messages=( +- messages.ServiceRestartRequired( +- service=services.knownservices['sssd'].systemd_name, +- server=domain2range1 +- ).to_dict(), +- ), ++ messages=fuzzy_restart_messages, + result=dict( + cn=[domain2range1], + ipabaseid=[unicode(domain2range1_base_id)], +@@ -1000,12 +981,7 @@ class test_range(Declarative): + command=('idrange_mod', [domain2range1], + dict(ipaautoprivategroups='false')), + expected=dict( +- messages=( +- messages.ServiceRestartRequired( +- service=services.knownservices['sssd'].systemd_name, +- server=domain2range1 +- ).to_dict(), +- ), ++ messages=fuzzy_restart_messages, + result=dict( + cn=[domain2range1], + ipabaseid=[unicode(domain2range1_base_id)], +@@ -1027,12 +1003,7 @@ class test_range(Declarative): + command=('idrange_mod', [domain2range1], + dict(ipaautoprivategroups='hybrid')), + expected=dict( +- messages=( +- messages.ServiceRestartRequired( +- service=services.knownservices['sssd'].systemd_name, +- server=domain2range1 +- ).to_dict(), +- ), ++ messages=fuzzy_restart_messages, + result=dict( + cn=[domain2range1], + ipabaseid=[unicode(domain2range1_base_id)], +@@ -1054,12 +1025,7 @@ class test_range(Declarative): + command=('idrange_mod', [domain2range1], + dict(ipaautoprivategroups='')), + expected=dict( +- messages=( +- messages.ServiceRestartRequired( +- service=services.knownservices['sssd'].systemd_name, +- server=domain2range1 +- ).to_dict(), +- ), ++ messages=fuzzy_restart_messages, + result=dict( + cn=[domain2range1], + ipabaseid=[unicode(domain2range1_base_id)], +@@ -1116,6 +1082,7 @@ class test_range(Declarative): + result=dict(failed=[]), + value=[testrange9], + summary=u'Deleted ID range "%s"' % testrange9, ++ messages=fuzzy_restart_messages, + ), + ), + diff --git a/SOURCES/0033-PKINIT-certificate-fix-renewal-on-hidden-replica_rhel#4913.patch b/SOURCES/0033-PKINIT-certificate-fix-renewal-on-hidden-replica_rhel#4913.patch new file mode 100644 index 0000000..d272dda --- /dev/null +++ b/SOURCES/0033-PKINIT-certificate-fix-renewal-on-hidden-replica_rhel#4913.patch @@ -0,0 +1,58 @@ +diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py +index 619be83..9be1b67 100644 +--- a/ipaserver/plugins/cert.py ++++ b/ipaserver/plugins/cert.py +@@ -55,7 +55,7 @@ from ipapython.dn import DN + from ipapython.ipautil import datetime_from_utctimestamp + from ipaserver.plugins.service import normalize_principal, validate_realm + from ipaserver.masters import ( +- ENABLED_SERVICE, CONFIGURED_SERVICE, is_service_enabled ++ ENABLED_SERVICE, CONFIGURED_SERVICE, HIDDEN_SERVICE, is_service_enabled + ) + + try: +@@ -300,7 +300,7 @@ def caacl_check(principal, ca, profile_id): + def ca_kdc_check(api_instance, hostname): + master_dn = api_instance.Object.server.get_dn(unicode(hostname)) + kdc_dn = DN(('cn', 'KDC'), master_dn) +- wanted = {ENABLED_SERVICE, CONFIGURED_SERVICE} ++ wanted = {ENABLED_SERVICE, CONFIGURED_SERVICE, HIDDEN_SERVICE} + try: + kdc_entry = api_instance.Backend.ldap2.get_entry( + kdc_dn, ['ipaConfigString']) +diff --git a/ipatests/test_integration/test_replica_promotion.py b/ipatests/test_integration/test_replica_promotion.py +index b71f2d5..7ef44c5 100644 +--- a/ipatests/test_integration/test_replica_promotion.py ++++ b/ipatests/test_integration/test_replica_promotion.py +@@ -26,6 +26,7 @@ from ipalib.constants import ( + ) + from ipaplatform.paths import paths + from ipapython import certdb ++from ipatests.test_integration.test_cert import get_certmonger_fs_id + from ipatests.test_integration.test_dns_locations import ( + resolve_records_from_server, IPA_DEFAULT_MASTER_SRV_REC + ) +@@ -1241,6 +1242,23 @@ class TestHiddenReplicaPromotion(IntegrationTest): + 'ipa-crlgen-manage', 'status']) + assert "CRL generation: enabled" in result.stdout_text + ++ def test_hidden_replica_renew_pkinit_cert(self): ++ """Renew the PKINIT cert on a hidden replica. ++ ++ Test for https://pagure.io/freeipa/issue/9611 ++ """ ++ # Get Request ID ++ cmd = ['getcert', 'list', '-f', paths.KDC_CERT] ++ result = self.replicas[0].run_command(cmd) ++ req_id = get_certmonger_fs_id(result.stdout_text) ++ ++ self.replicas[0].run_command([ ++ 'getcert', 'resubmit', '-f', paths.KDC_CERT ++ ]) ++ tasks.wait_for_certmonger_status( ++ self.replicas[0], ('MONITORING'), req_id, timeout=600 ++ ) ++ + + class TestHiddenReplicaKRA(IntegrationTest): + """Test KRA & hidden replica features. diff --git a/SPECS/ipa.spec b/SPECS/ipa.spec index a7f2e9f..6a331f7 100644 --- a/SPECS/ipa.spec +++ b/SPECS/ipa.spec @@ -190,7 +190,7 @@ Name: %{package_name} Version: %{IPA_VERSION} -Release: 9%{?rc_version:.%rc_version}%{?dist} +Release: 12%{?rc_version:.%rc_version}%{?dist} Summary: The Identity, Policy and Audit system License: GPLv3+ @@ -235,6 +235,13 @@ Patch0023: 0023-rpcserver-validate-Kerberos-principal-name-before-running-k Patch0024: 0024-Vault-add-additional-fallback-to-RSA-OAEP-wrapping-algo_rhel#28259.patch Patch0025: 0025-dcerpc-invalidate-forest-trust-intfo-cache-when-filtering-out-realm-domains_rhel#28559.patch Patch0026: 0026-backport-test-fixes_rhel#29908.patch +Patch0027: 0027-kdb-fix-vulnerability-in-GCD-rules-handling.patch +Patch0028: 0028-kdb-apply-combinatorial-logic-for-ticket-flags.patch +Patch0029: 0029-Allow_the_admin_user_to_be_disabled_rhel#34756.patch +Patch0030: 0030-ipa-otptoken-import-open-the-key-file-in-binary-mode_rhel#39616.patch +Patch0031: 0031-ipa-crlgen-manage-manage-the-cert-status-task-execution-time_rhel#30280.patch +Patch0032: 0032-idrange-add-add-a-warning-because-389ds-restart-is-required_rhel#28996.patch +Patch0033: 0033-PKINIT-certificate-fix-renewal-on-hidden-replica_rhel#4913.patch %if 0%{?rhel} >= 8 Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch Patch1002: 1002-Revert-freeipa.spec-depend-on-bind-dnssec-utils.patch @@ -1750,6 +1757,28 @@ fi %endif %changelog +* Wed Jul 17 2024 Rafael Jeffman - 4.9.13-9 +- Allow the admin user to be disabled + Resolves: RHEL-34756 +- ipa-otptoken-import: open the key file in binary mode + Resolves: RHEL-39616 +- ipa-crlgen-manage: manage the cert status task execution time + Resolves: RHEL-30280 +- idrange-add: add a warning because 389ds restart is required + Resolves: RHEL-28996 +- PKINIT certificate: fix renewal on hidden replica + Resolves: RHEL-4913, RHEL-45908 + +* Wed Jun 12 2024 Julien Rische - 4.9.13-11 +- Add missing part of backported CVE-2024-3183 fix + Resolves: RHEL-29927 + +* Tue Apr 30 2024 Julien Rische - 4.9.13-10 +- kdb: apply combinatorial logic for ticket flags (CVE-2024-3183) + Resolves: RHEL-29927 +- kdb: fix vulnerability in GCD rules handling (CVE-2024-2698) + Resolves: RHEL-29692 + * Fri Apr 12 2024 Rafael Jeffman - 9.4.13-9 - dcerpc: invalidate forest trust intfo cache when filtering out realm domains Resolves: RHEL-28559