freeipa/daemons/ipa-slapi-plugins/ipa-lockout/ipa_lockout.c

764 lines
22 KiB
C

/** BEGIN COPYRIGHT BLOCK
* 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 <http://www.gnu.org/licenses/>.
*
* Additional permission under GPLv3 section 7:
*
* In the following paragraph, "GPL" means the GNU General Public
* License, version 3 or any later version, and "Non-GPL Code" means
* code that is governed neither by the GPL nor a license
* compatible with the GPL.
*
* You may link the code of this Program with Non-GPL Code and convey
* linked combinations including the two, provided that such Non-GPL
* Code only links to the code of this Program through those well
* defined interfaces identified in the file named EXCEPTION found in
* the source code files (the "Approved Interfaces"). The files of
* Non-GPL Code may instantiate templates or use macros or inline
* functions from the Approved Interfaces without causing the resulting
* work to be covered by the GPL. Only the copyright holders of this
* Program may make changes or additions to the list of Approved
* Interfaces.
*
* Copyright (C) 2010 Red Hat, Inc.
* All rights reserved.
* END COPYRIGHT BLOCK **/
/**
* IPA Lockout plug-in
*
* Update the Kerberos lockout variables on LDAP binds.
*
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <time.h>
#include "slapi-plugin.h"
#include "nspr.h"
#include "util.h"
#define IPALOCKOUT_PLUGIN_NAME "ipa-lockout-plugin"
#define IPALOCKOUT_PLUGIN_VERSION 0x00010000
#define IPA_PLUGIN_NAME IPALOCKOUT_PLUGIN_NAME
#define IPALOCKOUT_FEATURE_DESC "IPA Lockout"
#define IPALOCKOUT_PLUGIN_DESC "IPA Lockout plugin"
#define IPALOCKOUT_POSTOP_DESC "IPA Lockout postop plugin"
#define IPALOCKOUT_PREOP_DESC "IPA Lockout preop plugin"
static Slapi_PluginDesc pdesc = {
IPALOCKOUT_FEATURE_DESC,
"Red Hat, Inc.",
"1.0",
IPALOCKOUT_PLUGIN_DESC
};
struct ipa_context {
bool disable_last_success;
bool disable_lockout;
};
static void *_PluginID = NULL;
static char *_PluginDN = NULL;
static int g_plugin_started = 0;
static struct ipa_context *global_ipactx = NULL;
#define GENERALIZED_TIME_LENGTH 15
/**
*
* management functions
*
*/
int ipalockout_init(Slapi_PBlock * pb);
static int ipalockout_start(Slapi_PBlock * pb);
static int ipalockout_close(Slapi_PBlock * pb);
static int ipalockout_postop_init(Slapi_PBlock * pb);
static int ipalockout_preop_init(Slapi_PBlock * pb);
/**
*
* the ops (where the real work is done)
*
*/
static int ipalockout_postop(Slapi_PBlock *pb);
static int ipalockout_preop(Slapi_PBlock *pb);
/**
*
* Get the plug-in version
*
*/
int ipalockout_version(void)
{
return IPALOCKOUT_PLUGIN_VERSION;
}
/**
* Plugin identity mgmt
*/
void setPluginID(void *pluginID)
{
_PluginID = pluginID;
}
void *getPluginID(void)
{
return _PluginID;
}
void setPluginDN(char *pluginDN)
{
_PluginDN = pluginDN;
}
char *getPluginDN(void)
{
return _PluginDN;
}
static int
ipalockout_get_global_config(struct ipa_context *ipactx)
{
Slapi_Value *value = NULL;
Slapi_Attr *attr = NULL;
char *dn = NULL;
char *basedn = NULL;
Slapi_DN *sdn;
Slapi_Entry *config_entry;
int ret;
/* Get cn=config so we can get the default naming context */
sdn = slapi_sdn_new_dn_byref("cn=config");
ret = slapi_search_internal_get_entry(sdn, NULL, &config_entry,
getPluginID());
slapi_sdn_free(&sdn);
if (ret) {
goto done;
}
basedn = slapi_entry_attr_get_charptr(config_entry,
"nsslapd-defaultnamingcontext");
slapi_entry_free(config_entry);
if (!basedn) {
goto done;
}
ret = asprintf(&dn, "cn=ipaConfig,cn=etc,%s", basedn);
if (ret == -1) {
LOG_OOM();
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
sdn = slapi_sdn_new_dn_byref(dn);
ret = slapi_search_internal_get_entry(sdn, NULL, &config_entry,
getPluginID());
slapi_sdn_free(&sdn);
if (ret) {
goto done;
}
ret = slapi_entry_attr_find(config_entry, "ipaConfigString", &attr);
if (ret == -1) {
/* no config, nothing to do */
ret = 0;
goto done;
}
ret = slapi_attr_first_value(attr, &value);
while (ret != -1) {
const struct berval *val;
val = slapi_value_get_berval(value);
if (!val) {
ret = LDAP_OPERATIONS_ERROR;
slapi_value_free(&value);
goto done;
}
if (strncasecmp("KDC:Disable Last Success",
val->bv_val, val->bv_len) == 0) {
ipactx->disable_last_success = true;
}
else if (strncasecmp("KDC:Disable Lockout",
val->bv_val, val->bv_len) == 0) {
ipactx->disable_lockout = true;
}
ret = slapi_attr_next_value(attr, ret, &value);
}
slapi_value_free(&value);
ret = 0;
done:
if (config_entry)
slapi_entry_free(config_entry);
free(dn);
free(basedn);
return ret;
}
int
ipalockout_init(Slapi_PBlock *pb)
{
int status = EOK;
char *plugin_identity = NULL;
LOG_TRACE("--in-->\n");
slapi_pblock_get(pb, SLAPI_PLUGIN_IDENTITY, &plugin_identity);
PR_ASSERT(plugin_identity);
setPluginID(plugin_identity);
if (slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION,
SLAPI_PLUGIN_VERSION_01) != 0 ||
slapi_pblock_set(pb, SLAPI_PLUGIN_START_FN,
(void *) ipalockout_start) != 0 ||
slapi_pblock_set(pb, SLAPI_PLUGIN_CLOSE_FN,
(void *) ipalockout_close) != 0 ||
slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION,
(void *) &pdesc) != 0 ||
slapi_register_plugin("postoperation",
1,
"ipalockout_init",
ipalockout_postop_init,
IPALOCKOUT_POSTOP_DESC,
NULL,
plugin_identity
) ||
slapi_register_plugin("preoperation",
1,
"ipalockout_init",
ipalockout_preop_init,
IPALOCKOUT_PREOP_DESC,
NULL,
plugin_identity
)
) {
LOG_FATAL("failed to register plugin\n");
status = EFAIL;
}
LOG_TRACE("<--out--\n");
return status;
}
static int
ipalockout_postop_init(Slapi_PBlock *pb)
{
int status = EOK;
if (slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION,
SLAPI_PLUGIN_VERSION_01) != 0 ||
slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION,
(void *) &pdesc) != 0 ||
slapi_pblock_set(pb, SLAPI_PLUGIN_POST_BIND_FN,
(void *) ipalockout_postop) != 0) {
status = EFAIL;
}
return status;
}
static int
ipalockout_preop_init(Slapi_PBlock *pb)
{
int status = EOK;
if (slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION,
SLAPI_PLUGIN_VERSION_01) != 0 ||
slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION,
(void *) &pdesc) != 0 ||
slapi_pblock_set(pb, SLAPI_PLUGIN_PRE_BIND_FN,
(void *) ipalockout_preop) != 0) {
status = EFAIL;
}
return status;
}
static int
ipalockout_start(Slapi_PBlock * pb)
{
LOG_TRACE("--in-->\n");
/* Check if we're already started */
if (g_plugin_started) {
goto done;
}
g_plugin_started = 1;
global_ipactx = (struct ipa_context *)malloc(sizeof(*global_ipactx));
global_ipactx->disable_last_success = false;
global_ipactx->disable_lockout = false;
ipalockout_get_global_config(global_ipactx);
LOG("ready for service\n");
LOG_TRACE("<--out--\n");
done:
return EOK;
}
static int
ipalockout_close(Slapi_PBlock * pb)
{
LOG_TRACE( "--in-->\n");
LOG_TRACE("<--out--\n");
return EOK;
}
/*
* In the post-operation we know whether the bind was successful or not
* so here we handle updating the Kerberos lockout policy attributes.
*/
static int ipalockout_postop(Slapi_PBlock *pb)
{
char *dn = NULL;
char *policy_dn = NULL;
Slapi_Entry *target_entry = NULL;
Slapi_Entry *policy_entry = NULL;
Slapi_DN *sdn = NULL;
Slapi_DN *pdn = NULL;
Slapi_PBlock *pbtm = NULL;
Slapi_Mods *smods = NULL;
Slapi_Value *objectclass = NULL;
char *errstr = NULL;
int ldrc, rc = 0;
int ret = LDAP_SUCCESS;
unsigned long failedcount = 0;
char failedcountstr[32];
int failed_bind = 0;
struct tm utctime;
time_t time_now;
char timestr[GENERALIZED_TIME_LENGTH+1];
unsigned int failcnt_interval = 0;
char *lastfail = NULL;
int tries = 0;
int failure = 1;
LOG_TRACE("--in-->\n");
/* Just bail if we aren't ready to service requests yet. */
if (!g_plugin_started) {
goto done;
}
if (global_ipactx->disable_lockout) {
goto done;
}
slapi_pblock_get(pb, SLAPI_RESULT_CODE, &rc);
/* free the dn here */
if (slapi_pblock_get(pb, SLAPI_CONN_DN, &dn) != 0) {
LOG_FATAL("Error retrieving bind DN\n");
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
/* dn will be NULL on failed auth, get the target instead */
/* don't free this dn */
if (dn == NULL && rc != LDAP_SUCCESS) {
failed_bind = 1;
if (slapi_pblock_get(pb, SLAPI_BIND_TARGET, &dn) != 0) {
LOG_FATAL("Error retrieving target DN\n");
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
}
/* Client is anonymously bound */
if (dn == NULL) {
LOG_TRACE("anonymous bind\n");
goto done;
}
/* Get the entry */
sdn = slapi_sdn_new_dn_byref(dn);
if (sdn == NULL) {
LOG_OOM();
errstr = "Out of memory.\n";
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
ldrc = slapi_search_internal_get_entry(sdn, NULL, &target_entry,
getPluginID());
if (ldrc != LDAP_SUCCESS) {
LOG_FATAL("Failed to retrieve entry \"%s\": %d\n", dn, ldrc);
goto done;
}
/* Only update kerberos principal entries */
objectclass = slapi_value_new_string("krbPrincipalAux");
if ((slapi_entry_attr_has_syntax_value(target_entry, SLAPI_ATTR_OBJECTCLASS, objectclass)) != 1) {
LOG_TRACE("Not a kerberos user\n");
slapi_value_free(&objectclass);
goto done;
}
slapi_value_free(&objectclass);
/* Only update if there is a password policy */
policy_dn = slapi_entry_attr_get_charptr(target_entry, "krbPwdPolicyReference");
if (policy_dn == NULL) {
LOG_TRACE("No kerberos password policy\n");
goto done;
} else {
pdn = slapi_sdn_new_dn_byref(policy_dn);
ldrc = slapi_search_internal_get_entry(pdn, NULL, &policy_entry,
getPluginID());
slapi_sdn_free(&pdn);
if (ldrc != LDAP_SUCCESS) {
LOG_FATAL("Failed to retrieve entry \"%s\": %d\n", policy_dn, ldrc);
errstr = "Failed to retrieve account policy.";
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
}
failedcount = slapi_entry_attr_get_ulong(target_entry, "krbLoginFailedCount");
failcnt_interval = slapi_entry_attr_get_uint(policy_entry, "krbPwdFailureCountInterval");
lastfail = slapi_entry_attr_get_charptr(target_entry, "krbLastFailedAuth");
time_now = time(NULL);
if (lastfail != NULL) {
struct tm tm;
int res = 0;
memset(&tm, 0, sizeof(struct tm));
res = sscanf(lastfail,
"%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 (res == 6) {
tm.tm_year -= 1900;
tm.tm_mon -= 1;
if (time_now > timegm(&tm) + failcnt_interval) {
failedcount = 0;
}
}
}
while (tries < 5) {
smods = slapi_mods_new();
/* On failures try very hard to update the entry so that failures
* are counted properly. This involves doing a DELETE of the value
* we expect and an ADD of the new one in the same update. If the
* record has changed while we were handling the request our
* update will fail and we will try again.
*
* On a successful bind just do a replace and set failurecount to 0.
*/
if (failed_bind) {
PR_snprintf(failedcountstr, sizeof(failedcountstr), "%lu", failedcount);
if (!gmtime_r(&(time_now), &utctime)) {
errstr = "failed to parse current date (buggy gmtime_r ?)\n";
LOG_FATAL("%s", errstr);
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
strftime(timestr, GENERALIZED_TIME_LENGTH+1,
"%Y%m%d%H%M%SZ", &utctime);
slapi_mods_add_string(smods, LDAP_MOD_DELETE, "krbLoginFailedCount", failedcountstr);
failedcount += 1;
PR_snprintf(failedcountstr, sizeof(failedcountstr), "%lu", failedcount);
slapi_mods_add_string(smods, LDAP_MOD_ADD, "krbLoginFailedCount", failedcountstr);
if (lastfail)
slapi_mods_add_string(smods, LDAP_MOD_DELETE, "krbLastFailedAuth", lastfail);
slapi_mods_add_string(smods, LDAP_MOD_ADD, "krbLastFailedAuth", timestr);
} else {
PR_snprintf(failedcountstr, sizeof(failedcountstr), "%lu", 0L);
time_now = time(NULL);
if (!gmtime_r(&(time_now), &utctime)) {
errstr = "failed to parse current date (buggy gmtime_r ?)\n";
LOG_FATAL("%s", errstr);
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
strftime(timestr, GENERALIZED_TIME_LENGTH+1,
"%Y%m%d%H%M%SZ", &utctime);
slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "krbLoginFailedCount", failedcountstr);
if (!global_ipactx->disable_last_success) {
slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "krbLastSuccessfulAuth", timestr);
}
}
pbtm = slapi_pblock_new();
slapi_modify_internal_set_pb (pbtm, slapi_entry_get_dn_const(target_entry),
slapi_mods_get_ldapmods_byref(smods),
NULL, /* Controls */
NULL, /* UniqueID */
getPluginID(), /* PluginID */
0); /* Flags */
slapi_modify_internal_pb (pbtm);
slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
if (rc != LDAP_SUCCESS) {
LOG_TRACE("WARNING: modify error %d on entry '%s'\n",
rc, slapi_entry_get_dn_const(target_entry));
ldrc = slapi_search_internal_get_entry(sdn, NULL, &target_entry,
getPluginID());
if (ldrc != LDAP_SUCCESS) {
LOG_FATAL("Failed to retrieve entry \"%s\": %d\n", dn, ldrc);
goto done;
}
slapi_mods_free(&smods);
slapi_pblock_destroy(pbtm);
if (lastfail) slapi_ch_free_string(&lastfail);
smods = NULL;
pbtm = NULL;
lastfail = NULL;
tries += 1;
} else {
LOG_TRACE("<= apply mods: Successful\n");
failure = 0;
break;
}
} /* while */
if (failure) {
ret = LDAP_OPERATIONS_ERROR;
}
done:
if (!failed_bind && dn != NULL) slapi_ch_free_string(&dn);
slapi_entry_free(target_entry);
if (policy_dn) {
slapi_ch_free_string(&policy_dn);
slapi_entry_free(policy_entry);
}
if (sdn) slapi_sdn_free(&sdn);
if (lastfail) slapi_ch_free_string(&lastfail);
if (pbtm) slapi_pblock_destroy(pbtm);
if (smods) slapi_mods_free(&smods);
LOG("postop returning %d: %s\n", ret, errstr ? errstr : "success\n");
if (ret) {
slapi_send_ldap_result(pb, ret, NULL, errstr, 0, NULL);
}
LOG_TRACE("<--out--\n");
return (ret == 0 ? EOK : EFAIL);
}
/*
* In the pre-op stage the bind hasn't occurred yet. It is here that
* we do the lockout enforcement.
*/
static int ipalockout_preop(Slapi_PBlock *pb)
{
char *dn = NULL;
char *policy_dn = NULL;
Slapi_Entry *target_entry = NULL;
Slapi_Entry *policy_entry = NULL;
Slapi_DN *sdn = NULL;
Slapi_DN *pdn = NULL;
Slapi_Value *objectclass = NULL;
char *errstr = NULL;
int ldrc = 0;
int ret = LDAP_SUCCESS;
unsigned long failedcount = 0;
time_t time_now;
unsigned int failcnt_interval = 0;
unsigned int max_fail = 0;
unsigned int lockout_duration = 0;
time_t last_failed = 0;
char *lastfail = NULL;
char *unlock_time = NULL;
LOG_TRACE("--in-->\n");
/* Just bail if we aren't ready to service requests yet. */
if (!g_plugin_started) {
goto done;
}
if (global_ipactx->disable_lockout) {
goto done;
}
if (slapi_pblock_get(pb, SLAPI_BIND_TARGET, &dn) != 0) {
LOG_FATAL("Error retrieving target DN\n");
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
/* Client is anonymously bound */
if (dn == NULL) {
LOG_TRACE("anonymous bind\n");
goto done;
}
/* Get the entry */
sdn = slapi_sdn_new_dn_byref(dn);
if (sdn == NULL) {
LOG_OOM();
errstr = "Out of memory.\n";
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
ldrc = slapi_search_internal_get_entry(sdn, NULL, &target_entry,
getPluginID());
if (ldrc != LDAP_SUCCESS) {
LOG_FATAL("Failed to retrieve entry \"%s\": %d\n", dn, ldrc);
goto done;
}
/* Only handle kerberos principal entries */
objectclass = slapi_value_new_string("krbPrincipalAux");
if ((slapi_entry_attr_has_syntax_value(target_entry, SLAPI_ATTR_OBJECTCLASS, objectclass)) != 1) {
LOG_TRACE("Not a kerberos user\n");
slapi_value_free(&objectclass);
goto done;
}
slapi_value_free(&objectclass);
/* Only continue if there is a password policy */
policy_dn = slapi_entry_attr_get_charptr(target_entry, "krbPwdPolicyReference");
if (policy_dn == NULL) {
LOG_TRACE("No kerberos password policy\n");
goto done;
} else {
pdn = slapi_sdn_new_dn_byref(policy_dn);
ldrc = slapi_search_internal_get_entry(pdn, NULL, &policy_entry,
getPluginID());
slapi_sdn_free(&pdn);
if (ldrc != LDAP_SUCCESS) {
LOG_FATAL("Failed to retrieve entry \"%s\": %d\n", policy_dn, ldrc);
errstr = "Failed to retrieve account policy.";
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
}
failedcount = slapi_entry_attr_get_ulong(target_entry, "krbLoginFailedCount");
time_now = time(NULL);
failcnt_interval = slapi_entry_attr_get_uint(policy_entry, "krbPwdFailureCountInterval");
lastfail = slapi_entry_attr_get_charptr(target_entry, "krbLastFailedAuth");
unlock_time = slapi_entry_attr_get_charptr(target_entry, "krbLastAdminUnlock");
if (lastfail != NULL) {
struct tm tm;
int res = 0;
memset(&tm, 0, sizeof(struct tm));
res = sscanf(lastfail,
"%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 (res == 6) {
tm.tm_year -= 1900;
tm.tm_mon -= 1;
last_failed = timegm(&tm);
LOG("%ld > %ld ?\n",
(long)time_now, (long)last_failed + failcnt_interval);
LOG("diff %ld\n",
(long)((last_failed + failcnt_interval) - time_now));
if (time_now > last_failed + failcnt_interval) {
failedcount = 0;
}
}
if (unlock_time) {
time_t unlock;
memset(&tm, 0, sizeof(struct tm));
res = sscanf(lastfail,
"%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 (res == 6) {
tm.tm_year -= 1900;
tm.tm_mon -= 1;
unlock = timegm(&tm);
if (last_failed <= unlock) {
goto done;
}
}
slapi_ch_free_string(&unlock_time);
}
slapi_ch_free_string(&lastfail);
}
max_fail = slapi_entry_attr_get_uint(policy_entry, "krbPwdMaxFailure");
if (max_fail == 0) {
goto done;
}
lockout_duration = slapi_entry_attr_get_uint(policy_entry, "krbPwdLockoutDuration");
if (lockout_duration == 0) {
errstr = "Entry permanently locked.\n";
ret = LDAP_UNWILLING_TO_PERFORM;
goto done;
}
if (failedcount > max_fail) {
if (time_now < last_failed + lockout_duration) {
/* Too many failures */
LOG_TRACE("Too many failed logins. %lu out of %d\n", failedcount, max_fail);
errstr = "Too many failed logins.\n";
ret = LDAP_UNWILLING_TO_PERFORM;
}
}
done:
slapi_entry_free(target_entry);
slapi_entry_free(policy_entry);
if (policy_dn) slapi_ch_free_string(&policy_dn);
if (sdn) slapi_sdn_free(&sdn);
LOG("preop returning %d: %s\n", ret, errstr ? errstr : "success\n");
if (ret) {
slapi_send_ldap_result(pb, ret, NULL, errstr, 0, NULL);
}
LOG_TRACE("<--out--\n");
return (ret == 0 ? EOK : EFAIL);
}