diff --git a/ACI.txt b/ACI.txt index e575d82a3..54488f288 100644 --- a/ACI.txt +++ b/ACI.txt @@ -241,9 +241,9 @@ aci: (targetfilter = "(|(objectclass=ipapwdpolicy)(objectclass=krbpwdpolicy))")( dn: cn=IPA.EXAMPLE,cn=kerberos,dc=ipa,dc=example aci: (targetfilter = "(|(objectclass=ipapwdpolicy)(objectclass=krbpwdpolicy))")(version 3.0;acl "permission:System: Delete Group Password Policy";allow (delete) groupdn = "ldap:///cn=System: Delete Group Password Policy,cn=permissions,cn=pbac,dc=ipa,dc=example";) dn: cn=IPA.EXAMPLE,cn=kerberos,dc=ipa,dc=example -aci: (targetattr = "ipapwddictcheck || ipapwdmaxrepeat || ipapwdmaxsequence || ipapwdusercheck || krbmaxpwdlife || krbminpwdlife || krbpwdfailurecountinterval || krbpwdhistorylength || krbpwdlockoutduration || krbpwdmaxfailure || krbpwdmindiffchars || krbpwdminlength")(targetfilter = "(|(objectclass=ipapwdpolicy)(objectclass=krbpwdpolicy))")(version 3.0;acl "permission:System: Modify Group Password Policy";allow (write) groupdn = "ldap:///cn=System: Modify Group Password Policy,cn=permissions,cn=pbac,dc=ipa,dc=example";) +aci: (targetattr = "ipapwddictcheck || ipapwdmaxrepeat || ipapwdmaxsequence || ipapwdusercheck || krbmaxpwdlife || krbminpwdlife || krbpwdfailurecountinterval || krbpwdhistorylength || krbpwdlockoutduration || krbpwdmaxfailure || krbpwdmindiffchars || krbpwdminlength || passwordgracelimit")(targetfilter = "(|(objectclass=ipapwdpolicy)(objectclass=krbpwdpolicy))")(version 3.0;acl "permission:System: Modify Group Password Policy";allow (write) groupdn = "ldap:///cn=System: Modify Group Password Policy,cn=permissions,cn=pbac,dc=ipa,dc=example";) dn: cn=IPA.EXAMPLE,cn=kerberos,dc=ipa,dc=example -aci: (targetattr = "cn || cospriority || createtimestamp || entryusn || ipapwddictcheck || ipapwdmaxrepeat || ipapwdmaxsequence || ipapwdusercheck || krbmaxpwdlife || krbminpwdlife || krbpwdfailurecountinterval || krbpwdhistorylength || krbpwdlockoutduration || krbpwdmaxfailure || krbpwdmindiffchars || krbpwdminlength || modifytimestamp || objectclass")(targetfilter = "(|(objectclass=ipapwdpolicy)(objectclass=krbpwdpolicy))")(version 3.0;acl "permission:System: Read Group Password Policy";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Group Password Policy,cn=permissions,cn=pbac,dc=ipa,dc=example";) +aci: (targetattr = "cn || cospriority || createtimestamp || entryusn || ipapwddictcheck || ipapwdmaxrepeat || ipapwdmaxsequence || ipapwdusercheck || krbmaxpwdlife || krbminpwdlife || krbpwdfailurecountinterval || krbpwdhistorylength || krbpwdlockoutduration || krbpwdmaxfailure || krbpwdmindiffchars || krbpwdminlength || modifytimestamp || objectclass || passwordgracelimit")(targetfilter = "(|(objectclass=ipapwdpolicy)(objectclass=krbpwdpolicy))")(version 3.0;acl "permission:System: Read Group Password Policy";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Group Password Policy,cn=permissions,cn=pbac,dc=ipa,dc=example";) dn: cn=radiusproxy,dc=ipa,dc=example aci: (targetattr = "cn || createtimestamp || description || entryusn || ipatokenradiusretries || ipatokenradiusserver || ipatokenradiustimeout || ipatokenusermapattribute || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipatokenradiusconfiguration)")(version 3.0;acl "permission:System: Read Radius Servers";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Radius Servers,cn=permissions,cn=pbac,dc=ipa,dc=example";) dn: cn=Realm Domains,cn=ipa,cn=etc,dc=ipa,dc=example diff --git a/API.txt b/API.txt index 0b540c6ff..4e20650ea 100644 --- a/API.txt +++ b/API.txt @@ -4058,7 +4058,7 @@ output: Entry('result') output: Output('summary', type=[, ]) output: PrimaryKey('value') command: pwpolicy_add/1 -args: 1,18,3 +args: 1,19,3 arg: Str('cn', cli_name='group') option: Str('addattr*', cli_name='addattr') option: Flag('all', autofill=True, cli_name='all', default=False) @@ -4075,6 +4075,7 @@ option: Int('krbpwdlockoutduration?', cli_name='lockouttime') option: Int('krbpwdmaxfailure?', cli_name='maxfail') option: Int('krbpwdmindiffchars?', cli_name='minclasses') option: Int('krbpwdminlength?', cli_name='minlength') +option: Int('passwordgracelimit?', cli_name='gracelimit', default=0) option: Flag('raw', autofill=True, cli_name='raw', default=False) option: Str('setattr*', cli_name='setattr') option: Str('version?') @@ -4090,7 +4091,7 @@ output: Output('result', type=[]) output: Output('summary', type=[, ]) output: ListOfPrimaryKeys('value') command: pwpolicy_find/1 -args: 1,20,4 +args: 1,21,4 arg: Str('criteria?') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('cn?', autofill=False, cli_name='group') @@ -4107,6 +4108,7 @@ option: Int('krbpwdlockoutduration?', autofill=False, cli_name='lockouttime') option: Int('krbpwdmaxfailure?', autofill=False, cli_name='maxfail') option: Int('krbpwdmindiffchars?', autofill=False, cli_name='minclasses') option: Int('krbpwdminlength?', autofill=False, cli_name='minlength') +option: Int('passwordgracelimit?', autofill=False, cli_name='gracelimit', default=0) option: Flag('pkey_only?', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) option: Int('sizelimit?', autofill=False) @@ -4117,7 +4119,7 @@ output: ListOfEntries('result') output: Output('summary', type=[, ]) output: Output('truncated', type=[]) command: pwpolicy_mod/1 -args: 1,20,3 +args: 1,21,3 arg: Str('cn?', cli_name='group') option: Str('addattr*', cli_name='addattr') option: Flag('all', autofill=True, cli_name='all', default=False) @@ -4135,6 +4137,7 @@ option: Int('krbpwdlockoutduration?', autofill=False, cli_name='lockouttime') option: Int('krbpwdmaxfailure?', autofill=False, cli_name='maxfail') option: Int('krbpwdmindiffchars?', autofill=False, cli_name='minclasses') option: Int('krbpwdminlength?', autofill=False, cli_name='minlength') +option: Int('passwordgracelimit?', autofill=False, cli_name='gracelimit', default=0) option: Flag('raw', autofill=True, cli_name='raw', default=False) option: Flag('rights', autofill=True, default=False) option: Str('setattr*', cli_name='setattr') diff --git a/VERSION.m4 b/VERSION.m4 index 1f07faeb7..c2173a821 100644 --- a/VERSION.m4 +++ b/VERSION.m4 @@ -86,8 +86,8 @@ define(IPA_DATA_VERSION, 20100614120000) # # ######################################################## define(IPA_API_VERSION_MAJOR, 2) -# Last change: add idp API -define(IPA_API_VERSION_MINOR, 247) +# Last change: add graceperiodlimit +define(IPA_API_VERSION_MINOR, 248) ######################################################## # Following values are auto-generated from values above diff --git a/configure.ac b/configure.ac index 8cce77e98..665aeda51 100644 --- a/configure.ac +++ b/configure.ac @@ -646,6 +646,7 @@ AC_CONFIG_FILES([ daemons/ipa-slapi-plugins/ipa-cldap/Makefile daemons/ipa-slapi-plugins/ipa-dns/Makefile daemons/ipa-slapi-plugins/ipa-enrollment/Makefile + daemons/ipa-slapi-plugins/ipa-graceperiod/Makefile daemons/ipa-slapi-plugins/ipa-lockout/Makefile daemons/ipa-slapi-plugins/ipa-otp-counter/Makefile daemons/ipa-slapi-plugins/ipa-otp-lasttoken/Makefile diff --git a/daemons/ipa-slapi-plugins/Makefile.am b/daemons/ipa-slapi-plugins/Makefile.am index 216b2ad9a..cdc02a0f7 100644 --- a/daemons/ipa-slapi-plugins/Makefile.am +++ b/daemons/ipa-slapi-plugins/Makefile.am @@ -5,6 +5,7 @@ SUBDIRS = \ ipa-cldap \ ipa-dns \ ipa-enrollment \ + ipa-graceperiod \ ipa-lockout \ ipa-modrdn \ ipa-otp-counter \ diff --git a/daemons/ipa-slapi-plugins/ipa-graceperiod/Makefile.am b/daemons/ipa-slapi-plugins/ipa-graceperiod/Makefile.am new file mode 100644 index 000000000..5836a3e5f --- /dev/null +++ b/daemons/ipa-slapi-plugins/ipa-graceperiod/Makefile.am @@ -0,0 +1,42 @@ +NULL = + +PLUGIN_COMMON_DIR = $(srcdir)/../common + +AM_CPPFLAGS = \ + -I$(srcdir) \ + -I$(PLUGIN_COMMON_DIR) \ + -DPREFIX=\""$(prefix)"\" \ + -DBINDIR=\""$(bindir)"\" \ + -DLIBDIR=\""$(libdir)"\" \ + -DLIBEXECDIR=\""$(libexecdir)"\" \ + -DDATADIR=\""$(datadir)"\" \ + -I$(top_srcdir)/util \ + $(DIRSRV_CFLAGS) \ + $(LDAP_CFLAGS) \ + $(WARN_CFLAGS) \ + $(NULL) + +plugindir = $(libdir)/dirsrv/plugins +plugin_LTLIBRARIES = \ + libipa_graceperiod.la \ + $(NULL) + +libipa_graceperiod_la_SOURCES = \ + ipa_graceperiod.c \ + $(NULL) + +libipa_graceperiod_la_LDFLAGS = -avoid-version + +libipa_graceperiod_la_LIBADD = \ + $(LDAP_LIBS) \ + $(top_builddir)/util/libutil.la \ + $(NULL) + +appdir = $(IPA_DATA_DIR) +app_DATA = \ + graceperiod-conf.ldif \ + $(NULL) + +EXTRA_DIST = \ + $(app_DATA) \ + $(NULL) diff --git a/daemons/ipa-slapi-plugins/ipa-graceperiod/graceperiod-conf.ldif b/daemons/ipa-slapi-plugins/ipa-graceperiod/graceperiod-conf.ldif new file mode 100644 index 000000000..94ad20631 --- /dev/null +++ b/daemons/ipa-slapi-plugins/ipa-graceperiod/graceperiod-conf.ldif @@ -0,0 +1,15 @@ +dn: cn=IPA Graceperiod,cn=plugins,cn=config +changetype: add +objectclass: top +objectclass: nsSlapdPlugin +objectclass: extensibleObject +cn: IPA Graceperiod +nsslapd-pluginpath: libipa_graceperiod +nsslapd-plugininitfunc: ipagraceperiod_init +nsslapd-plugintype: object +nsslapd-pluginenabled: on +nsslapd-pluginid: ipagraceperiod_version +nsslapd-pluginversion: 1.0 +nsslapd-pluginvendor: Red Hat, Inc. +nsslapd-plugindescription: IPA Graceperiod plugin +nsslapd-plugin-depends-on-type: database diff --git a/daemons/ipa-slapi-plugins/ipa-graceperiod/ipa_graceperiod.c b/daemons/ipa-slapi-plugins/ipa-graceperiod/ipa_graceperiod.c new file mode 100644 index 000000000..0860b5c20 --- /dev/null +++ b/daemons/ipa-slapi-plugins/ipa-graceperiod/ipa_graceperiod.c @@ -0,0 +1,507 @@ +/** 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 . + * + * 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 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 +#include +#include +#include +#include "slapi-plugin.h" +#include "nspr.h" +#include + +#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 *pwd_expiration = NULL; + 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); + + pwd_expiration = slapi_entry_attr_get_charptr(target_entry, "krbPasswordExpiration"); + if (pwd_expiration == NULL) { + /* No expiration means nothing to do */ + LOG_TRACE("No krbPasswordExpiration for %s, nothing to do\n", dn); + 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 %d\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_limit > 0) && (grace_user_time >= grace_limit)) { + LOG_TRACE("%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); +} diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c index b24a5ffab..afa231289 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c @@ -1071,6 +1071,7 @@ static int ipapwd_post_modadd(Slapi_PBlock *pb) struct ipapwd_krbcfg *krbcfg = NULL; char *principal = NULL; Slapi_Value *ipahost; + Slapi_Value *zero; LOG_TRACE("=>\n"); @@ -1167,6 +1168,13 @@ static int ipapwd_post_modadd(Slapi_PBlock *pb) } slapi_value_free(&ipahost); } + zero = slapi_value_new_string("0"); + if (!slapi_entry_attr_has_syntax_value(pwdop->pwdata.target, + "passwordgraceusertime", zero)) { + /* Clear the passwordgraceusertime from the user entry */ + slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "passwordgraceusertime", "0"); + } + slapi_value_free(&zero); ret = ipapwd_apply_mods(pwdop->pwdata.dn, smods); if (ret) diff --git a/freeipa.spec.in b/freeipa.spec.in index 11ec4876c..704bbfee1 100755 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -1067,6 +1067,7 @@ rm %{buildroot}/%{plugin_dir}/libipa_extdom_extop.la rm %{buildroot}/%{plugin_dir}/libipa_range_check.la rm %{buildroot}/%{plugin_dir}/libipa_otp_counter.la rm %{buildroot}/%{plugin_dir}/libipa_otp_lasttoken.la +rm %{buildroot}/%{plugin_dir}/libipa_graceperiod.la rm %{buildroot}/%{plugin_dir}/libtopology.la rm %{buildroot}/%{_libdir}/krb5/plugins/kdb/ipadb.la rm %{buildroot}/%{_libdir}/samba/pdb/ipasam.la @@ -1426,6 +1427,7 @@ fi %attr(755,root,root) %{plugin_dir}/libipa_sidgen.so %attr(755,root,root) %{plugin_dir}/libipa_sidgen_task.so %attr(755,root,root) %{plugin_dir}/libipa_extdom_extop.so +%attr(755,root,root) %{plugin_dir}/libipa_graceperiod.so %attr(755,root,root) %{_libdir}/krb5/plugins/kdb/ipadb.so %{_mandir}/man1/ipa-replica-conncheck.1* %{_mandir}/man1/ipa-replica-install.1* diff --git a/install/share/60basev2.ldif b/install/share/60basev2.ldif index 952755309..53c857d1b 100644 --- a/install/share/60basev2.ldif +++ b/install/share/60basev2.ldif @@ -58,4 +58,4 @@ attributeTypes: (2.16.840.1.113730.3.8.23.2 NAME 'ipaPwdMaxRepeat' EQUALITY inte attributeTypes: (2.16.840.1.113730.3.8.23.3 NAME 'ipaPwdMaxSequence' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4') attributeTypes: (2.16.840.1.113730.3.8.23.4 NAME 'ipaPwdDictCheck' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4') attributeTypes: (2.16.840.1.113730.3.8.23.5 NAME 'ipaPwdUserCheck' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4') -objectClasses: (2.16.840.1.113730.3.8.24.1 NAME 'ipaPwdPolicy' DESC 'IPA Password policy object class' SUP top MAY (ipaPwdMaxRepeat $ ipaPwdMaxSequence $ ipaPwdDictCheck $ ipaPwdUserCheck) X-ORIGIN 'IPA v4') +objectClasses: (2.16.840.1.113730.3.8.24.1 NAME 'ipaPwdPolicy' DESC 'IPA Password policy object class' SUP top MAY (ipaPwdMaxRepeat $ ipaPwdMaxSequence $ ipaPwdDictCheck $ ipaPwdUserCheck $ passwordGraceLimit) X-ORIGIN 'IPA v4') diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index 04d26452d..a30e9ee11 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -246,6 +246,8 @@ class DsInstance(service.Service): self.step("configuring DNS plugin", self.__config_dns_module) self.step("enabling entryUSN plugin", self.__enable_entryusn) self.step("configuring lockout plugin", self.__config_lockout_module) + self.step("configuring graceperiod plugin", + self.__config_graceperiod_module) self.step("configuring topology plugin", self.__config_topology_module) self.step("creating indices", self.__create_indices) self.step("enabling referential integrity plugin", self.__add_referint_module) @@ -751,6 +753,9 @@ class DsInstance(service.Service): def __config_lockout_module(self): self._ldap_mod("lockout-conf.ldif") + def __config_graceperiod_module(self): + self._ldap_mod("graceperiod-conf.ldif") + def __config_topology_module(self): self._ldap_mod("ipa-topology-conf.ldif", self.sub_dict) diff --git a/ipaserver/plugins/pwpolicy.py b/ipaserver/plugins/pwpolicy.py index 816faffe0..b044659d4 100644 --- a/ipaserver/plugins/pwpolicy.py +++ b/ipaserver/plugins/pwpolicy.py @@ -244,7 +244,7 @@ class pwpolicy(LDAPObject): 'krbpwdmaxfailure', 'krbpwdfailurecountinterval', 'krbpwdlockoutduration', 'ipapwdmaxrepeat', 'ipapwdmaxsequence', 'ipapwddictcheck', - 'ipapwdusercheck', + 'ipapwdusercheck', 'passwordgracelimit', ] managed_permissions = { 'System: Read Group Password Policy': { @@ -257,7 +257,7 @@ class pwpolicy(LDAPObject): 'krbpwdlockoutduration', 'krbpwdmaxfailure', 'krbpwdmindiffchars', 'krbpwdminlength', 'objectclass', 'ipapwdmaxrepeat', 'ipapwdmaxsequence', 'ipapwddictcheck', - 'ipapwdusercheck', + 'ipapwdusercheck', 'passwordgracelimit', }, 'default_privileges': { 'Password Policy Readers', @@ -285,7 +285,7 @@ class pwpolicy(LDAPObject): 'krbpwdhistorylength', 'krbpwdlockoutduration', 'krbpwdmaxfailure', 'krbpwdmindiffchars', 'krbpwdminlength', 'ipapwdmaxrepeat', 'ipapwdmaxsequence', 'ipapwddictcheck', - 'ipapwdusercheck', + 'ipapwdusercheck', 'passwordgracelimit', }, 'replaces': [ '(targetattr = "krbmaxpwdlife || krbminpwdlife || krbpwdhistorylength || krbpwdmindiffchars || krbpwdminlength || krbpwdmaxfailure || krbpwdfailurecountinterval || krbpwdlockoutduration")(target = "ldap:///cn=*,cn=$REALM,cn=kerberos,$SUFFIX")(version 3.0;acl "permission:Modify Group Password Policy";allow (write) groupdn = "ldap:///cn=Modify Group Password Policy,cn=permissions,cn=pbac,$SUFFIX";)', @@ -396,6 +396,15 @@ class pwpolicy(LDAPObject): doc=_('Check if the password contains the username'), default=False, ), + Int( + 'passwordgracelimit?', + cli_name='gracelimit', + label=_('Grace login limit'), + doc=_('Number of LDAP authentications allowed after expiration'), + minvalue=-1, + maxvalue=Int.MAX_UINT32, + default=0, + ), ) def get_dn(self, *keys, **options): diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py index 67f3d102e..448391eee 100644 --- a/ipaserver/plugins/user.py +++ b/ipaserver/plugins/user.py @@ -1189,6 +1189,9 @@ class userstatus(LDAPObject): label=_('Time now'), flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'}, ), + Str('passwordgraceusertime', + label=_('Password grace count'), + flags={'no_create', 'no_update', 'no_search'},), ) @@ -1230,7 +1233,9 @@ class user_status(LDAPQuery): def execute(self, *keys, **options): ldap = self.obj.backend dn, _oc = self.api.Object.user.get_either_dn(*keys, **options) - attr_list = ['krbloginfailedcount', 'krblastsuccessfulauth', 'krblastfailedauth', 'nsaccountlock'] + attr_list = ['krbloginfailedcount', 'krblastsuccessfulauth', + 'krblastfailedauth', 'nsaccountlock', + 'passwordgraceusertime'] disabled = False masters = get_masters(ldap) @@ -1258,6 +1263,8 @@ class user_status(LDAPQuery): for attr in ['krblastsuccessfulauth', 'krblastfailedauth']: newresult[attr] = entry.get(attr, [u'N/A']) newresult['krbloginfailedcount'] = entry.get('krbloginfailedcount', u'0') + newresult['passwordgraceusertime'] = \ + entry.get('passwordgraceusertime', u'0') if not options.get('raw', False): for attr in ['krblastsuccessfulauth', 'krblastfailedauth']: try: diff --git a/ipatests/test_integration/test_pwpolicy.py b/ipatests/test_integration/test_pwpolicy.py index 3fc2cb634..718acd7d5 100644 --- a/ipatests/test_integration/test_pwpolicy.py +++ b/ipatests/test_integration/test_pwpolicy.py @@ -20,6 +20,7 @@ class TestPWPolicy(IntegrationTest): """ Test password policy in action. """ + num_replicas = 1 topology = 'line' @@ -261,3 +262,92 @@ class TestPWPolicy(IntegrationTest): ) assert result.returncode != 0 assert 'minlength' in result.stderr_text + + def test_graceperiod_expired(self): + """Test the LDAP bind grace period""" + str(self.master.domain.basedn) + dn = "uid={user},cn=users,cn=accounts,{base_dn}".format( + user=USER, base_dn=str(self.master.domain.basedn)) + + self.master.run_command( + ["ipa", "pwpolicy-mod", POLICY, "--gracelimit", "3", ], + ) + + # Resetting the password will mark it as expired + self.reset_password(self.master) + + for i in range(2, -1, -1): + result = self.master.run_command( + ["ldapsearch", "-e", "ppolicy", "-D", dn, + "-w", PASSWORD, "-b", dn], raiseonerr=False + ) + # We're in grace, this will succeed + assert result.returncode == 0 + + # verify that we get the expected ppolicy output + assert 'Password expired, {} grace logins remain'.format(i) \ + in result.stderr_text + + # Now grace is done and binds should fail. + result = self.master.run_command( + ["ldapsearch", "-e", "ppolicy", "-D", dn, + "-w", PASSWORD, "-b", dn], raiseonerr=False + ) + assert result.returncode == 49 + + assert 'Password is expired' in result.stderr_text + assert 'Password expired, 0 grace logins remain' in result.stderr_text + + # Test that resetting the password resets the grace counter + self.reset_password(self.master) + result = tasks.ldapsearch_dm( + self.master, dn, ['passwordgraceusertime',], + ) + + assert 'passwordgraceusertime: 0' in result.stdout_text.lower() + + def test_graceperiod_not_replicated(self): + """Test that the grace period is reset on password reset""" + str(self.master.domain.basedn) + dn = "uid={user},cn=users,cn=accounts,{base_dn}".format( + user=USER, base_dn=str(self.master.domain.basedn)) + + self.master.run_command( + ["ipa", "pwpolicy-mod", POLICY, "--gracelimit", "3", ], + ) + + # Resetting the password will mark it as expired + self.reset_password(self.master) + + # Generate some logins but don't exceed the limit + for _i in range(2, -1, -1): + result = self.master.run_command( + ["ldapsearch", "-e", "ppolicy", "-D", dn, + "-w", PASSWORD, "-b", dn], raiseonerr=False + ) + + # Verify that passwordgraceusertime is not replicated + result = tasks.ldapsearch_dm( + self.master, dn, ['passwordgraceusertime',], + ) + assert 'passwordgraceusertime: 2' in result.stdout_text.lower() + + result = tasks.ldapsearch_dm( + self.replicas[0], dn, ['passwordgraceusertime',], + ) + assert 'passwordgraceusertime: 0' in result.stdout_text.lower() + + self.reset_password(self.master) + + # Resetting the password should reset passwordgraceusertime + result = tasks.ldapsearch_dm( + self.master, dn, ['passwordgraceusertime',], + ) + assert 'passwordgraceusertime: 0' in result.stdout_text.lower() + self.reset_password(self.master) + + result = tasks.ldapsearch_dm( + self.replicas[0], dn, ['passwordgraceusertime',], + ) + assert 'passwordgraceusertime: 0' in result.stdout_text.lower() + self.reset_password(self.master) diff --git a/ipatests/test_xmlrpc/test_user_plugin.py b/ipatests/test_xmlrpc/test_user_plugin.py index 34b00cb01..74d78b0c6 100644 --- a/ipatests/test_xmlrpc/test_user_plugin.py +++ b/ipatests/test_xmlrpc/test_user_plugin.py @@ -222,6 +222,7 @@ class TestUser(XMLRPC_test): krblastfailedauth=[u'N/A'], krblastsuccessfulauth=[u'N/A'], krbloginfailedcount=u'0', + passwordgraceusertime=u'0', now=isodate_re.match, server=api.env.host, ), ],