/* * MIT Kerberos KDC database backend for FreeIPA * * Authors: Simo Sorce * * Copyright (C) 2011 Simo Sorce, Red Hat * see file 'COPYING' for use and warranty information * * This program is free software you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ipa_kdb.h" #include "ipa_krb5.h" #include /* * During TGS request search by ipaKrbPrincipalName (case-insensitive) * and krbPrincipalName (case-sensitive) */ #define PRINC_TGS_SEARCH_FILTER_EXTRA "(&(|(objectclass=krbprincipalaux)" \ "(objectclass=krbprincipal)" \ "(objectclass=ipakrbprincipal))" \ "(|(ipakrbprincipalalias=%s)" \ "(krbprincipalname:caseIgnoreIA5Match:=%s))" \ "%s)" #define PRINC_SEARCH_FILTER_EXTRA "(&(|(objectclass=krbprincipalaux)" \ "(objectclass=krbprincipal))" \ "(krbprincipalname=%s)" \ "%s)" #define PRINC_TGS_SEARCH_FILTER_WILD_EXTRA "(&(|(objectclass=krbprincipalaux)" \ "(objectclass=krbprincipal)" \ "(objectclass=ipakrbprincipal))" \ "(|(ipakrbprincipalalias=*)" \ "(krbprincipalname=*))" \ "%s)" static char *std_principal_attrs[] = { "krbPrincipalName", "krbCanonicalName", "krbUPEnabled", "krbPrincipalKey", "krbTicketPolicyReference", "krbPrincipalExpiration", "krbPasswordExpiration", "krbPwdPolicyReference", "krbPrincipalType", "krbPwdHistory", "krbLastPwdChange", "krbPrincipalAliases", "krbLastSuccessfulAuth", "krbLastFailedAuth", "krbLoginFailedCount", "krbPrincipalAuthInd", "krbExtraData", "krbLastAdminUnlock", "krbObjectReferences", "krbTicketFlags", "krbMaxTicketLife", "krbMaxRenewableAge", /* IPA SPECIFIC ATTRIBUTES */ "uid", "nsaccountlock", "passwordHistory", IPA_KRB_AUTHZ_DATA_ATTR, IPA_USER_AUTH_TYPE, "ipatokenRadiusConfigLink", "krbAuthIndMaxTicketLife", "krbAuthIndMaxRenewableAge", "ipaNTSecurityIdentifier", "ipaUniqueID", "objectClass", NULL }; static char *std_tktpolicy_attrs[] = { "krbmaxticketlife", "krbmaxrenewableage", "krbticketflags", "krbauthindmaxticketlife", "krbauthindmaxrenewableage", NULL }; #define TKTFLAGS_BIT 0x01 #define MAXTKTLIFE_BIT 0x02 #define MAXRENEWABLEAGE_BIT 0x04 static char *std_principal_obj_classes[] = { "krbprincipal", "krbprincipalaux", "krbTicketPolicyAux", NULL }; #define STD_PRINCIPAL_OBJ_CLASSES_SIZE (sizeof(std_principal_obj_classes) / sizeof(char *) - 1) #define DEFAULT_TL_DATA_CONTENT "\x00\x00\x00\x00principal@UNINITIALIZED" static int ipadb_ldap_attr_to_tl_data(LDAP *lcontext, LDAPMessage *le, char *attrname, krb5_tl_data **result, int *num) { struct berval **vals; krb5_tl_data *prev, *next; krb5_int16 be_type; int i; int ret = ENOENT; *result = NULL; prev = NULL; next = NULL; vals = ldap_get_values_len(lcontext, le, attrname); if (vals) { for (i = 0; vals[i]; i++) { next = calloc(1, sizeof(krb5_tl_data)); if (!next) { ret = ENOMEM; goto done; } /* fill tl_data struct with the data */ memcpy(&be_type, vals[i]->bv_val, 2); next->tl_data_type = ntohs(be_type); next->tl_data_length = vals[i]->bv_len - 2; next->tl_data_contents = malloc(next->tl_data_length); if (!next->tl_data_contents) { ret = ENOMEM; goto done; } memcpy(next->tl_data_contents, vals[i]->bv_val + 2, next->tl_data_length); if (prev) { prev->tl_data_next = next; } else { *result = next; } prev = next; } *num = i; ret = 0; ldap_value_free_len(vals); } done: if (ret) { free(next); if (*result) { prev = *result; while (prev) { next = prev->tl_data_next; free(prev); prev = next; } } *result = NULL; *num = 0; } return ret; } static krb5_error_code ipadb_set_tl_data(krb5_db_entry *entry, krb5_int16 type, krb5_ui_2 length, const krb5_octet *data) { krb5_error_code kerr; krb5_tl_data *new_td = NULL; krb5_tl_data *td; for (td = entry->tl_data; td; td = td->tl_data_next) { if (td->tl_data_type == type) { break; } } if (!td) { /* an existing entry was not found, make new */ new_td = malloc(sizeof(krb5_tl_data)); if (!new_td) { kerr = ENOMEM; goto done; } td = new_td; td->tl_data_next = entry->tl_data; td->tl_data_type = type; entry->tl_data = td; entry->n_tl_data++; } td->tl_data_length = length; td->tl_data_contents = malloc(td->tl_data_length); if (!td->tl_data_contents) { kerr = ENOMEM; goto done; } memcpy(td->tl_data_contents, data, td->tl_data_length); new_td = NULL; kerr = 0; done: free(new_td); return kerr; } static int ipadb_ldap_attr_to_key_data(LDAP *lcontext, LDAPMessage *le, char *attrname, krb5_key_data **result, int *num, krb5_kvno *res_mkvno) { struct berval **vals; int mkvno; int ret; vals = ldap_get_values_len(lcontext, le, attrname); if (!vals) { return ENOENT; } ret = ber_decode_krb5_key_data(vals[0], &mkvno, num, result); ldap_value_free_len(vals); if (ret == 0) { *res_mkvno = mkvno; } return ret; } static void ipadb_validate_otp(struct ipadb_context *ipactx, LDAPMessage *lentry, enum ipadb_user_auth *ua) { static const char *attrs[] = { "dn", NULL }; static const char *dttmpl = "%Y%m%d%H%M%SZ"; static const char *ftmpl = "(&" "(objectClass=ipaToken)(ipatokenOwner=%s)" "(|(ipatokenNotBefore<=%s)(!(ipatokenNotBefore=*)))" "(|(ipatokenNotAfter>=%s)(!(ipatokenNotAfter=*)))" "(|(ipatokenDisabled=FALSE)(!(ipatokenDisabled=*)))" ")"; krb5_error_code kerr = 0; LDAPMessage *res = NULL; char datetime[16] = {}; char *filter = NULL; struct tm tm = {}; char *dn = NULL; time_t now = 0; int count = 0; if (!(*ua & IPADB_USER_AUTH_OTP)) return; /* Get the current time. */ if (time(&now) == (time_t) -1) return; if (gmtime_r(&now, &tm) == NULL) return; /* Make the current time string. */ if (strftime(datetime, sizeof(datetime), dttmpl, &tm) == 0) return; /* Make the filter. */ dn = ldap_get_dn(ipactx->lcontext, lentry); if (dn == NULL) return; count = asprintf(&filter, ftmpl, dn, datetime, datetime); ldap_memfree(dn); if (count < 0) return; /* Fetch the active token list. */ kerr = ipadb_simple_search(ipactx, ipactx->base, LDAP_SCOPE_SUBTREE, filter, (char**) attrs, &res); free(filter); if (kerr != 0 || res == NULL) return; /* Count the number of active tokens. */ count = ldap_count_entries(ipactx->lcontext, res); ldap_msgfree(res); /* If the user is configured for OTP, but has no active tokens, remove * OTP from the list since the user obviously can't log in this way. */ if (count == 0) *ua &= ~IPADB_USER_AUTH_OTP; } static void ipadb_validate_radius(struct ipadb_context *ipactx, LDAPMessage *lentry, enum ipadb_user_auth *ua) { struct berval **vals; if (!(*ua & IPADB_USER_AUTH_RADIUS)) return; /* Ensure that the user has a link to a RADIUS config. */ vals = ldap_get_values_len(ipactx->lcontext, lentry, "ipatokenRadiusConfigLink"); if (vals == NULL || vals[0] == NULL) *ua &= ~IPADB_USER_AUTH_RADIUS; else *ua = IPADB_USER_AUTH_RADIUS; if (vals != NULL) ldap_value_free_len(vals); } static enum ipadb_user_auth ipadb_get_user_auth(struct ipadb_context *ipactx, LDAPMessage *lentry) { enum ipadb_user_auth gua = IPADB_USER_AUTH_NONE; enum ipadb_user_auth ua = IPADB_USER_AUTH_NONE; const struct ipadb_global_config *gcfg = NULL; /* Get the global user_auth settings. */ gcfg = ipadb_get_global_config(ipactx); if (gcfg != NULL) gua = gcfg->user_auth; /* lcontext == NULL means ipadb_get_global_config() failed to load * global config and cleared the ipactx */ if (ipactx->lcontext == NULL) return IPADB_USER_AUTH_NONE; /* Get the user's user_auth settings if not disabled. */ if ((gua & IPADB_USER_AUTH_DISABLED) == 0) ipadb_parse_user_auth(ipactx->lcontext, lentry, &ua); /* Filter out the disabled flag. */ gua &= ~IPADB_USER_AUTH_DISABLED; ua &= ~IPADB_USER_AUTH_DISABLED; /* Determine which user_auth policy is active: user or global. */ if (ua == IPADB_USER_AUTH_NONE) ua = gua; /* Perform flag validation. */ ipadb_validate_otp(ipactx, lentry, &ua); ipadb_validate_radius(ipactx, lentry, &ua); return ua; } #define OSA_ADB_PRINC_VERSION_1 0x12345C01 /* The XDR encoding of OSA_PRINC_ENC is as follows: version: int (signed 32 bit integer) name: nullstring (null terminated variable string) aux_attributes: long (signed 32 bit integer) old_key_next: u_int (unsigned 32 bit integer) adm_hist_kvno: u_char (unisgned char) old_keys: array of keys, we do not care so alway u_int of 0 */ #define OSA_PRINC_ENC_BASE_SIZE 20 static krb5_error_code ipadb_policydn_to_kdam_tl_data(const char *policydn, krb5_db_entry *entry) { krb5_error_code kerr; uint32_t tmp; char *policy_name = NULL; char *p; uint8_t *buf = NULL; size_t buf_len; int slen; int plen; int cur; /* policy objects must use cn as the RDN */ if (strncmp(policydn, "cn=", 3) != 0) { return KRB5_KDB_INTERNAL_ERROR; } /* Should we try to consider the case where a ',' is part of the polict * name ? */ policy_name = strdup(&policydn[3]); if (!policy_name) { kerr = ENOMEM; goto done; } p = strchr(policy_name, ','); if (p) *p = '\0'; /* Now we open code a basic KRB5_TL_KADM_DATA which is a XDR encoded * structure in MIT code */ slen = strlen(policy_name) + 1; /* A xdr varstring is preceeded by a 32bit len field and is always 32 * bit aligned */ plen = slen + 4; plen = (((plen + 3) / 4) * 4); buf_len = OSA_PRINC_ENC_BASE_SIZE + plen; buf = calloc(1, buf_len); if (!buf) { kerr = ENOMEM; goto done; } /* version */ cur = 0; tmp = htobe32(OSA_ADB_PRINC_VERSION_1); memcpy(&buf[cur], &tmp, 4); cur += 4; /* name */ tmp = htobe32(slen); memcpy(&buf[cur], &tmp, 4); memcpy(&buf[cur + 4], policy_name, slen); cur += plen; /* All the other fileds are left empty */ kerr = ipadb_set_tl_data(entry, KRB5_TL_KADM_DATA, buf_len, buf); done: free(policy_name); free(buf); return kerr; } static void strv_free(char **strv) { int i; if (strv == NULL) { return; } for (i = 0; strv[i] != NULL; i++) { free(strv[i]); } free(strv); } static krb5_error_code ipadb_get_ldap_auth_ind(krb5_context kcontext, LDAP *lcontext, LDAPMessage *lentry, krb5_db_entry *entry) { krb5_error_code ret = 0; char **authinds = NULL; char *aistr = NULL; char *ap = NULL; size_t len = 0; size_t l = 0; int count = 0; int i = 0; ret = ipadb_ldap_attr_to_strlist(lcontext, lentry, "krbPrincipalAuthInd", &authinds); switch (ret) { case 0: break; case ENOENT: return 0; default: return ret; } for (count = 0; authinds != NULL && authinds[count] != NULL; count++) { len += strlen(authinds[count]) + 1; } if (len == 0) { strv_free(authinds); return 0; } aistr = malloc(len); if (aistr == NULL) { ret = errno; goto cleanup; } /* Create a space-separated string of authinds. */ ap = aistr; l = len; for (i = 0; i < count; i++) { ret = snprintf(ap, l, "%s ", authinds[i]); if (ret <= 0 || ret > (int) l) { ret = ENOMEM; goto cleanup; } ap += ret; l -= ret; } aistr[len - 1] = '\0'; ret = krb5_dbe_set_string(kcontext, entry, "require_auth", aistr); cleanup: strv_free(authinds); free(aistr); return ret; } static void ipadb_parse_authind_policies(krb5_context kcontext, LDAP *lcontext, LDAPMessage *lentry, krb5_db_entry *entry, enum ipadb_user_auth ua) { int result; int ret; struct ipadb_e_data *ied; const struct { char *attribute; enum ipadb_user_auth flag; enum ipadb_user_auth_idx idx; } life_authind_map[] = { {"krbAuthIndMaxTicketLife;otp", IPADB_USER_AUTH_OTP, IPADB_USER_AUTH_IDX_OTP}, {"krbAuthIndMaxTicketLife;radius", IPADB_USER_AUTH_RADIUS, IPADB_USER_AUTH_IDX_RADIUS}, {"krbAuthIndMaxTicketLife;pkinit", IPADB_USER_AUTH_PKINIT, IPADB_USER_AUTH_IDX_PKINIT}, {"krbAuthIndMaxTicketLife;hardened", IPADB_USER_AUTH_HARDENED, IPADB_USER_AUTH_IDX_HARDENED}, {NULL, IPADB_USER_AUTH_NONE, IPADB_USER_AUTH_IDX_MAX}, }, age_authind_map[] = { {"krbAuthIndMaxRenewableAge;otp", IPADB_USER_AUTH_OTP, IPADB_USER_AUTH_IDX_OTP}, {"krbAuthIndMaxRenewableAge;radius", IPADB_USER_AUTH_RADIUS, IPADB_USER_AUTH_IDX_RADIUS}, {"krbAuthIndMaxRenewableAge;pkinit", IPADB_USER_AUTH_PKINIT, IPADB_USER_AUTH_IDX_PKINIT}, {"krbAuthIndMaxRenewableAge;hardened", IPADB_USER_AUTH_HARDENED, IPADB_USER_AUTH_IDX_HARDENED}, {NULL, IPADB_USER_AUTH_NONE, IPADB_USER_AUTH_IDX_MAX}, }; ied = (struct ipadb_e_data *)entry->e_data; if (ied == NULL) { return; } for (size_t i = 0; life_authind_map[i].attribute != NULL; i++) { if (ua & life_authind_map[i].flag) { ret = ipadb_ldap_attr_to_int(lcontext, lentry, life_authind_map[i].attribute, &result); if (ret == 0) { ied->pol_limits[life_authind_map[i].idx].max_life = result; } ret = ipadb_ldap_attr_to_int(lcontext, lentry, age_authind_map[i].attribute, &result); if (ret == 0) { ied->pol_limits[age_authind_map[i].idx].max_renewable_life = result; } } } } static krb5_error_code ipadb_parse_ldap_entry(krb5_context kcontext, char *principal, LDAPMessage *lentry, krb5_db_entry **kentry, uint32_t *polmask) { const krb5_octet rad_string[] = "otp\0[{\"indicators\": [\"radius\"]}]"; const krb5_octet otp_string[] = "otp\0[{\"indicators\": [\"otp\"]}]"; struct ipadb_context *ipactx; enum ipadb_user_auth ua; LDAP *lcontext; krb5_db_entry *entry; struct ipadb_e_data *ied; krb5_error_code kerr; krb5_tl_data *res_tl_data; krb5_key_data *res_key_data; krb5_kvno mkvno = 0; char **restrlist; char *restring; char *uidstring; char **authz_data_list; char *princ_sid; krb5_timestamp restime; bool resbool; int result; int ret; *polmask = 0; entry = calloc(1, sizeof(krb5_db_entry)); if (!entry) { return ENOMEM; } /* proceed to fill in attributes in the order they are defined in * krb5_db_entry in kdb.h */ ipactx = ipadb_get_context(kcontext); if (!ipactx) { free(entry); return KRB5_KDB_DBNOTINITED; } entry->magic = KRB5_KDB_MAGIC_NUMBER; entry->len = KRB5_KDB_V1_BASE_LENGTH; /* Get User Auth configuration. */ ua = ipadb_get_user_auth(ipactx, lentry); /* ipadb_get_user_auth() calls into ipadb_get_global_config() * and that might fail, causing lcontext to become NULL */ if (!ipactx->lcontext) { krb5_klog_syslog(LOG_INFO, "No LDAP connection in ipadb_parse_ldap_entry(); retrying...\n"); ret = ipadb_get_connection(ipactx); if (ret != 0) { krb5_klog_syslog(LOG_ERR, "No LDAP connection on retry in ipadb_parse_ldap_entry()!\n"); kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } } /* If any code below would result in invalidating ipactx->lcontext, * lcontext must be updated with the new ipactx->lcontext value. * We rely on the fact that none of LDAP-parsing helpers does it. */ lcontext = ipactx->lcontext; /* ignore mask for now */ ret = ipadb_ldap_attr_to_int(lcontext, lentry, "krbTicketFlags", &result); if (ret == 0) { entry->attributes = result; } else { *polmask |= TKTFLAGS_BIT; } ret = ipadb_ldap_attr_to_int(lcontext, lentry, "krbMaxTicketLife", &result); if (ret == 0) { entry->max_life = result; } else { *polmask |= MAXTKTLIFE_BIT; } ret = ipadb_ldap_attr_to_int(lcontext, lentry, "krbMaxRenewableAge", &result); if (ret == 0) { entry->max_renewable_life = result; } else { *polmask |= MAXRENEWABLEAGE_BIT; } ret = ipadb_ldap_attr_to_krb5_timestamp(lcontext, lentry, "krbPrincipalexpiration", &restime); switch (ret) { case 0: entry->expiration = restime; case ENOENT: break; default: kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } ret = ipadb_ldap_attr_to_krb5_timestamp(lcontext, lentry, "krbPasswordExpiration", &restime); switch (ret) { case 0: entry->pw_expiration = restime; /* If we are using only RADIUS, we don't know expiration. */ if (ua == IPADB_USER_AUTH_RADIUS) entry->pw_expiration = 0; case ENOENT: break; default: kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } ret = ipadb_ldap_attr_to_krb5_timestamp(lcontext, lentry, "krbLastSuccessfulAuth", &restime); switch (ret) { case 0: entry->last_success = restime; case ENOENT: break; default: kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } ret = ipadb_ldap_attr_to_krb5_timestamp(lcontext, lentry, "krbLastFailedAuth", &restime); switch (ret) { case 0: entry->last_failed = restime; case ENOENT: break; default: kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } ret = ipadb_ldap_attr_to_int(lcontext, lentry, "krbLoginFailedCount", &result); if (ret == 0) { entry->fail_auth_count = result; } /* TODO: e_length, e_data */ if (principal) { kerr = krb5_parse_name(kcontext, principal, &entry->princ); if (kerr != 0) { goto done; } } else { /* see if canonical name is available */ ret = ipadb_ldap_attr_to_str(lcontext, lentry, "krbCanonicalName", &restring); switch (ret) { case ENOENT: /* if not pick the first principal name in the entry */ ret = ipadb_ldap_attr_to_str(lcontext, lentry, "krbPrincipalName", &restring); if (ret != 0) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } case 0: break; default: kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } kerr = krb5_parse_name(kcontext, restring, &entry->princ); free(restring); if (kerr != 0) { goto done; } } ret = ipadb_ldap_attr_to_tl_data(lcontext, lentry, "krbExtraData", &res_tl_data, &result); switch (ret) { case 0: entry->tl_data = res_tl_data; entry->n_tl_data = result; break; case ENOENT: /* The kadmin utility expects always at least KRB5_TL_MOD_PRINC tl_data * to be available. So if krbExtraData is missing (may happen when a * user is created but no password has been set yet) then add a default * one. */ kerr = ipadb_set_tl_data(entry, KRB5_TL_MOD_PRINC, sizeof(DEFAULT_TL_DATA_CONTENT), (const krb5_octet *)DEFAULT_TL_DATA_CONTENT); break; default: kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } ret = ipadb_get_ldap_auth_ind(kcontext, lcontext, lentry, entry); if (ret) goto done; ret = ipadb_ldap_attr_to_key_data(lcontext, lentry, "krbPrincipalKey", &res_key_data, &result, &mkvno); switch (ret) { case 0: /* Only set a principal's key if password auth can be used. Otherwise * the KDC would add pre-authentication methods to the NEEDED_PREAUTH * reply for AS-REQs which indicate the password authentication is * available. This might confuse applications like e.g. SSSD which try * to determine suitable authentication methods and corresponding * prompts with the help of MIT Kerberos' responder interface which * acts on the returned pre-authentication methods. A typical example * is enforced OTP authentication where of course keys are available * for the first factor but password authentication should not be * advertised by the KDC. */ if (!(ua & IPADB_USER_AUTH_PASSWORD) && (ua != IPADB_USER_AUTH_NONE)) { /* This is the same behavior as ENOENT below. */ ipa_krb5_free_key_data(res_key_data, result); break; } entry->key_data = res_key_data; entry->n_key_data = result; if (mkvno) { krb5_int16 kvno16le = htole16((krb5_int16)mkvno); kerr = ipadb_set_tl_data(entry, KRB5_TL_MKVNO, sizeof(kvno16le), (krb5_octet *)&kvno16le); if (kerr) { goto done; } } case ENOENT: break; default: kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } ret = ipadb_ldap_attr_to_bool(lcontext, lentry, "nsAccountLock", &resbool); if ((ret == 0 && resbool == true) || (ret != 0 && ret != ENOENT)) { entry->attributes |= KRB5_KDB_DISALLOW_ALL_TIX; } ied = calloc(1, sizeof(struct ipadb_e_data)); if (!ied) { kerr = ENOMEM; goto done; } ied->magic = IPA_E_DATA_MAGIC; entry->e_data = (krb5_octet *)ied; ied->entry_dn = ldap_get_dn(lcontext, lentry); if (!ied->entry_dn) { kerr = ENOMEM; goto done; } /* mark this as an ipa_user if it has the posixaccount objectclass */ ret = ipadb_ldap_attr_has_value(lcontext, lentry, "objectClass", "posixAccount"); if (ret != 0 && ret != ENOENT) { kerr = ret; goto done; } if (ret == 0) { ied->ipa_user = true; ret = ipadb_ldap_attr_to_str(lcontext, lentry, "uid", &uidstring); if (ret != 0 && ret != ENOENT) { kerr = ret; goto done; } ied->user = uidstring; } /* check if it has the krbTicketPolicyAux objectclass */ ret = ipadb_ldap_attr_has_value(lcontext, lentry, "objectClass", "krbTicketPolicyAux"); if (ret != 0 && ret != ENOENT) { kerr = ret; goto done; } if (ret == 0) { ied->has_tktpolaux = true; } ret = ipadb_ldap_attr_to_str(lcontext, lentry, "krbPwdPolicyReference", &restring); switch (ret) { case ENOENT: /* use the default policy if ref. is not available */ ret = asprintf(&restring, "cn=global_policy,%s", ipactx->realm_base); if (ret == -1) { kerr = ENOMEM; goto done; } case 0: break; default: kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } ied->pw_policy_dn = restring; kerr = ipadb_policydn_to_kdam_tl_data(restring, entry); if (kerr) goto done; ret = ipadb_ldap_attr_to_strlist(lcontext, lentry, "passwordHistory", &restrlist); if (ret != 0 && ret != ENOENT) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } if (ret == 0) { ied->pw_history = restrlist; } ret = ipadb_ldap_attr_to_krb5_timestamp(lcontext, lentry, "krbLastPwdChange", &restime); if (ret == 0) { krb5_int32 time32le = htole32((krb5_int32)restime); kerr = ipadb_set_tl_data(entry, KRB5_TL_LAST_PWD_CHANGE, sizeof(time32le), (krb5_octet *)&time32le); if (kerr) { goto done; } ied->last_pwd_change = krb5_ts2tt(restime); } ret = ipadb_ldap_attr_to_krb5_timestamp(lcontext, lentry, "krbLastAdminUnlock", &restime); if (ret == 0) { krb5_int32 time32le = htole32((krb5_int32)restime); kerr = ipadb_set_tl_data(entry, KRB5_TL_LAST_ADMIN_UNLOCK, sizeof(time32le), (krb5_octet *)&time32le); if (kerr) { goto done; } ied->last_admin_unlock = krb5_ts2tt(restime); } ret = ipadb_ldap_attr_to_strlist(lcontext, lentry, IPA_KRB_AUTHZ_DATA_ATTR, &authz_data_list); if (ret != 0 && ret != ENOENT) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } if (ret == 0) { ied->authz_data = authz_data_list; } ied->user_auth = ua; /* If enabled, set the otp user string, enabling otp. */ if (ua & IPADB_USER_AUTH_OTP) { kerr = ipadb_set_tl_data(entry, KRB5_TL_STRING_ATTRS, sizeof(otp_string), otp_string); if (kerr) goto done; } else if (ua & IPADB_USER_AUTH_RADIUS) { kerr = ipadb_set_tl_data(entry, KRB5_TL_STRING_ATTRS, sizeof(rad_string), rad_string); if (kerr) goto done; } if (ua & ~IPADB_USER_AUTH_NONE) { ipadb_parse_authind_policies(kcontext, lcontext, lentry, entry, ua); } /* Add SID if it is associated with the principal account */ ied->has_sid = false; ied->sid = NULL; ret = ipadb_ldap_attr_to_str(ipactx->lcontext, lentry, "ipaNTSecurityIdentifier", &princ_sid); if (ret == 0 && princ_sid != NULL) { alloc_sid(&ied->sid); if (ied->sid == NULL) { kerr = KRB5_KDB_INTERNAL_ERROR; free(princ_sid); goto done; } ret = ipadb_string_to_sid(princ_sid, ied->sid); free(princ_sid); if (ret != 0) { kerr = ret; goto done; } ied->has_sid = true; } kerr = 0; done: if (kerr) { ipadb_free_principal(kcontext, entry); entry = NULL; } *kentry = entry; return kerr; } krb5_error_code ipadb_fetch_principals_with_extra_filter(struct ipadb_context *ipactx, unsigned int flags, const char *principal, const char *filter, LDAPMessage **result) { krb5_error_code kerr; char *src_filter = NULL, *esc_original_princ = NULL; int ret; int len = 0; if (!ipactx->lcontext) { ret = ipadb_get_connection(ipactx); if (ret != 0) { kerr = KRB5_KDB_SERVER_INTERNAL_ERR; goto done; } } /* Escape filter but do not touch '*' as this function accepts * wildcards in names. */ esc_original_princ = ipadb_filter_escape(principal, false); if (!esc_original_princ) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } len = strlen(esc_original_princ); /* Starting in DAL 8.0, aliases are always okay. */ #ifdef KRB5_KDB_FLAG_ALIAS_OK if (!(flags & KRB5_KDB_FLAG_ALIAS_OK)) { ret = asprintf(&src_filter, PRINC_SEARCH_FILTER_EXTRA, esc_original_princ, filter ? filter : ""); } else #endif { /* In case we've got a principal name as '*', we don't need to specify * the principal itself, use pre-defined filter for a wild-card search. */ if ((len == 1) && (esc_original_princ[0] == '*')) { ret = asprintf(&src_filter, PRINC_TGS_SEARCH_FILTER_WILD_EXTRA, filter ? filter : ""); } else { ret = asprintf(&src_filter, PRINC_TGS_SEARCH_FILTER_EXTRA, esc_original_princ, esc_original_princ, filter ? filter : ""); } } if (ret == -1) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } kerr = ipadb_simple_search(ipactx, ipactx->base, LDAP_SCOPE_SUBTREE, src_filter, std_principal_attrs, result); done: free(src_filter); free(esc_original_princ); return kerr; } static krb5_error_code ipadb_fetch_principals(struct ipadb_context *ipactx, unsigned int flags, char *principal, LDAPMessage **result) { return ipadb_fetch_principals_with_extra_filter(ipactx, flags, principal, NULL, result); } krb5_error_code ipadb_find_principal(krb5_context kcontext, unsigned int flags, LDAPMessage *res, char **principal, LDAPMessage **entry) { struct ipadb_context *ipactx; bool found = false; LDAPMessage *le = NULL; struct berval **vals = NULL; int result; krb5_error_code ret; size_t princ_len = 0; ipactx = ipadb_get_context(kcontext); if (!ipactx) { ret = KRB5_KDB_DBNOTINITED; goto done; } princ_len = strlen(*principal); for (le = ldap_first_entry(ipactx->lcontext, res); le != NULL; le = ldap_next_entry(ipactx->lcontext, le)) { vals = ldap_get_values_len(ipactx->lcontext, le, "krbprincipalname"); if (vals == NULL) continue; /* We need to check for a strict match as a '*' in the name may have * caused the ldap server to return multiple entries. */ for (int i = 0; vals[i]; i++) { #ifdef KRB5_KDB_FLAG_ALIAS_OK if ((flags & KRB5_KDB_FLAG_ALIAS_OK) == 0) { found = ((vals[i]->bv_len == princ_len) && strncmp(vals[i]->bv_val, *principal, vals[i]->bv_len) == 0); if (found) break; continue; } #endif /* The KDC will accept aliases when doing TGT lookup * (ref_tgt_again in do_tgs_req.c), so use case-insensitive * comparison. */ if (ulc_casecmp(vals[i]->bv_val, vals[i]->bv_len, *principal, princ_len, NULL, NULL, &result) != 0) { ret = KRB5_KDB_INTERNAL_ERROR; goto done; } if (result != 0) continue; /* Fix case on the incoming principal to ensure that a valid * name/alias is returned even if krbCanonicalName is not * present. */ free(*principal); *principal = strndup(vals[i]->bv_val, vals[i]->bv_len); if (!*principal) { ret = KRB5_KDB_INTERNAL_ERROR; goto done; } princ_len = strlen(*principal); found = true; break; } ldap_value_free_len(vals); vals = NULL; if (!found) { continue; } /* We need to check if this is the canonical name. */ vals = ldap_get_values_len(ipactx->lcontext, le, "krbcanonicalname"); if (vals == NULL) break; #ifdef KRB5_KDB_FLAG_ALIAS_OK /* If aliases aren't accepted by the KDC, use case-sensitive * comparison. */ if ((flags & KRB5_KDB_FLAG_ALIAS_OK) == 0) { found = ((vals[0]->bv_len == strlen(*principal)) && strncmp(vals[0]->bv_val, *principal, vals[0]->bv_len) == 0); if (!found) { ldap_value_free_len(vals); vals = NULL; continue; } } #endif free(*principal); *principal = strndup(vals[0]->bv_val, vals[0]->bv_len); if (!*principal) { ret = KRB5_KDB_INTERNAL_ERROR; goto done; } break; } if (!found || !le) { ret = KRB5_KDB_NOENTRY; goto done; } ret = 0; *entry = le; done: if (vals) ldap_value_free_len(vals); return ret; } static krb5_flags maybe_require_preauth(struct ipadb_context *ipactx, krb5_db_entry *entry) { const struct ipadb_global_config *config; struct ipadb_e_data *ied; 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; } } /* By default require preauth for all principals */ return KRB5_KDB_REQUIRES_PRE_AUTH; } static krb5_error_code ipadb_fetch_tktpolicy(krb5_context kcontext, LDAPMessage *lentry, krb5_db_entry *entry, uint32_t polmask) { struct ipadb_context *ipactx; krb5_error_code kerr; char *policy_dn = NULL; LDAPMessage *res = NULL; LDAPMessage *first; int result; int ret; ipactx = ipadb_get_context(kcontext); if (!ipactx) { return KRB5_KDB_DBNOTINITED; } ret = ipadb_ldap_attr_to_str(ipactx->lcontext, lentry, "krbticketpolicyreference", &policy_dn); switch (ret) { case 0: break; case ENOENT: ret = asprintf(&policy_dn, "cn=%s,cn=kerberos,%s", ipactx->realm, ipactx->base); if (ret == -1) { kerr = ENOMEM; goto done; } break; default: kerr = ret; goto done; } kerr = ipadb_simple_search(ipactx, policy_dn, LDAP_SCOPE_BASE, "(objectclass=krbticketpolicyaux)", std_tktpolicy_attrs, &res); if (kerr == 0) { first = ldap_first_entry(ipactx->lcontext, res); if (!first) { kerr = KRB5_KDB_NOENTRY; } else { if (polmask & MAXTKTLIFE_BIT) { ret = ipadb_ldap_attr_to_int(ipactx->lcontext, first, "krbmaxticketlife", &result); if (ret == 0) { entry->max_life = result; } else { entry->max_life = 86400; } } if (polmask & MAXRENEWABLEAGE_BIT) { ret = ipadb_ldap_attr_to_int(ipactx->lcontext, first, "krbmaxrenewableage", &result); if (ret == 0) { entry->max_renewable_life = result; } else { entry->max_renewable_life = 604800; } } 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 (kerr == KRB5_KDB_NOENTRY) { /* No policy at all ?? * set hardcoded default policy for now */ if (polmask & MAXTKTLIFE_BIT) { entry->max_life = 86400; } if (polmask & MAXRENEWABLEAGE_BIT) { entry->max_renewable_life = 604800; } if (polmask & TKTFLAGS_BIT) { entry->attributes |= maybe_require_preauth(ipactx, entry); } kerr = 0; } done: ldap_msgfree(res); free(policy_dn); return kerr; } static krb5_boolean is_request_for_us(krb5_context kcontext, krb5_principal local_tgs, krb5_const_principal search_for) { krb5_boolean for_us; for_us = krb5_realm_compare(kcontext, local_tgs, search_for) || krb5_principal_compare_any_realm(kcontext, local_tgs, search_for); return for_us; } static krb5_error_code dbget_princ(krb5_context kcontext, struct ipadb_context *ipactx, krb5_const_principal search_for, unsigned int flags, krb5_db_entry **entry) { krb5_error_code kerr; char *principal = NULL; LDAPMessage *res = NULL; LDAPMessage *lentry; uint32_t pol; if ((flags & KRB5_KDB_FLAG_CLIENT_REFERRALS_ONLY) != 0 && (flags & KRB5_KDB_FLAG_CANONICALIZE) != 0) { /* AS_REQ with canonicalization*/ krb5_principal norm_princ = NULL; /* unparse the Kerberos principal without (our) outer realm. */ kerr = krb5_unparse_name_flags(kcontext, search_for, KRB5_PRINCIPAL_UNPARSE_NO_REALM | KRB5_PRINCIPAL_UNPARSE_DISPLAY, &principal); if (kerr != 0) { goto done; } /* Re-parse the principal to normalize it. Innner realm becomes * the realm if present. If no inner realm, our default realm * will be used instead (as it was before). */ kerr = krb5_parse_name(kcontext, principal, &norm_princ); if (kerr != 0) { goto done; } /* Unparse without escaping '@' and '/' because we are going to use them * in LDAP filters where escaping character '\' will be escaped and the * result will never match. */ kerr = krb5_unparse_name_flags(kcontext, norm_princ, KRB5_PRINCIPAL_UNPARSE_DISPLAY, &principal); krb5_free_principal(kcontext, norm_princ); } else { /* Unparse without escaping '@' and '/' because we are going to use them * in LDAP filters where escaping character '\' will be escaped and the * result will never match. */ kerr = krb5_unparse_name_flags(kcontext, search_for, KRB5_PRINCIPAL_UNPARSE_DISPLAY, &principal); } if (kerr != 0) { goto done; } kerr = ipadb_fetch_principals(ipactx, flags, principal, &res); if (kerr != 0) { goto done; } kerr = ipadb_find_principal(kcontext, flags, res, &principal, &lentry); if (kerr != 0) { goto done; } kerr = ipadb_parse_ldap_entry(kcontext, principal, lentry, entry, &pol); if (kerr != 0) { goto done; } if (pol) { kerr = ipadb_fetch_tktpolicy(kcontext, lentry, *entry, pol); if (kerr != 0) { goto done; } } done: ldap_msgfree(res); krb5_free_unparsed_name(kcontext, principal); return kerr; } static krb5_error_code dbget_alias(krb5_context kcontext, struct ipadb_context *ipactx, krb5_const_principal search_for, unsigned int flags, krb5_db_entry **entry) { krb5_error_code kerr = 0; char *principal = NULL; krb5_principal norm_princ = NULL; char *trusted_realm = NULL; krb5_db_entry *kentry = NULL; krb5_data *realm; /* TODO: also support hostbased aliases */ /* Enterprise principal name type is for potential aliases or principals * from trusted realms. The logic below only applies to this type */ if (krb5_princ_type(kcontext, search_for) != KRB5_NT_ENTERPRISE_PRINCIPAL) { return KRB5_KDB_NOENTRY; } /* enterprise principal can only have single component in the name * according to RFC6806 section 5. */ if (krb5_princ_size(kcontext, search_for) != 1) { return KRB5_KDB_NOENTRY; } /* unparse the Kerberos principal without (our) outer realm. */ kerr = krb5_unparse_name_flags(kcontext, search_for, KRB5_PRINCIPAL_UNPARSE_NO_REALM | KRB5_PRINCIPAL_UNPARSE_DISPLAY, &principal); if (kerr != 0) { goto done; } /* Re-parse the principal to normalize it. Innner realm becomes * the realm if present. If no inner realm, our default realm * will be used instead (as it was before). */ kerr = krb5_parse_name(kcontext, principal, &norm_princ); if (kerr != 0) { goto done; } if (krb5_realm_compare(kcontext, ipactx->local_tgs, norm_princ)) { /* In realm alias, try to retrieve it and let the caller handle it. */ kerr = dbget_princ(kcontext, ipactx, norm_princ, flags, entry); goto done; } /* The request is out of realm starting from here */ /* * Per RFC6806 section 7 and 8, the canonicalize flag is required for * both client and server referrals. But it is more useful to ignore it * like Windows KDC does for client referrals. */ if (((flags & KRB5_KDB_FLAG_CANONICALIZE) == 0) && ((flags & KRB5_KDB_FLAG_CLIENT_REFERRALS_ONLY) == 0)) { kerr = KRB5_KDB_NOENTRY; goto done; } /* Determine the trusted realm to refer to. We don't need the principal * itself, only its realm */ realm = krb5_princ_realm(kcontext, norm_princ); kerr = ipadb_is_princ_from_trusted_realm(kcontext, realm->data, realm->length, &trusted_realm); if (kerr == KRB5_KDB_NOENTRY) { /* If no trusted realm found, refresh trusted domain data and try again * because it might be a freshly added trust to AD */ kerr = ipadb_reinit_mspac(ipactx, false); if (kerr != 0) { kerr = KRB5_KDB_NOENTRY; goto done; } kerr = ipadb_is_princ_from_trusted_realm(kcontext, realm->data, realm->length, &trusted_realm); } if (kerr != 0) { goto done; } /* This is a known trusted realm. Issue a referral depending on whether this * is client or server referral request */ if (flags & KRB5_KDB_FLAG_CLIENT_REFERRALS_ONLY) { /* client referral out of realm, set next realm. */ kerr = krb5_set_principal_realm(kcontext, norm_princ, trusted_realm); if (kerr != 0) { goto done; } kentry = calloc(1, sizeof(krb5_db_entry)); if (!kentry) { kerr = ENOMEM; goto done; } kentry->princ = norm_princ; norm_princ = NULL; *entry = kentry; goto done; } if (flags & KRB5_KDB_FLAG_INCLUDE_PAC) { /* TGS request where KDC wants to generate PAC * but the principal is out of our realm */ kerr = KRB5_KDB_NOENTRY; goto done; } /* server referrals: lookup krbtgt/next_realm@our_realm */ krb5_free_principal(kcontext, norm_princ); norm_princ = NULL; kerr = krb5_build_principal_ext(kcontext, &norm_princ, strlen(ipactx->realm), ipactx->realm, KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME, strlen(trusted_realm), trusted_realm, 0); if (kerr != 0) { goto done; } kerr = dbget_princ(kcontext, ipactx, norm_princ, flags, entry); done: free(trusted_realm); krb5_free_principal(kcontext, norm_princ); krb5_free_unparsed_name(kcontext, principal); return kerr; } /* TODO: handle case where main object and krbprincipal data are not * the same object but linked objects ? * (by way of krbprincipalaux being in a separate object from krbprincipal). * Currently we only support objcts with both objectclasses present at the * same time. */ krb5_error_code ipadb_get_principal(krb5_context kcontext, krb5_const_principal search_for, unsigned int flags, krb5_db_entry **entry) { struct ipadb_context *ipactx; krb5_error_code kerr; *entry = NULL; ipactx = ipadb_get_context(kcontext); if (!ipactx) { return KRB5_KDB_DBNOTINITED; } if (!is_request_for_us(kcontext, ipactx->local_tgs, search_for)) { return KRB5_KDB_NOENTRY; } /* Lookup local names and aliases first. */ kerr = dbget_princ(kcontext, ipactx, search_for, flags, entry); if (kerr != KRB5_KDB_NOENTRY) { return kerr; } return dbget_alias(kcontext, ipactx, search_for, flags, entry); } void ipadb_free_principal_e_data(krb5_context kcontext, krb5_octet *e_data) { struct ipadb_e_data *ied; int i; ied = (struct ipadb_e_data *)e_data; if (ied->magic == IPA_E_DATA_MAGIC) { ldap_memfree(ied->entry_dn); free(ied->passwd); free(ied->user); free(ied->pw_policy_dn); for (i = 0; ied->pw_history && ied->pw_history[i]; i++) { free(ied->pw_history[i]); } free(ied->pw_history); for (i = 0; ied->authz_data && ied->authz_data[i]; i++) { free(ied->authz_data[i]); } free(ied->authz_data); free(ied->pol); free_sid(&ied->sid); free(ied); } } void ipadb_free_principal(krb5_context kcontext, krb5_db_entry *entry) { krb5_tl_data *prev, *next; if (entry) { krb5_free_principal(kcontext, entry->princ); prev = entry->tl_data; while(prev) { next = prev->tl_data_next; free(prev->tl_data_contents); free(prev); prev = next; } ipa_krb5_free_key_data(entry->key_data, entry->n_key_data); if (entry->e_data) { ipadb_free_principal_e_data(kcontext, entry->e_data); } free(entry); } } static krb5_error_code ipadb_get_tl_data(krb5_db_entry *entry, krb5_int16 type, krb5_ui_2 length, krb5_octet *data) { krb5_tl_data *td; for (td = entry->tl_data; td; td = td->tl_data_next) { if (td->tl_data_type == type) { break; } } if (!td) { return ENOENT; } if (td->tl_data_length != length) { return EINVAL; } memcpy(data, td->tl_data_contents, length); return 0; } struct ipadb_mods { LDAPMod **mods; int alloc_size; int tip; }; static int new_ipadb_mods(struct ipadb_mods **imods) { struct ipadb_mods *r; r = malloc(sizeof(struct ipadb_mods)); if (!r) { return ENOMEM; } /* alloc the average space for a full change of all ldap attrinbutes */ r->alloc_size = 15; r->mods = calloc(r->alloc_size, sizeof(LDAPMod *)); if (!r->mods) { free(r); return ENOMEM; } r->tip = 0; *imods = r; return 0; } static void ipadb_mods_free(struct ipadb_mods *imods) { if (imods == NULL) { return; } ldap_mods_free(imods->mods, 1); free(imods); } static krb5_error_code ipadb_mods_new(struct ipadb_mods *imods, LDAPMod **slot) { LDAPMod **lmods = NULL; LDAPMod *m; int n; lmods = imods->mods; for (n = imods->tip; n < imods->alloc_size && lmods[n] != NULL; n++) { /* find empty slot */ ; } if (n + 1 > imods->alloc_size) { /* need to increase size */ lmods = realloc(imods->mods, (n * 2) * sizeof(LDAPMod *)); if (!lmods) { return ENOMEM; } imods->mods = lmods; imods->alloc_size = n * 2; memset(&lmods[n + 1], 0, (imods->alloc_size - n - 1) * sizeof(LDAPMod *)); } m = calloc(1, sizeof(LDAPMod)); if (!m) { return ENOMEM; } imods->tip = n; *slot = imods->mods[n] = m; return 0; } static void ipadb_mods_free_tip(struct ipadb_mods *imods) { LDAPMod *m; int i; if (imods->alloc_size == 0) { return; } m = imods->mods[imods->tip]; if (!m) { return; } free(m->mod_type); if (m->mod_values) { for (i = 0; m->mod_values[i]; i++) { free(m->mod_values[i]); } } free(m->mod_values); free(m); imods->mods[imods->tip] = NULL; imods->tip--; } static krb5_error_code ipadb_get_ldap_mod_str(struct ipadb_mods *imods, char *attribute, char *value, int mod_op) { krb5_error_code kerr; LDAPMod *m = NULL; kerr = ipadb_mods_new(imods, &m); if (kerr) { return kerr; } m->mod_op = mod_op; m->mod_type = strdup(attribute); if (!m->mod_type) { kerr = ENOMEM; goto done; } m->mod_values = calloc(2, sizeof(char *)); if (!m->mod_values) { kerr = ENOMEM; goto done; } m->mod_values[0] = strdup(value); if (!m->mod_values[0]) { kerr = ENOMEM; goto done; } kerr = 0; done: if (kerr) { ipadb_mods_free_tip(imods); } return kerr; } static krb5_error_code ipadb_get_ldap_mod_int(struct ipadb_mods *imods, char *attribute, int value, int mod_op) { krb5_error_code kerr; char *v = NULL; int ret; ret = asprintf(&v, "%d", value); if (ret == -1) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } kerr = ipadb_get_ldap_mod_str(imods, attribute, v, mod_op); done: free(v); return kerr; } static krb5_error_code ipadb_get_ldap_mod_time(struct ipadb_mods *imods, char *attribute, krb5_timestamp value, int mod_op) { struct tm date, *t; time_t timeval; char v[20]; timeval = krb5_ts2tt(value); t = gmtime_r(&timeval, &date); if (t == NULL) { return EINVAL; } strftime(v, 20, "%Y%m%d%H%M%SZ", &date); return ipadb_get_ldap_mod_str(imods, attribute, v, mod_op); } static krb5_error_code ipadb_get_ldap_mod_bvalues(struct ipadb_mods *imods, char *attribute, struct berval **values, int num_values, int mod_op) { krb5_error_code kerr; LDAPMod *m = NULL; int i; if (values == NULL || values[0] == NULL || num_values <= 0) { return EINVAL; } kerr = ipadb_mods_new(imods, &m); if (kerr) { return kerr; } m->mod_op = mod_op | LDAP_MOD_BVALUES; m->mod_type = strdup(attribute); if (!m->mod_type) { kerr = ENOMEM; goto done; } m->mod_bvalues = calloc(num_values + 1, sizeof(struct berval *)); if (!m->mod_bvalues) { kerr = ENOMEM; goto done; } for (i = 0; i < num_values; i++) { m->mod_bvalues[i] = values[i]; } kerr = 0; done: if (kerr) { /* we need to free bvalues manually here otherwise * ipadb_mods_free_tip will free contents which we * did not allocate here */ free(m->mod_bvalues); m->mod_bvalues = NULL; ipadb_mods_free_tip(imods); } return kerr; } static krb5_error_code ipadb_get_ldap_mod_extra_data(struct ipadb_mods *imods, krb5_tl_data *tl_data, int mod_op) { krb5_error_code kerr; krb5_tl_data *data; struct berval **bvs = NULL; krb5_int16 be_type; int n, i; for (n = 0, data = tl_data; data; data = data->tl_data_next) { if (data->tl_data_type == KRB5_TL_LAST_PWD_CHANGE || data->tl_data_type == KRB5_TL_KADM_DATA || data->tl_data_type == KRB5_TL_DB_ARGS || data->tl_data_type == KRB5_TL_MKVNO || data->tl_data_type == KRB5_TL_LAST_ADMIN_UNLOCK) { continue; } n++; } if (n == 0) { return ENOENT; } bvs = calloc(n + 1, sizeof(struct berval *)); if (!bvs) { kerr = ENOMEM; goto done; } for (i = 0, data = tl_data; data; data = data->tl_data_next) { if (data->tl_data_type == KRB5_TL_LAST_PWD_CHANGE || data->tl_data_type == KRB5_TL_KADM_DATA || data->tl_data_type == KRB5_TL_DB_ARGS || data->tl_data_type == KRB5_TL_MKVNO || data->tl_data_type == KRB5_TL_LAST_ADMIN_UNLOCK) { continue; } be_type = htons(data->tl_data_type); bvs[i] = calloc(1, sizeof(struct berval)); if (!bvs[i]) { kerr = ENOMEM; goto done; } bvs[i]->bv_len = data->tl_data_length + 2; bvs[i]->bv_val = malloc(bvs[i]->bv_len); if (!bvs[i]->bv_val) { kerr = ENOMEM; goto done; } memcpy(bvs[i]->bv_val, &be_type, 2); memcpy(&(bvs[i]->bv_val[2]), data->tl_data_contents, data->tl_data_length); i++; if (i > n) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } } kerr = ipadb_get_ldap_mod_bvalues(imods, "krbExtraData", bvs, i, mod_op); done: if (kerr) { for (i = 0; bvs && bvs[i]; i++) { free(bvs[i]->bv_val); free(bvs[i]); } } free(bvs); return kerr; } static krb5_error_code ipadb_get_mkvno_from_tl_data(krb5_tl_data *tl_data, int *mkvno) { krb5_tl_data *data; int master_kvno = 0; krb5_int16 tmp; for (data = tl_data; data; data = data->tl_data_next) { if (data->tl_data_type != KRB5_TL_MKVNO) { continue; } if (data->tl_data_length != 2) { return KRB5_KDB_TRUNCATED_RECORD; } memcpy(&tmp, data->tl_data_contents, 2); master_kvno = le16toh(tmp); break; } if (master_kvno == 0) { /* fall back to std mkvno of 1 */ *mkvno = 1; } else { *mkvno = master_kvno; } return 0; } static krb5_error_code ipadb_get_ldap_mod_key_data(struct ipadb_mods *imods, krb5_key_data *key_data, int n_key_data, int mkvno, int mod_op) { krb5_error_code kerr; struct berval *bval = NULL; LDAPMod *mod; int ret; /* If the key data is empty, remove all keys. */ if (n_key_data == 0 || key_data == NULL) { kerr = ipadb_mods_new(imods, &mod); if (kerr != 0) return kerr; mod->mod_op = LDAP_MOD_DELETE; mod->mod_bvalues = NULL; mod->mod_type = strdup("krbPrincipalKey"); if (mod->mod_type == NULL) { ipadb_mods_free_tip(imods); return ENOMEM; } return 0; } ret = ber_encode_krb5_key_data(key_data, n_key_data, mkvno, &bval); if (ret != 0) { kerr = ret; goto done; } kerr = ipadb_get_ldap_mod_bvalues(imods, "krbPrincipalKey", &bval, 1, mod_op); done: if (kerr) { ber_bvfree(bval); } return kerr; } static krb5_error_code ipadb_get_ldap_mod_str_list(struct ipadb_mods *imods, char *attrname, char **strlist, int len, int mod_op) { krb5_error_code kerr; struct berval **bvs = NULL; int i; bvs = calloc(len + 1, sizeof(struct berval *)); if (!bvs) { kerr = ENOMEM; goto done; } for (i = 0; i < len; i++) { bvs[i] = calloc(1, sizeof(struct berval)); if (!bvs[i]) { kerr = ENOMEM; goto done; } bvs[i]->bv_val = strdup(strlist[i]); if (!bvs[i]->bv_val) { kerr = ENOMEM; goto done; } bvs[i]->bv_len = strlen(strlist[i]) + 1; } kerr = ipadb_get_ldap_mod_bvalues(imods, attrname, bvs, len, mod_op); done: if (kerr) { for (i = 0; bvs && bvs[i]; i++) { free(bvs[i]->bv_val); free(bvs[i]); } } free(bvs); return kerr; } static krb5_error_code ipadb_principal_to_mods(krb5_context kcontext, struct ipadb_mods *imods, char *principal, int mod_op) { krb5_error_code kerr; if (principal == NULL) { kerr = EINVAL; goto done; } kerr = ipadb_get_ldap_mod_str(imods, "krbPrincipalName", principal, mod_op); if (kerr) { goto done; } kerr = ipadb_get_ldap_mod_str(imods, "krbCanonicalName", principal, mod_op); if (kerr) { goto done; } kerr = 0; done: return kerr; } static krb5_error_code ipadb_get_ldap_mod_auth_ind(krb5_context kcontext, struct ipadb_mods *imods, krb5_db_entry *entry, int mod_op) { krb5_error_code ret = 0; char **strlist = NULL; char *ais = NULL; char *ai = NULL; char *s = NULL; size_t ai_size = 0; int cnt = 0; size_t i = 0; ret = krb5_dbe_get_string(kcontext, entry, "require_auth", &ais); if (ret) { return ret; } if (ais == NULL) { return 0; } ai_size = strlen(ais) + 1; for (i = 0; i < ai_size; i++) { if (ais[i] != ' ') { continue; } if (i > 0 && ais[i - 1] != ' ') { cnt++; } } strlist = calloc(cnt + 2, sizeof(*strlist)); if (strlist == NULL) { free(ais); return errno; } cnt = 0; ai = strtok_r(ais, " ", &s); while (ai != NULL) { if (ai[0] != '\0') { strlist[cnt++] = ai; } ai = strtok_r(NULL, " ", &s); } ret = ipadb_get_ldap_mod_str_list(imods, "krbPrincipalAuthInd", strlist, cnt, mod_op); free(ais); free(strlist); return ret; } static krb5_error_code ipadb_entry_to_mods(krb5_context kcontext, struct ipadb_mods *imods, krb5_db_entry *entry, int mod_op) { krb5_error_code kerr; krb5_int32 time32le; int mkvno; char *req_auth_str = NULL; /* check each mask flag in order */ /* KADM5_PRINC_EXPIRE_TIME */ if (entry->mask & KMASK_PRINC_EXPIRE_TIME) { kerr = ipadb_get_ldap_mod_time(imods, "krbPrincipalExpiration", entry->expiration, mod_op); if (kerr) { goto done; } } /* KADM5_PW_EXPIRATION */ if (entry->mask & KMASK_PW_EXPIRATION) { kerr = ipadb_get_ldap_mod_time(imods, "krbPasswordExpiration", entry->pw_expiration, mod_op); if (entry->pw_expiration == 0) { kerr = ipadb_get_ldap_mod_time(imods, "krbPasswordExpiration", entry->pw_expiration, LDAP_MOD_DELETE); } if (kerr) { goto done; } } /* KADM5_LAST_PWD_CHANGE */ /* apparently, at least some versions of kadmin fail to set this flag * when they do include a pwd change timestamp in TL_DATA. * So for now check if KADM5_KEY_DATA has been set, which kadm5 * always does on password changes */ #if KADM5_ACTUALLY_SETS_LAST_PWD_CHANGE if (entry->mask & KMASK_LAST_PWD_CHANGE) { if (!entry->n_tl_data) { kerr = EINVAL; goto done; } #else if (entry->n_tl_data && entry->mask & KMASK_KEY_DATA) { #endif kerr = ipadb_get_tl_data(entry, KRB5_TL_LAST_PWD_CHANGE, sizeof(time32le), (krb5_octet *)&time32le); if (kerr && kerr != ENOENT) { goto done; } if (kerr == 0) { kerr = ipadb_get_ldap_mod_time(imods, "krbLastPwdChange", le32toh(time32le), mod_op); if (kerr) { goto done; } } } /* 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) { goto done; } } /* KADM5_MAX_LIFE */ if (entry->mask & KMASK_MAX_LIFE) { kerr = ipadb_get_ldap_mod_int(imods, "krbMaxTicketLife", (int)entry->max_life, mod_op); if (kerr) { goto done; } } /* KADM5_MOD_TIME */ /* KADM5_MOD_NAME */ /* KADM5_KVNO */ /* KADM5_MKVNO */ /* KADM5_AUX_ATTRIBUTES */ /* KADM5_POLICY */ /* KADM5_POLICY_CLR */ /* version 2 masks */ /* KADM5_MAX_RLIFE */ if (entry->mask & KMASK_MAX_RLIFE) { kerr = ipadb_get_ldap_mod_int(imods, "krbMaxRenewableAge", (int)entry->max_renewable_life, mod_op); if (kerr) { goto done; } } /* KADM5_LAST_SUCCESS */ if (entry->mask & KMASK_LAST_SUCCESS) { kerr = ipadb_get_ldap_mod_time(imods, "krbLastSuccessfulAuth", entry->last_success, mod_op); if (kerr) { goto done; } } /* KADM5_LAST_FAILED */ if (entry->mask & KMASK_LAST_FAILED) { kerr = ipadb_get_ldap_mod_time(imods, "krbLastFailedAuth", entry->last_failed, mod_op); if (kerr) { goto done; } } /* KADM5_FAIL_AUTH_COUNT */ if (entry->mask & KMASK_FAIL_AUTH_COUNT) { kerr = ipadb_get_ldap_mod_int(imods, "krbLoginFailedCount", (int)entry->fail_auth_count, mod_op); if (kerr) { goto done; } } /* KADM5_KEY_DATA */ if (entry->mask & KMASK_KEY_DATA) { /* TODO: password changes should go via change_pwd * then we can get clear text and set all needed * LDAP attributes */ kerr = ipadb_get_mkvno_from_tl_data(entry->tl_data, &mkvno); if (kerr) { goto done; } kerr = ipadb_get_ldap_mod_key_data(imods, entry->key_data, entry->n_key_data, mkvno, mod_op); if (kerr) { goto done; } } kerr = ipadb_get_ldap_mod_auth_ind(kcontext, imods, entry, mod_op); if (kerr) goto done; /* KADM5_TL_DATA */ if (entry->mask & KMASK_TL_DATA) { kerr = ipadb_get_tl_data(entry, KRB5_TL_LAST_ADMIN_UNLOCK, sizeof(time32le), (krb5_octet *)&time32le); if (kerr && kerr != ENOENT) { goto done; } if (kerr == 0) { kerr = ipadb_get_ldap_mod_time(imods, "krbLastAdminUnlock", le32toh(time32le), mod_op); if (kerr) { goto done; } } kerr = krb5_dbe_get_string(kcontext, entry, "require_auth", &req_auth_str); if (kerr) { goto done; } /* Do not store auth indicators from the string attribute in * krbExtraData. Remove require_auth value from the entry temporarily. */ if (req_auth_str != NULL) { kerr = krb5_dbe_set_string(kcontext, entry, "require_auth", NULL); if (kerr) { goto done; } } kerr = ipadb_get_ldap_mod_extra_data(imods, entry->tl_data, mod_op); if (kerr && kerr != ENOENT) { goto done; } /* Restore require_auth value */ if (req_auth_str != NULL) { kerr = krb5_dbe_set_string(kcontext, entry, "require_auth", req_auth_str); if (kerr) { goto done; } } } /* KADM5_LOAD */ /* Handle password change related operations. */ if (entry->e_data) { struct ipadb_e_data *ied; time_t now = time(NULL); time_t expire_time; char **new_history; int nh_len; int ret; int i; ied = (struct ipadb_e_data *)entry->e_data; if (ied->magic != IPA_E_DATA_MAGIC) { kerr = EINVAL; goto done; } /* * We need to set userPassword and history only if this is * a IPA User, we don't do that for simple service principals */ if (ied->ipa_user && ied->passwd) { kerr = ipadb_get_ldap_mod_str(imods, "userPassword", ied->passwd, mod_op); if (kerr) { goto done; } /* Also set new password expiration time. * Have to do it here because kadmin doesn't know policies and * resets entry->mask after we have gone through the password * change code. */ kerr = ipadb_get_pwd_expiration(kcontext, entry, ied, &expire_time); if (kerr) { goto done; } kerr = ipadb_get_ldap_mod_time(imods, "krbPasswordExpiration", expire_time, mod_op); if (expire_time == 0) { kerr = ipadb_get_ldap_mod_time(imods, "krbPasswordExpiration", expire_time, LDAP_MOD_DELETE); } if (kerr) { goto done; } } if (ied->ipa_user && ied->passwd && ied->pol && ied->pol->history_length) { ret = ipapwd_generate_new_history(ied->passwd, now, ied->pol->history_length, ied->pw_history, &new_history, &nh_len); if (ret) { kerr = ret; goto done; } kerr = ipadb_get_ldap_mod_str_list(imods, "passwordHistory", new_history, nh_len, mod_op); for (i = 0; i < nh_len; i++) { free(new_history[i]); } free(new_history); if (kerr) { goto done; } } } kerr = 0; done: free(req_auth_str); return kerr; } /* adds default objectclasses and attributes */ static krb5_error_code ipadb_entry_default_attrs(struct ipadb_mods *imods) { krb5_error_code kerr; LDAPMod *m = NULL; size_t i; kerr = ipadb_mods_new(imods, &m); if (kerr) { return kerr; } m->mod_op = LDAP_MOD_ADD; m->mod_type = strdup("objectClass"); if (!m->mod_type) { kerr = ENOMEM; goto done; } m->mod_values = calloc(STD_PRINCIPAL_OBJ_CLASSES_SIZE + 1, sizeof(char *)); if (!m->mod_values) { kerr = ENOMEM; goto done; } for (i = 0; i < STD_PRINCIPAL_OBJ_CLASSES_SIZE; i++) { m->mod_values[i] = strdup(std_principal_obj_classes[i]); if (!m->mod_values[i]) { kerr = ENOMEM; goto done; } } kerr = 0; done: if (kerr) { ipadb_mods_free_tip(imods); } return kerr; } static krb5_error_code ipadb_add_principal(krb5_context kcontext, krb5_db_entry *entry) { struct ipadb_context *ipactx; krb5_error_code kerr; char *principal = NULL; struct ipadb_mods *imods = NULL; char *dn = NULL; int ret; ipactx = ipadb_get_context(kcontext); if (!ipactx) { kerr = KRB5_KDB_DBNOTINITED; goto done; } if (!ipactx->override_restrictions) { return KRB5_KDB_CONSTRAINT_VIOLATION; } kerr = krb5_unparse_name(kcontext, entry->princ, &principal); if (kerr != 0) { goto done; } ret = asprintf(&dn, "krbPrincipalName=%s,cn=%s,cn=kerberos,%s", principal, ipactx->realm, ipactx->base); if (ret == -1) { kerr = ENOMEM; goto done; } ret = new_ipadb_mods(&imods); if (ret != 0) { kerr = ret; goto done; } kerr = ipadb_entry_default_attrs(imods); if (kerr != 0) { goto done; } kerr = ipadb_principal_to_mods(kcontext, imods, principal, LDAP_MOD_ADD); if (kerr != 0) { goto done; } kerr = ipadb_entry_to_mods(kcontext, imods, entry, LDAP_MOD_ADD); if (kerr != 0) { goto done; } kerr = ipadb_simple_add(ipactx, dn, imods->mods); done: ipadb_mods_free(imods); krb5_free_unparsed_name(kcontext, principal); ldap_memfree(dn); return kerr; } static krb5_error_code ipadb_modify_principal(krb5_context kcontext, krb5_db_entry *entry) { struct ipadb_context *ipactx; krb5_error_code kerr; char *principal = NULL; LDAPMessage *res = NULL; LDAPMessage *lentry; struct ipadb_mods *imods = NULL; char *dn = NULL; struct ipadb_e_data *ied; ipactx = ipadb_get_context(kcontext); if (!ipactx) { return KRB5_KDB_DBNOTINITED; } kerr = new_ipadb_mods(&imods); if (kerr) { goto done; } ied = (struct ipadb_e_data *)entry->e_data; if (!ied || !ied->entry_dn) { kerr = krb5_unparse_name(kcontext, entry->princ, &principal); if (kerr != 0) { goto done; } kerr = ipadb_fetch_principals(ipactx, 0, principal, &res); if (kerr != 0) { goto done; } /* FIXME: no alias allowed for now, should we allow modifies * by alias name ? */ kerr = ipadb_find_principal(kcontext, 0, res, &principal, &lentry); if (kerr != 0) { goto done; } dn = ldap_get_dn(ipactx->lcontext, lentry); if (!dn) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } kerr = ipadb_principal_to_mods(kcontext, imods, principal, LDAP_MOD_REPLACE); if (kerr != 0) { goto done; } } kerr = ipadb_entry_to_mods(kcontext, imods, entry, LDAP_MOD_REPLACE); if (kerr != 0) { goto done; } if (!ied || !ied->entry_dn) { kerr = ipadb_simple_modify(ipactx, dn, imods->mods); } else { kerr = ipadb_simple_modify(ipactx, ied->entry_dn, imods->mods); } done: ipadb_mods_free(imods); ldap_msgfree(res); krb5_free_unparsed_name(kcontext, principal); ldap_memfree(dn); return kerr; } krb5_error_code ipadb_put_principal(krb5_context kcontext, krb5_db_entry *entry, char **db_args) { if (entry->mask & KMASK_PRINCIPAL) { return ipadb_add_principal(kcontext, entry); } else { return ipadb_modify_principal(kcontext, entry); } } static krb5_error_code ipadb_delete_entry(krb5_context kcontext, LDAPMessage *lentry) { struct ipadb_context *ipactx; krb5_error_code kerr; char *dn = NULL; int ret; ipactx = ipadb_get_context(kcontext); if (!ipactx) { kerr = KRB5_KDB_DBNOTINITED; goto done; } if (!ipactx->lcontext) { ret = ipadb_get_connection(ipactx); if (ret != 0) { kerr = KRB5_KDB_SERVER_INTERNAL_ERR; goto done; } } dn = ldap_get_dn(ipactx->lcontext, lentry); if (!dn) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } kerr = ipadb_simple_delete(ipactx, dn); done: ldap_memfree(dn); return kerr; } static krb5_error_code ipadb_delete_alias(krb5_context kcontext, LDAPMessage *lentry, char *principal) { struct ipadb_context *ipactx; krb5_error_code kerr; char *dn = NULL; int ret; ipactx = ipadb_get_context(kcontext); if (!ipactx) { kerr = KRB5_KDB_DBNOTINITED; goto done; } if (!ipactx->lcontext) { ret = ipadb_get_connection(ipactx); if (ret != 0) { kerr = KRB5_KDB_SERVER_INTERNAL_ERR; goto done; } } dn = ldap_get_dn(ipactx->lcontext, lentry); if (!dn) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } kerr = ipadb_simple_delete_val(ipactx, dn, "krbprincipalname", principal); done: ldap_memfree(dn); return kerr; } krb5_error_code ipadb_delete_principal(krb5_context kcontext, krb5_const_principal search_for) { struct ipadb_context *ipactx; krb5_error_code kerr; char *principal = NULL; char *canonicalized = NULL; LDAPMessage *res = NULL; LDAPMessage *lentry; unsigned int flags = 0; ipactx = ipadb_get_context(kcontext); if (!ipactx) { return KRB5_KDB_DBNOTINITED; } if (!ipactx->override_restrictions) { return KRB5_KDB_CONSTRAINT_VIOLATION; } kerr = krb5_unparse_name(kcontext, search_for, &principal); if (kerr != 0) { goto done; } kerr = ipadb_fetch_principals(ipactx, 0, principal, &res); if (kerr != 0) { goto done; } canonicalized = strdup(principal); if (!canonicalized) { kerr = ENOMEM; goto done; } #ifdef KRB5_KDB_FLAG_ALIAS_OK flags = KRB5_KDB_FLAG_ALIAS_OK; #endif kerr = ipadb_find_principal(kcontext, flags, res, &canonicalized, &lentry); if (kerr != 0) { goto done; } /* check if this is an alias (remove it) or if we should remove the whole * ldap record */ /* TODO: should we use case insensitive matching here ? */ if (strcmp(canonicalized, principal) == 0) { kerr = ipadb_delete_entry(kcontext, lentry); } else { kerr = ipadb_delete_alias(kcontext, lentry, principal); } done: ldap_msgfree(res); free(canonicalized); krb5_free_unparsed_name(kcontext, principal); return kerr; } #if KRB5_KDB_API_VERSION < 8 krb5_error_code ipadb_iterate(krb5_context kcontext, char *match_entry, int (*func)(krb5_pointer, krb5_db_entry *), krb5_pointer func_arg) #else krb5_error_code ipadb_iterate(krb5_context kcontext, char *match_entry, int (*func)(krb5_pointer, krb5_db_entry *), krb5_pointer func_arg, krb5_flags iterflags) #endif { struct ipadb_context *ipactx; krb5_error_code kerr; LDAPMessage *res = NULL; LDAPMessage *lentry; krb5_db_entry *kentry; uint32_t pol; ipactx = ipadb_get_context(kcontext); if (!ipactx) { return KRB5_KDB_DBNOTINITED; } /* If no match_entry is given iterate through all krb princs like the db2 * or ldap plugin */ if (match_entry == NULL) { match_entry = "*"; } /* fetch list of principal matching filter */ kerr = ipadb_fetch_principals(ipactx, 0, match_entry, &res); if (kerr != 0) { goto done; } lentry = ldap_first_entry(ipactx->lcontext, res); while (lentry) { kentry = NULL; kerr = ipadb_parse_ldap_entry(kcontext, NULL, lentry, &kentry, &pol); if (kerr == 0 && pol != 0) { kerr = ipadb_fetch_tktpolicy(kcontext, lentry, kentry, pol); } if (kerr == 0) { /* Now call the callback with the entry */ func(func_arg, kentry); } ipadb_free_principal(kcontext, kentry); lentry = ldap_next_entry(ipactx->lcontext, lentry); } kerr = 0; done: ldap_msgfree(res); return kerr; }