ipa-pwd-extop: Use common password policy code

This commit is contained in:
Simo Sorce 2011-07-01 13:32:57 -04:00
parent a1637d47c0
commit 7ea0b5d56e
4 changed files with 128 additions and 449 deletions

View File

@ -2,7 +2,8 @@ NULL =
PLUGIN_COMMON_DIR=../common
KRB5_UTIL_DIR= ../../../util
KRB5_UTIL_SRCS = $(KRB5_UTIL_DIR)/ipa_krb5.c
KRB5_UTIL_SRCS = $(KRB5_UTIL_DIR)/ipa_krb5.c \
$(KRB5_UTIL_DIR)/ipa_pwd.c
INCLUDES = \
-I. \

View File

@ -120,6 +120,24 @@ static int filter_keys(struct ipapwd_krbcfg *krbcfg,
return 0;
}
static int ipapwd_to_ldap_pwpolicy_error(int ipapwderr)
{
switch (ipapwderr) {
case IPAPWD_POLICY_ACCOUNT_EXPIRED:
return LDAP_PWPOLICY_PWDMODNOTALLOWED;
case IPAPWD_POLICY_PWD_TOO_YOUNG:
return LDAP_PWPOLICY_PWDTOOYOUNG;
case IPAPWD_POLICY_PWD_TOO_SHORT:
return LDAP_PWPOLICY_PWDTOOSHORT;
case IPAPWD_POLICY_PWD_IN_HISTORY:
return LDAP_PWPOLICY_PWDINHISTORY;
case IPAPWD_POLICY_PWD_COMPLEXITY:
return LDAP_PWPOLICY_INVALIDPWDSYNTAX;
}
/* in case of unhandled error return access denied */
return LDAP_PWPOLICY_PWDMODNOTALLOWED;
}
static int ipapwd_chpwop(Slapi_PBlock *pb, struct ipapwd_krbcfg *krbcfg)
{
@ -374,12 +392,13 @@ parse_req_done:
ret = ipapwd_CheckPolicy(&pwdata);
if (ret) {
errMesg = "Password Fails to meet minimum strength criteria";
if (ret & IPAPWD_POLICY_ERROR) {
slapi_pwpolicy_make_response_control(pb, -1, -1, ret & IPAPWD_POLICY_MASK);
rc = LDAP_CONSTRAINT_VIOLATION;
} else {
if (ret == IPAPWD_POLICY_ERROR) {
errMesg = "Internal error";
rc = ret;
} else {
ret = ipapwd_to_ldap_pwpolicy_error(ret);
slapi_pwpolicy_make_response_control(pb, -1, -1, ret);
rc = LDAP_CONSTRAINT_VIOLATION;
}
goto free_and_return;
}

View File

@ -61,6 +61,7 @@
#include <openssl/md4.h>
#include "ipa_krb5.h"
#include "ipa_pwd.h"
#define IPAPWD_PLUGIN_NAME "ipa-pwd-extop"
#define IPAPWD_FEATURE_DESC "IPA Password Manager"
@ -80,10 +81,9 @@ struct ipapwd_data {
char *dn;
char *password;
time_t timeNow;
time_t lastPwChange;
time_t expireTime;
int changetype;
int pwHistoryLen;
struct ipapwd_policy policy;
};
struct ipapwd_operation {
@ -94,11 +94,6 @@ struct ipapwd_operation {
#define GENERALIZED_TIME_LENGTH 15
#define IPAPWD_POLICY_MASK 0x0FF
#define IPAPWD_POLICY_ERROR 0x100
#define IPAPWD_POLICY_OK 0
/* from ipapwd_common.c */
struct ipapwd_krbcfg {
krb5_context krbctx;

View File

@ -329,8 +329,9 @@ static int ipapwd_rdn_count(const char *dn)
return rdnc;
}
static int ipapwd_getPolicy(const char *dn,
Slapi_Entry *target, Slapi_Entry **e)
int ipapwd_getPolicy(const char *dn,
Slapi_Entry *target,
struct ipapwd_policy *policy)
{
const char *krbPwdPolicyReference;
const char *pdn;
@ -347,6 +348,7 @@ static int ipapwd_getPolicy(const char *dn,
int buffer_flags=0;
Slapi_ValueSet* results = NULL;
char* actual_type_name = NULL;
int tmpint;
LOG_TRACE("Searching policy for [%s]\n", dn);
@ -379,8 +381,6 @@ static int ipapwd_getPolicy(const char *dn,
scope = LDAP_SCOPE_SUBTREE;
}
*e = NULL;
pb = slapi_pblock_new();
slapi_search_internal_set_pb(pb,
pdn, scope,
@ -413,10 +413,8 @@ static int ipapwd_getPolicy(const char *dn,
/* if there is only one, return that */
if (i == 1) {
*e = slapi_entry_dup(es[0]);
ret = 0;
goto done;
pe = es[0];
goto fill;
}
/* count number of RDNs in DN */
@ -463,8 +461,27 @@ static int ipapwd_getPolicy(const char *dn,
goto done;
}
*e = slapi_entry_dup(pe);
fill:
policy->min_pwd_life = slapi_entry_attr_get_int(pe, "krbMinPwdLife");
tmpint = slapi_entry_attr_get_int(pe, "krbMaxPwdLife");
if (tmpint != 0) {
policy->max_pwd_life = tmpint;
}
tmpint = slapi_entry_attr_get_int(pe, "krbPwdMinLength");
if (tmpint != 0) {
policy->min_pwd_length = tmpint;
}
policy->history_length = slapi_entry_attr_get_int(pe,
"krbPwdHistoryLength");
policy->min_complexity = slapi_entry_attr_get_int(pe,
"krbPwdMinDiffChars");
ret = 0;
done:
if (results) {
pwd_values_free(&results, &actual_type_name, buffer_flags);
@ -477,24 +494,6 @@ done:
return ret;
}
static Slapi_Value *ipapwd_strip_pw_date(Slapi_Value *pw)
{
const char *pwstr;
pwstr = slapi_value_get_string(pw);
return slapi_value_new_string(&pwstr[GENERALIZED_TIME_LENGTH]);
}
/* searches the directory and finds the policy closest to the DN */
/* return 0 on success, -1 on error or if no policy is found */
static int ipapwd_sv_pw_cmp(const void *pv1, const void *pv2)
{
const char *pw1 = slapi_value_get_string(*((Slapi_Value **)pv1));
const char *pw2 = slapi_value_get_string(*((Slapi_Value **)pv2));
return strncmp(pw1, pw2, GENERALIZED_TIME_LENGTH);
}
/*==Common-public-functions=============================================*/
@ -620,62 +619,19 @@ done:
return rc;
}
/* 90 days default pwd max lifetime */
#define IPAPWD_DEFAULT_PWDLIFE (90 * 24 *3600)
#define IPAPWD_DEFAULT_MINLEN 0
/* check password strenght and history */
int ipapwd_CheckPolicy(struct ipapwd_data *data)
{
char *krbPrincipalExpiration = NULL;
char *krbLastPwdChange = NULL;
char *krbPasswordExpiration = NULL;
int krbMaxPwdLife = IPAPWD_DEFAULT_PWDLIFE;
int krbPwdMinLength = IPAPWD_DEFAULT_MINLEN;
int krbPwdMinDiffChars = 0;
int krbMinPwdLife = 0;
int pwdCharLen = 0;
Slapi_Entry *policy = NULL;
Slapi_Attr *passwordHistory = NULL;
struct tm tm;
int tmp, ret;
char *old_pw;
struct ipapwd_policy pol = {0};
time_t acct_expiration;
time_t pwd_expiration;
time_t last_pwd_change;
char **pwd_history;
char *tmpstr;
int ret;
/* check account is not expired. Ignore unixtime = 0 (Jan 1 1970) */
krbPrincipalExpiration =
slapi_entry_attr_get_charptr(data->target, "krbPrincipalExpiration");
if (krbPrincipalExpiration &&
(strcasecmp("19700101000000Z", krbPrincipalExpiration) != 0)) {
/* if expiration date is set check it */
memset(&tm, 0, sizeof(struct tm));
ret = sscanf(krbPrincipalExpiration,
"%04u%02u%02u%02u%02u%02u",
&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
&tm.tm_hour, &tm.tm_min, &tm.tm_sec);
if (ret == 6) {
tm.tm_year -= 1900;
tm.tm_mon -= 1;
if (data->timeNow > timegm(&tm)) {
LOG_TRACE("Account Expired");
return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDMODNOTALLOWED;
}
}
/* FIXME: else error out ? */
}
slapi_ch_free_string(&krbPrincipalExpiration);
/* find the entry with the password policy */
ret = ipapwd_getPolicy(data->dn, data->target, &policy);
if (ret) {
LOG_TRACE("No password policy");
goto no_policy;
}
/* Retrieve Max History Len */
data->pwHistoryLen =
slapi_entry_attr_get_int(policy, "krbPwdHistoryLength");
pol.max_pwd_life = IPAPWD_DEFAULT_PWDLIFE;
pol.min_pwd_length = IPAPWD_DEFAULT_MINLEN;
if (data->changetype != IPA_CHANGETYPE_NORMAL) {
/* We must skip policy checks (Admin change) but
@ -685,279 +641,51 @@ int ipapwd_CheckPolicy(struct ipapwd_data *data)
data->expireTime = data->timeNow;
}
/* skip policy checks */
slapi_entry_free(policy);
goto no_policy;
}
/* first of all check current password, if any */
old_pw = slapi_entry_attr_get_charptr(data->target, "userPassword");
if (old_pw) {
Slapi_Value *cpw[2] = {NULL, NULL};
Slapi_Value *pw;
cpw[0] = slapi_value_new_string(old_pw);
pw = slapi_value_new_string(data->password);
if (!pw) {
LOG_OOM();
slapi_entry_free(policy);
slapi_ch_free_string(&old_pw);
slapi_value_free(&cpw[0]);
slapi_value_free(&pw);
return LDAP_OPERATIONS_ERROR;
}
ret = slapi_pw_find_sv(cpw, pw);
slapi_ch_free_string(&old_pw);
slapi_value_free(&cpw[0]);
slapi_value_free(&pw);
if (ret == 0) {
LOG_TRACE("Password in history\n");
slapi_entry_free(policy);
return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDINHISTORY;
}
}
krbPasswordExpiration =
slapi_entry_attr_get_charptr(data->target, "krbPasswordExpiration");
krbLastPwdChange =
slapi_entry_attr_get_charptr(data->target, "krbLastPwdChange");
/* if no previous change, it means this is probably a new account
* or imported, log and just ignore */
if (krbLastPwdChange) {
memset(&tm, 0, sizeof(struct tm));
ret = sscanf(krbLastPwdChange,
"%04u%02u%02u%02u%02u%02u",
&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
&tm.tm_hour, &tm.tm_min, &tm.tm_sec);
if (ret == 6) {
tm.tm_year -= 1900;
tm.tm_mon -= 1;
data->lastPwChange = timegm(&tm);
}
/* FIXME: *else* report an error ? */
/* do not load policies */
} else {
LOG_TRACE("Warning: Last Password Change Time is not available\n");
}
/* Check min age */
krbMinPwdLife = slapi_entry_attr_get_int(policy, "krbMinPwdLife");
/* if no default then treat it as no limit */
if (krbMinPwdLife != 0) {
/* check for reset cases */
if (krbLastPwdChange == NULL ||
((krbPasswordExpiration != NULL) &&
strcmp(krbPasswordExpiration, krbLastPwdChange) == 0)) {
/* Expiration and last change time are the same or
* missing this happens only when a password is reset
* by an admin or the account is new or no expiration
* policy is set, PASS */
LOG_TRACE("Ignore krbMinPwdLife Expiration, not enough info\n");
} else if (data->timeNow < data->lastPwChange + krbMinPwdLife) {
LOG_TRACE("Too soon to change password\n");
slapi_entry_free(policy);
slapi_ch_free_string(&krbPasswordExpiration);
slapi_ch_free_string(&krbLastPwdChange);
return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDTOOYOUNG;
/* find the entry with the password policy */
ret = ipapwd_getPolicy(data->dn, data->target, &pol);
if (ret) {
LOG_TRACE("No password policy, use defaults");
}
}
/* free strings or we leak them */
slapi_ch_free_string(&krbPasswordExpiration);
slapi_ch_free_string(&krbLastPwdChange);
tmpstr = slapi_entry_attr_get_charptr(data->target,
"krbPrincipalExpiration");
acct_expiration = ipapwd_gentime_to_time_t(tmpstr);
slapi_ch_free_string(&tmpstr);
/* Retrieve min length */
tmp = slapi_entry_attr_get_int(policy, "krbPwdMinLength");
if (tmp != 0) {
krbPwdMinLength = tmp;
}
tmpstr = slapi_entry_attr_get_charptr(data->target,
"krbPasswordExpiration");
pwd_expiration = ipapwd_gentime_to_time_t(tmpstr);
slapi_ch_free_string(&tmpstr);
/* check complexity */
/* FIXME: this code is partially based on Directory Server code,
* the plan is to merge this code later making it available
* trough a pulic DS API for slapi plugins */
krbPwdMinDiffChars =
slapi_entry_attr_get_int(policy, "krbPwdMinDiffChars");
if (krbPwdMinDiffChars != 0) {
int num_digits = 0;
int num_alphas = 0;
int num_uppers = 0;
int num_lowers = 0;
int num_specials = 0;
int num_8bit = 0;
int num_repeated = 0;
int max_repeated = 0;
int num_categories = 0;
char *p, *pwd;
tmpstr = slapi_entry_attr_get_charptr(data->target,
"krbLastPwdChange");
last_pwd_change = ipapwd_gentime_to_time_t(tmpstr);
slapi_ch_free_string(&tmpstr);
pwd = strdup(data->password);
pwd_history = slapi_entry_attr_get_charray(data->target,
"passwordHistory");
/* check character types */
p = pwd;
while (p && *p) {
if (ldap_utf8isdigit(p)) {
num_digits++;
} else if (ldap_utf8isalpha(p)) {
num_alphas++;
if (slapi_utf8isLower((unsigned char *)p)) {
num_lowers++;
} else {
num_uppers++;
}
} else {
/* check if this is an 8-bit char */
if (*p & 128) {
num_8bit++;
} else {
num_specials++;
}
}
/* check policy */
ret = ipapwd_check_policy(&pol, data->password,
data->timeNow,
acct_expiration,
pwd_expiration,
last_pwd_change,
pwd_history);
/* check for repeating characters. If this is the
first char of the password, no need to check */
if (pwd != p) {
int len = ldap_utf8len(p);
char *prev_p = ldap_utf8prev(p);
if (len == ldap_utf8len(prev_p)) {
if (memcmp(p, prev_p, len) == 0) {
num_repeated++;
if (max_repeated < num_repeated) {
max_repeated = num_repeated;
}
} else {
num_repeated = 0;
}
} else {
num_repeated = 0;
}
}
p = ldap_utf8next(p);
}
free(pwd);
p = pwd = NULL;
/* tally up the number of character categories */
if (num_digits > 0) ++num_categories;
if (num_uppers > 0) ++num_categories;
if (num_lowers > 0) ++num_categories;
if (num_specials > 0) ++num_categories;
if (num_8bit > 0) ++num_categories;
/* FIXME: the kerberos plicy schema does not define separated
* threshold values, so just treat anything as a category,
* we will fix this when we merge with DS policies */
if (max_repeated > 1) --num_categories;
if (num_categories < krbPwdMinDiffChars) {
LOG_TRACE("Password not complex enough\n");
slapi_entry_free(policy);
return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_INVALIDPWDSYNTAX;
}
}
/* Check password history */
ret = slapi_entry_attr_find(data->target,
"passwordHistory", &passwordHistory);
if (ret == 0) {
int err, hint, count, i, j;
const char *pwstr;
Slapi_Value **pH;
Slapi_Value *pw;
hint = 0;
count = 0;
err = slapi_attr_get_numvalues(passwordHistory, &count);
/* check history only if we have one */
if (count > 0 && data->pwHistoryLen > 0) {
pH = calloc(count + 2, sizeof(Slapi_Value *));
if (!pH) {
LOG_OOM();
slapi_entry_free(policy);
return LDAP_OPERATIONS_ERROR;
}
i = 0;
hint = slapi_attr_first_value(passwordHistory, &pw);
while (hint != -1) {
pwstr = slapi_value_get_string(pw);
/* if shorter than GENERALIZED_TIME_LENGTH, it
* is garbage, we never set timeless entries */
if (pwstr &&
(strlen(pwstr) > GENERALIZED_TIME_LENGTH)) {
pH[i] = pw;
i++;
}
hint = slapi_attr_next_value(passwordHistory, hint, &pw);
}
qsort(pH, i, sizeof(Slapi_Value *), ipapwd_sv_pw_cmp);
if (i > data->pwHistoryLen) {
i = data->pwHistoryLen;
pH[i] = NULL;
}
for (j = 0; pH[j]; j++) {
pH[j] = ipapwd_strip_pw_date(pH[j]);
}
pw = slapi_value_new_string(data->password);
if (!pw) {
LOG_OOM();
slapi_entry_free(policy);
free(pH);
return LDAP_OPERATIONS_ERROR;
}
err = slapi_pw_find_sv(pH, pw);
for (j = 0; pH[j]; j++) {
slapi_value_free(&pH[j]);
}
slapi_value_free(&pw);
free(pH);
if (err == 0) {
LOG_TRACE("Password in history\n");
slapi_entry_free(policy);
return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDINHISTORY;
}
}
}
/* Calculate max age */
tmp = slapi_entry_attr_get_int(policy, "krbMaxPwdLife");
if (tmp != 0) {
krbMaxPwdLife = tmp;
}
slapi_entry_free(policy);
no_policy:
/* check min lenght */
pwdCharLen = ldap_utf8characters(data->password);
if (pwdCharLen < krbPwdMinLength) {
LOG_TRACE("Password too short (%d < %d)\n",
pwdCharLen, krbPwdMinLength);
return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDTOOSHORT;
}
slapi_ch_array_free(pwd_history);
if (data->expireTime == 0) {
data->expireTime = data->timeNow + krbMaxPwdLife;
data->expireTime = data->timeNow + pol.max_pwd_life;
}
return IPAPWD_POLICY_OK;
data->policy = pol;
return ret;
}
/* Searches the dn in directory,
@ -1154,10 +882,12 @@ int ipapwd_SetPassword(struct ipapwd_krbcfg *krbcfg,
"userPassword", data->password);
/* set password history */
pwvals = ipapwd_setPasswordHistory(smods, data);
if (pwvals) {
slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE,
"passwordHistory", pwvals);
if (data->policy.history_length > 0) {
pwvals = ipapwd_setPasswordHistory(smods, data);
if (pwvals) {
slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE,
"passwordHistory", pwvals);
}
}
/* FIXME:
@ -1186,111 +916,45 @@ Slapi_Value **ipapwd_setPasswordHistory(Slapi_Mods *smods,
struct ipapwd_data *data)
{
Slapi_Value **pH = NULL;
Slapi_Attr *passwordHistory = NULL;
char timestr[GENERALIZED_TIME_LENGTH+1];
char *histr, *old_pw;
struct tm utctime;
int ret, pc;
char **pwd_history = NULL;
char **new_pwd_history = NULL;
int n = 0;
int ret;
int i;
old_pw = slapi_entry_attr_get_charptr(data->target, "userPassword");
if (!old_pw) {
/* no old password to store, just return */
return NULL;
pwd_history = slapi_entry_attr_get_charray(data->target,
"passwordHistory");
ret = ipapwd_generate_new_history(data->password, data->timeNow,
data->policy.history_length,
pwd_history, &new_pwd_history, &n);
if (ret) {
LOG_FATAL("failed to generate new password history!\n");
goto done;
}
if (!gmtime_r(&(data->timeNow), &utctime)) {
LOG_FATAL("failed to retrieve current date (buggy gmtime_r ?)\n");
return NULL;
}
strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime);
histr = slapi_ch_smprintf("%s%s", timestr, old_pw);
if (!histr) {
pH = (Slapi_Value **)slapi_ch_calloc(n + 1, sizeof(Slapi_Value *));
if (!pH) {
LOG_OOM();
return NULL;
goto done;
}
/* retrieve current history */
ret = slapi_entry_attr_find(data->target,
"passwordHistory", &passwordHistory);
if (ret == 0) {
int err, hint, count, i, j;
const char *pwstr;
Slapi_Value *pw;
hint = 0;
count = 0;
err = slapi_attr_get_numvalues(passwordHistory, &count);
/* if we have one */
if (err == 0 && count > 0 && data->pwHistoryLen > 0) {
pH = calloc(count + 2, sizeof(Slapi_Value *));
if (!pH) {
LOG_OOM();
free(histr);
return NULL;
}
i = 0;
hint = slapi_attr_first_value(passwordHistory, &pw);
while (hint != -1) {
pwstr = slapi_value_get_string(pw);
/* if shorter than GENERALIZED_TIME_LENGTH, it
* is garbage, we never set timeless entries */
if (pwstr &&
(strlen(pwstr) > GENERALIZED_TIME_LENGTH)) {
pH[i] = pw;
i++;
}
hint = slapi_attr_next_value(passwordHistory, hint, &pw);
}
qsort(pH, i, sizeof(Slapi_Value *), ipapwd_sv_pw_cmp);
if (i >= data->pwHistoryLen) {
/* need to rotate out the first entry */
for (j = 0; j < data->pwHistoryLen; j++) {
pH[j] = pH[j + 1];
}
i = data->pwHistoryLen;
pH[i] = NULL;
i--;
}
pc = i;
/* copy only interesting entries */
for (i = 0; i < pc; i++) {
pH[i] = slapi_value_dup(pH[i]);
if (pH[i] == NULL) {
LOG_OOM();
while (i) {
i--;
slapi_value_free(&pH[i]);
}
free(pH);
free(histr);
return NULL;
}
}
}
}
if (pH == NULL) {
pH = calloc(2, sizeof(Slapi_Value *));
if (!pH) {
for (i = 0; i < n; i++) {
pH[i] = slapi_value_new_string(new_pwd_history[i]);
if (!pH[i]) {
ipapwd_free_slapi_value_array(&pH);
LOG_OOM();
free(histr);
return NULL;
goto done;
}
pc = 0;
}
/* add new history value */
pH[pc] = slapi_value_new_string(histr);
free(histr);
done:
slapi_ch_array_free(pwd_history);
for (i = 0; i < n; i++) {
free(new_pwd_history[i]);
}
free(new_pwd_history);
return pH;
}