2025-08-12 22:28:56 +02:00

514 lines
15 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) 2022-2023 Red Hat, Inc.
* All rights reserved.
* END COPYRIGHT BLOCK **/
/**
* IPA Graceperiod plug-in
*
* Limit LDAP operations to password changes while in the grace period.
*
*/
#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 <krb5.h>
#include "util.h"
#include "ipa_pwd.h"
#define IPAGRACEPERIOD_PLUGIN_NAME "ipa-graceperiod-plugin"
#define IPAGRACEPERIOD_PLUGIN_VERSION 0x00010000
#define IPA_PLUGIN_NAME IPAGRACEPERIOD_PLUGIN_NAME
#define IPAGRACEPERIOD_FEATURE_DESC "IPA Graceperiod"
#define IPAGRACEPERIOD_PLUGIN_DESC "IPA Graceperiod plugin"
#define IPAGRACEPERIOD_PREOP_DESC "IPA Graceperiod preop plugin"
static Slapi_PluginDesc pdesc = {
IPAGRACEPERIOD_FEATURE_DESC,
"Red Hat, Inc.",
"1.0",
IPAGRACEPERIOD_PLUGIN_DESC
};
struct ipa_context {
bool disable_last_success;
bool disable_lockout;
};
static void *_PluginID = NULL;
static int g_plugin_started = 0;
#if 0
static struct ipa_context *global_ipactx = NULL;
#endif
static char *ipa_global_policy = NULL;
int ipagraceperiod_getpolicy(Slapi_Entry *target_entry, Slapi_Entry **policy_entry,
Slapi_ValueSet** values, char **actual_type_name,
const char **policy_dn, int *attr_free_flags,
char **errstr);
int ipagraceperiod_version(void);
static void *getPluginID(void);
static void setPluginID(void *pluginID);
#define GENERALIZED_TIME_LENGTH 15
/**
*
* management functions
*
*/
int ipagraceperiod_init(Slapi_PBlock * pb);
static int ipagraceperiod_start(Slapi_PBlock * pb);
static int ipagraceperiod_close(Slapi_PBlock * pb);
static int ipagraceperiod_preop_init(Slapi_PBlock * pb);
static int ipagraceperiod_get_global_config(void);
/**
*
* the ops (where the real work is done)
*
*/
static int ipagraceperiod_preop(Slapi_PBlock *pb);
/**
*
* Get the plug-in version
*
*/
int ipagraceperiod_version(void)
{
return IPAGRACEPERIOD_PLUGIN_VERSION;
}
/**
* Plugin identity mgmt
*/
static void setPluginID(void *pluginID)
{
_PluginID = pluginID;
}
static void *getPluginID(void)
{
return _PluginID;
}
static int
ipagraceperiod_get_global_config(void)
{
char *dn = NULL;
char *basedn = NULL;
char *realm = NULL;
Slapi_DN *sdn;
Slapi_Entry *config_entry = NULL;
krb5_context krbctx = NULL;
krb5_error_code krberr;
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);
config_entry = NULL;
if (!basedn) {
goto done;
}
krberr = krb5_init_context(&krbctx);
if (krberr) {
LOG_FATAL("krb5_init_context failed (%d)\n", krberr);
/* Yes, we failed, but it is because /etc/krb5.conf doesn't exist
* or is misconfigured. Start up in a degraded mode.
*/
} else {
krberr = krb5_get_default_realm(krbctx, &realm);
if (krberr) {
LOG_FATAL("Failed to get default realm (%d)\n", krberr);
} else {
ipa_global_policy =
slapi_ch_smprintf("cn=global_policy,cn=%s,cn=kerberos,%s",
realm, basedn);
if (!ipa_global_policy) {
LOG_OOM();
ret = LDAP_OPERATIONS_ERROR;
goto done;
}
}
}
ret = 0;
done:
if (config_entry)
slapi_entry_free(config_entry);
free(realm);
krb5_free_context(krbctx);
free(dn);
free(basedn);
return ret;
}
int ipagraceperiod_getpolicy(Slapi_Entry *target_entry, Slapi_Entry **policy_entry,
Slapi_ValueSet** values, char **actual_type_name,
const char **policy_dn, int *attr_free_flags,
char **errstr)
{
int ldrc = 0;
int type_name_disposition = 0;
Slapi_DN *pdn = NULL;
/* Only continue if there is a password policy */
ldrc = slapi_vattr_values_get(target_entry, "krbPwdPolicyReference",
values,
&type_name_disposition, actual_type_name,
SLAPI_VIRTUALATTRS_REQUEST_POINTERS,
attr_free_flags);
if (ldrc == 0) {
Slapi_Value *sv = NULL;
if (values != NULL) {
slapi_valueset_first_value(*values, &sv);
*policy_dn = slapi_value_get_string(sv);
}
} else {
*policy_dn = ipa_global_policy;
}
if (*policy_dn == NULL) {
LOG_TRACE("No kerberos password policy\n");
return LDAP_SUCCESS;
} 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.";
return LDAP_OPERATIONS_ERROR;
}
}
return LDAP_SUCCESS;
}
int
ipagraceperiod_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 *) ipagraceperiod_start) != 0 ||
slapi_pblock_set(pb, SLAPI_PLUGIN_CLOSE_FN,
(void *) ipagraceperiod_close) != 0 ||
slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION,
(void *) &pdesc) != 0 ||
slapi_register_plugin("preoperation",
1,
"ipagraceperiod_init",
ipagraceperiod_preop_init,
IPAGRACEPERIOD_PREOP_DESC,
NULL,
plugin_identity
)
) {
LOG_FATAL("failed to register plugin\n");
status = EFAIL;
}
LOG_TRACE("<--out--\n");
return status;
}
static int
ipagraceperiod_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 *) ipagraceperiod_preop) != 0) {
status = EFAIL;
}
return status;
}
static int
ipagraceperiod_start(Slapi_PBlock * pb)
{
LOG_TRACE("--in-->\n");
/* Check if we're already started */
if (g_plugin_started) {
goto done;
}
g_plugin_started = 1;
ipagraceperiod_get_global_config();
LOG("ready for service\n");
done:
LOG_TRACE("<--out--\n");
return EOK;
}
static int
ipagraceperiod_close(Slapi_PBlock * pb)
{
LOG_TRACE( "--in-->\n");
slapi_ch_free_string(&ipa_global_policy);
LOG_TRACE("<--out--\n");
return EOK;
}
/*
* In the pre-op stage the bind hasn't occurred yet. It is here that
* we do the lockout enforcement.
*/
static int ipagraceperiod_preop(Slapi_PBlock *pb)
{
char *dn = NULL;
const char *policy_dn = NULL;
Slapi_Entry *target_entry = NULL;
Slapi_Entry *policy_entry = NULL;
Slapi_Value *objectclass = NULL;
Slapi_DN *sdn = NULL;
char *errstr = NULL;
int ldrc = 0;
int rc = 0;
int ret = LDAP_SUCCESS;
char *actual_type_name = NULL;
int attr_free_flags = 0;
Slapi_ValueSet *values = NULL;
long grace_limit = 0;
int grace_user_time;
char *tmpstr = NULL;
time_t pwd_expiration;
int pwresponse_requested = 0;
Slapi_PBlock *pbtm = NULL;
Slapi_Mods *smods = NULL;
LOG_TRACE("--in-->\n");
/* Just bail if we aren't ready to service requests yet. */
if (!g_plugin_started) {
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_TRACE("Failed to retrieve entry \"%s\": %d\n", dn, ldrc);
goto done;
}
/* Only deal with users and sysaccount entries */
objectclass = slapi_value_new_string("posixAccount");
if ((slapi_entry_attr_has_syntax_value(target_entry, SLAPI_ATTR_OBJECTCLASS, objectclass)) != 1) {
LOG_TRACE("Not a posix user\n");
slapi_value_free(&objectclass);
objectclass = slapi_value_new_string("simplesecurityobject");
if ((slapi_entry_attr_has_syntax_value(target_entry, SLAPI_ATTR_OBJECTCLASS, objectclass)) != 1) {
LOG_TRACE("Not a sysaccount user\n");
slapi_value_free(&objectclass);
goto done;
}
}
slapi_value_free(&objectclass);
tmpstr = slapi_entry_attr_get_charptr(target_entry, "krbPasswordExpiration");
if (tmpstr == NULL) {
/* No expiration means nothing to do */
LOG_TRACE("No krbPasswordExpiration for %s, nothing to do\n", dn);
goto done;
}
pwd_expiration = ipapwd_gentime_to_time_t(tmpstr);
if (pwd_expiration > time(NULL)) {
/* Not expired, nothing to see here */
goto done;
}
ldrc = ipagraceperiod_getpolicy(target_entry, &policy_entry,
&values, &actual_type_name,
&policy_dn, &attr_free_flags,
&errstr);
if (ldrc != LDAP_SUCCESS || policy_dn == NULL) {
goto done;
}
slapi_pblock_get(pb, SLAPI_PWPOLICY, &pwresponse_requested);
/* This returns 0 if the attribute doesn't exist, so no grace but
* report that logins are not allowed.
*/
grace_limit = slapi_entry_attr_get_int(policy_entry, "passwordGraceLimit");
/* -1 means disable grace limit */
if (grace_limit == -1) {
LOG_TRACE("grace limit disabled, skipping\n");
goto done;
} else if (grace_limit < -1) {
LOG_FATAL("Invalid passwordGraceLimit value %ld\n", grace_limit);
return LDAP_OPERATIONS_ERROR;
}
grace_user_time = slapi_entry_attr_get_int(target_entry, "passwordGraceUserTime");
if ((grace_limit > 0) && (grace_user_time < grace_limit)) {
char graceUserTime[16] = {0};
grace_user_time++;
sprintf(graceUserTime, "%d", grace_user_time);
smods = slapi_mods_new();
slapi_mods_add_string(smods, LDAP_MOD_REPLACE,
"passwordGraceUserTime", graceUserTime);
pbtm = slapi_pblock_new();
slapi_modify_internal_set_pb(pbtm,
slapi_entry_get_dn_const(target_entry),
slapi_mods_get_ldapmods_byref(smods),
NULL, NULL, getPluginID(), 0);
slapi_modify_internal_pb(pbtm);
slapi_pblock_get(pbtm, 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));
}
if (pwresponse_requested) {
slapi_pwpolicy_make_response_control(pb, -1, grace_limit - grace_user_time , -1);
}
} else if (grace_user_time >= grace_limit) {
LOG_PWDPOLICY("%s password is expired and out of grace limit\n", dn);
errstr = "Password is expired.\n";
ret = LDAP_INVALID_CREDENTIALS;
if (pwresponse_requested) {
slapi_pwpolicy_make_response_control(pb, -1, 0, LDAP_PWPOLICY_PWDEXPIRED);
}
goto done;
}
slapi_add_pwd_control(pb, LDAP_CONTROL_PWEXPIRED, 0);
done:
slapi_pblock_destroy(pbtm);
slapi_mods_free(&smods);
slapi_entry_free(target_entry);
slapi_entry_free(policy_entry);
if (values != NULL) {
slapi_vattr_values_free(&values, &actual_type_name, attr_free_flags);
}
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);
}