From: Arran Cudbard-Bell Date: Fri, 1 Sep 2023 01:04:03 +0000 (-0600) Subject: ldap: Add support for custom access attribute negation values X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8a9eba69e7cae35ee1d24b6d1fefc27428f0fbc9;p=thirdparty%2Ffreeradius-server.git ldap: Add support for custom access attribute negation values Add support for using different profiles when the user is suspended, which is extremely common where suspension will place a user in a walled garden state. --- diff --git a/doc/schemas/ldap/openldap/freeradius-radius.schema b/doc/schemas/ldap/openldap/freeradius-radius.schema index 4bbd90acbaf..09030859de3 100644 --- a/doc/schemas/ldap/openldap/freeradius-radius.schema +++ b/doc/schemas/ldap/openldap/freeradius-radius.schema @@ -298,7 +298,14 @@ attributetype ( 1.3.6.1.4.1.11344.4.2.2.1.49 SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) -attributetype ( 1.3.6.1.4.1.11344.4.2.2.1.50 + attributetype ( 1.3.6.1.4.1.11344.4.2.2.1.50 + NAME 'radiusProfileSuspendedDN' + EQUALITY distinguishedNameMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 + ) + +attributetype ( 1.3.6.1.4.1.11344.4.2.2.1.51 NAME 'radiusProxyToRealm' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch @@ -525,7 +532,7 @@ objectclass ( 1.3.6.1.4.1.11344.4.2.2.2.1 NAME 'radiusProfile' SUP top AUXILIARY - MAY ( radiusArapFeatures $ radiusArapSecurity $ radiusArapZoneAccess $ radiusAuthType $ radiusCallbackId $ radiusCallbackNumber $ radiusCalledStationId $ radiusCallingStationId $ radiusClass $ radiusClientIPAddress $ radiusFilterId $ radiusFramedAppleTalkLink $ radiusFramedAppleTalkNetwork $ radiusFramedAppleTalkZone $ radiusFramedCompression $ radiusFramedIPAddress $ radiusFramedIPNetmask $ radiusFramedIPXNetwork $ radiusFramedMTU $ radiusFramedProtocol $ radiusAttribute $ radiusFramedRoute $ radiusFramedRouting $ radiusIdleTimeout $ radiusGroupName $ radiusHint $ radiusHuntgroupName $ radiusLoginIPHost $ radiusLoginLATGroup $ radiusLoginLATNode $ radiusLoginLATPort $ radiusLoginLATService $ radiusLoginService $ radiusLoginTCPPort $ radiusLoginTime $ radiusPasswordRetry $ radiusPortLimit $ radiusPrompt $ radiusProxyToRealm $ radiusRealm $ radiusServiceType $ radiusSessionTimeout $ radiusStripUserName $ radiusTerminationAction $ radiusTunnelClientEndpoint $ radiusProfileDN $ radiusSimultaneousUse $ radiusTunnelAssignmentId $ radiusTunnelMediumType $ radiusTunnelPassword $ radiusTunnelPreference $ radiusTunnelPrivateGroupId $ radiusTunnelServerEndpoint $ radiusTunnelType $ radiusUserCategory $ radiusVSA $ radiusExpiration $ dialupAccess $ radiusNASIpAddress $ radiusReplyMessage $ radiusFramedIPv6Address $ radiusDNSServerIPv6Address $ radiusRouteIPv6Information $ radiusDelegatedIPv6PrefixPool $ radiusStatefulIPv6AddressPool $ radiusControlAttribute $ radiusReplyAttribute $ radiusRequestAttribute ) + MAY ( radiusArapFeatures $ radiusArapSecurity $ radiusArapZoneAccess $ radiusAuthType $ radiusCallbackId $ radiusCallbackNumber $ radiusCalledStationId $ radiusCallingStationId $ radiusClass $ radiusClientIPAddress $ radiusFilterId $ radiusFramedAppleTalkLink $ radiusFramedAppleTalkNetwork $ radiusFramedAppleTalkZone $ radiusFramedCompression $ radiusFramedIPAddress $ radiusFramedIPNetmask $ radiusFramedIPXNetwork $ radiusFramedMTU $ radiusFramedProtocol $ radiusAttribute $ radiusFramedRoute $ radiusFramedRouting $ radiusIdleTimeout $ radiusGroupName $ radiusHint $ radiusHuntgroupName $ radiusLoginIPHost $ radiusLoginLATGroup $ radiusLoginLATNode $ radiusLoginLATPort $ radiusLoginLATService $ radiusLoginService $ radiusLoginTCPPort $ radiusLoginTime $ radiusPasswordRetry $ radiusPortLimit $ radiusPrompt $ radiusProxyToRealm $ radiusRealm $ radiusServiceType $ radiusSessionTimeout $ radiusStripUserName $ radiusTerminationAction $ radiusTunnelClientEndpoint $ radiusProfileDN $ radiusProfileSuspendedDN $ radiusSimultaneousUse $ radiusTunnelAssignmentId $ radiusTunnelMediumType $ radiusTunnelPassword $ radiusTunnelPreference $ radiusTunnelPrivateGroupId $ radiusTunnelServerEndpoint $ radiusTunnelType $ radiusUserCategory $ radiusVSA $ radiusExpiration $ dialupAccess $ radiusNASIpAddress $ radiusReplyMessage $ radiusFramedIPv6Address $ radiusDNSServerIPv6Address $ radiusRouteIPv6Information $ radiusDelegatedIPv6PrefixPool $ radiusStatefulIPv6AddressPool $ radiusControlAttribute $ radiusReplyAttribute $ radiusRequestAttribute ) ) # diff --git a/raddb/mods-available/ldap b/raddb/mods-available/ldap index f5c2fbfc93a..a0b944344d0 100644 --- a/raddb/mods-available/ldap +++ b/raddb/mods-available/ldap @@ -355,6 +355,18 @@ ldap { # Will result in the user being locked out. # # access_positive = yes + + # + # access_value_negate:: Which value we look for in access_attribute + # to indicate that we should negate the result. + # +# access_value_negate = 'false' + + # + # access_value_suspend:: Which value we look for in access_attribute + # to indicate that the user should be suspended. + # +# access_value_suspend = 'suspended' } # @@ -493,6 +505,17 @@ ldap { # is successful. # # attribute = 'radiusProfileDn' + + # + # attribute_suspended: The LDAP attribute containing profile DNs to apply + # in addition to the default profile above, when the user account is in + # the suspended state + # + # These are retrieved from the user object, at the same time as the + # attributes from the update section, are are applied if authorization + # is successful. + # +# attribute_suspended = 'radiusProfileDn' } # diff --git a/src/lib/ldap/base.h b/src/lib/ldap/base.h index 923acdc970d..477ecd0dbe6 100644 --- a/src/lib/ldap/base.h +++ b/src/lib/ldap/base.h @@ -95,10 +95,10 @@ ldap_create_session_tracking_control LDAP_P(( //!< Used to allocate static arrays of control pointers. #define LDAP_MAX_ATTRMAP 128 //!< Maximum number of mappings between LDAP and //!< FreeRADIUS attributes. -#define LDAP_MAP_RESERVED 4 //!< Number of additional items to allocate in expanded +#define LDAP_MAP_RESERVED 5 //!< Number of additional items to allocate in expanded //!< attribute name arrays. Currently for enable attribute, //!< group membership attribute, valuepair attribute, - //!< and profile attribute. + //!< profile attribute and profile suspend attribute. #define LDAP_MAX_CACHEABLE 64 //!< Maximum number of groups we retrieve from the server for //!< a given user which need resolving from name to DN or DN diff --git a/src/modules/rlm_ldap/rlm_ldap.c b/src/modules/rlm_ldap/rlm_ldap.c index 331ca217574..68a5ebe2e58 100644 --- a/src/modules/rlm_ldap/rlm_ldap.c +++ b/src/modules/rlm_ldap/rlm_ldap.c @@ -72,6 +72,7 @@ static const call_env_t sasl_call_env[] = { static CONF_PARSER profile_config[] = { { FR_CONF_OFFSET("attribute", FR_TYPE_STRING, rlm_ldap_t, profile_attr) }, + { FR_CONF_OFFSET("attribute_suspend", FR_TYPE_STRING, rlm_ldap_t, profile_attr_suspend) }, CONF_PARSER_TERMINATOR }; @@ -92,6 +93,8 @@ static CONF_PARSER user_config[] = { { FR_CONF_OFFSET("access_attribute", FR_TYPE_STRING, rlm_ldap_t, userobj_access_attr) }, { FR_CONF_OFFSET("access_positive", FR_TYPE_BOOL, rlm_ldap_t, access_positive), .dflt = "yes" }, + { FR_CONF_OFFSET("access_value_negate", FR_TYPE_STRING, rlm_ldap_t, access_value_negate), .dflt = "false" }, + { FR_CONF_OFFSET("access_value_suspend", FR_TYPE_STRING, rlm_ldap_t, access_value_suspend), .dflt = "suspended" }, CONF_PARSER_TERMINATOR }; @@ -1389,8 +1392,14 @@ static unlang_action_t mod_authorize_resume(rlm_rcode_t *p_result, UNUSED int *p * Check for access. */ if (inst->userobj_access_attr) { - rcode = rlm_ldap_check_access(inst, request, autz_ctx->entry); - if (rcode != RLM_MODULE_OK) { + autz_ctx->access_state = rlm_ldap_check_access(inst, request, autz_ctx->entry); + switch (autz_ctx->access_state) { + case LDAP_ACCESS_ALLOWED: + case LDAP_ACCESS_SUSPENDED: + break; + + case LDAP_ACCESS_DISALLOWED: + rcode = RLM_MODULE_DISALLOW; goto finish; } } @@ -1520,9 +1529,35 @@ static unlang_action_t mod_authorize_resume(rlm_rcode_t *p_result, UNUSED int *p /* * Apply a SET of user profiles. */ - if (inst->profile_attr) { - autz_ctx->profile_values = ldap_get_values_len(handle, autz_ctx->entry, inst->profile_attr); + switch (autz_ctx->access_state) { + case LDAP_ACCESS_ALLOWED: + if (inst->profile_attr) { + autz_ctx->profile_values = ldap_get_values_len(handle, autz_ctx->entry, inst->profile_attr); + + if (RDEBUG_ENABLED3) { + for (struct berval **bv_p = autz_ctx->profile_values; *bv_p; bv_p++) { + RDEBUG3("Will evaluate suspended profile with DN \"%pV\"", fr_box_strvalue_len((*bv_p)->bv_val, (*bv_p)->bv_len)); + } + } + } + break; + + case LDAP_ACCESS_SUSPENDED: + if (inst->profile_attr_suspend) { + autz_ctx->profile_values = ldap_get_values_len(handle, autz_ctx->entry, inst->profile_attr_suspend); + + if (RDEBUG_ENABLED3) { + for (struct berval **bv_p = autz_ctx->profile_values; *bv_p; bv_p++) { + RDEBUG3("Will evaluate suspended profile with DN \"%pV\"", fr_box_strvalue_len((*bv_p)->bv_val, (*bv_p)->bv_len)); + } + } + } + break; + + case LDAP_ACCESS_DISALLOWED: + break; } + FALL_THROUGH; case LDAP_AUTZ_USER_PROFILE: @@ -1618,18 +1653,35 @@ static unlang_action_t CC_HINT(nonnull) mod_authorize(rlm_rcode_t *p_result, mod inst->handle_config.admin_password, request, &inst->handle_config); if (!autz_ctx->ttrunk) goto fail; +#define CHECK_EXPANDED_SPACE(_expanded) fr_assert((size_t)_expanded->count < (NUM_ELEMENTS(_expanded->attrs) - 1)); + /* * Add any additional attributes we need for checking access, memberships, and profiles */ - if (inst->userobj_access_attr) expanded->attrs[expanded->count++] = inst->userobj_access_attr; + if (inst->userobj_access_attr) { + CHECK_EXPANDED_SPACE(expanded); + expanded->attrs[expanded->count++] = inst->userobj_access_attr; + } if (inst->userobj_membership_attr && (inst->cacheable_group_dn || inst->cacheable_group_name)) { + CHECK_EXPANDED_SPACE(expanded); expanded->attrs[expanded->count++] = inst->userobj_membership_attr; } - if (inst->profile_attr) expanded->attrs[expanded->count++] = inst->profile_attr; + if (inst->profile_attr) { + CHECK_EXPANDED_SPACE(expanded); + expanded->attrs[expanded->count++] = inst->profile_attr; + } - if (inst->valuepair_attr) expanded->attrs[expanded->count++] = inst->valuepair_attr; + if (inst->profile_attr_suspend) { + CHECK_EXPANDED_SPACE(expanded); + expanded->attrs[expanded->count++] = inst->profile_attr_suspend; + } + + if (inst->valuepair_attr) { + CHECK_EXPANDED_SPACE(expanded); + expanded->attrs[expanded->count++] = inst->valuepair_attr; + } expanded->attrs[expanded->count] = NULL; diff --git a/src/modules/rlm_ldap/rlm_ldap.h b/src/modules/rlm_ldap/rlm_ldap.h index f8d4301b5de..b55d634475c 100644 --- a/src/modules/rlm_ldap/rlm_ldap.h +++ b/src/modules/rlm_ldap/rlm_ldap.h @@ -55,6 +55,12 @@ typedef struct { bool access_positive; //!< If true the presence of the attribute will allow access, //!< else it will deny access. + char const *access_value_negate; //!< If the value of the access_attr matches this, the result + ///< will be negated. + char const *access_value_suspend; //!< Value that indicates suspension. Is not affected by + ///< access_positive and will always allow access, but will apply + ///< a different profile. + char const *valuepair_attr; //!< Generic dynamic mapping attribute, contains a RADIUS //!< attribute and value. @@ -93,13 +99,15 @@ typedef struct { //!< rlm_ldap module. bool allow_dangling_group_refs; //!< Don't error if we fail to resolve a group DN referenced - ///< from a user object. + ///< from a user object. /* * Profiles */ char const *profile_attr; //!< Attribute that identifies profiles to apply. May appear //!< in userobj or groupobj. + char const *profile_attr_suspend; //!< Attribute that identifies profiles to apply when the user's + ///< account is suspended. May appear in userobj or groupobj. /* * Accounting @@ -161,6 +169,15 @@ typedef enum { LDAP_AUTZ_MAP } ldap_autz_status_t; +/** User's access state + * + */ +typedef enum { + LDAP_ACCESS_ALLOWED = 0, //!< User is allowed to login. + LDAP_ACCESS_DISALLOWED, //!< User it not allow to login (disabled) + LDAP_ACCESS_SUSPENDED //!< User account has been suspended. +} ldap_access_state_t; + /** Holds state of in progress async authorization * */ @@ -177,6 +194,7 @@ typedef struct { int value_idx; char *profile_value; char const *dn; + ldap_access_state_t access_state; //!< What state a user's account is in. } ldap_autz_ctx_t; /** State list for xlat evaluation of LDAP group membership @@ -233,7 +251,7 @@ int rlm_ldap_find_user_async(TALLOC_CTX *ctx, rlm_ldap_t const *inst, request_t fr_value_box_t *filter_box, fr_ldap_thread_trunk_t *ttrunk, char const *attrs[], fr_ldap_query_t **query_out); -rlm_rcode_t rlm_ldap_check_access(rlm_ldap_t const *inst, request_t *request, LDAPMessage *entry); +ldap_access_state_t rlm_ldap_check_access(rlm_ldap_t const *inst, request_t *request, LDAPMessage *entry); void rlm_ldap_check_reply(module_ctx_t const *mctx, request_t *request, fr_ldap_thread_trunk_t const *ttrunk); diff --git a/src/modules/rlm_ldap/user.c b/src/modules/rlm_ldap/user.c index 9813d75a519..74282049163 100644 --- a/src/modules/rlm_ldap/user.c +++ b/src/modules/rlm_ldap/user.c @@ -183,31 +183,46 @@ unlang_action_t rlm_ldap_find_user_async(TALLOC_CTX *ctx, rlm_ldap_t const *inst * - #RLM_MODULE_DISALLOW if the user was denied access. * - #RLM_MODULE_OK otherwise. */ -rlm_rcode_t rlm_ldap_check_access(rlm_ldap_t const *inst, request_t *request, LDAPMessage *entry) +ldap_access_state_t rlm_ldap_check_access(rlm_ldap_t const *inst, request_t *request, LDAPMessage *entry) { - rlm_rcode_t rcode = RLM_MODULE_OK; + ldap_access_state_t ret = LDAP_ACCESS_ALLOWED; struct berval **values = NULL; values = ldap_get_values_len(fr_ldap_handle_thread_local(), entry, inst->userobj_access_attr); if (values) { + size_t negate_value_len = talloc_array_length(inst->access_value_negate) - 1; if (inst->access_positive) { - if ((values[0]->bv_len >= 5) && (strncasecmp(values[0]->bv_val, "false", 5) == 0)) { - REDEBUG("\"%s\" attribute exists but is set to 'false' - user locked out", - inst->userobj_access_attr); - rcode = RLM_MODULE_DISALLOW; + if ((values[0]->bv_len >= negate_value_len) && + (strncasecmp(values[0]->bv_val, inst->access_value_negate, negate_value_len) == 0)) { + REDEBUG("\"%s\" attribute exists but is set to '%s' - user locked out", + inst->userobj_access_attr, inst->access_value_negate); + ret = LDAP_ACCESS_DISALLOWED; + goto done; } /* RLM_MODULE_OK set above... */ - } else if ((values[0]->bv_len < 5) || (strncasecmp(values[0]->bv_val, "false", 5) != 0)) { + } else if ((values[0]->bv_len < negate_value_len) || + (strncasecmp(values[0]->bv_val, inst->access_value_negate, negate_value_len) != 0)) { REDEBUG("\"%s\" attribute exists - user locked out", inst->userobj_access_attr); - rcode = RLM_MODULE_DISALLOW; + ret = LDAP_ACCESS_DISALLOWED; + goto done; } + { + size_t suspend_value_len = talloc_array_length(inst->access_value_suspend) - 1; + if ((values[0]->bv_len == suspend_value_len) && + (strncasecmp(values[0]->bv_val, inst->access_value_suspend, suspend_value_len) == 0)) { + REDEBUG("\"%s\" attribute exists and indicates suspension", inst->userobj_access_attr); + ret = LDAP_ACCESS_SUSPENDED; + goto done; + } + } + done: ldap_value_free_len(values); } else if (inst->access_positive) { REDEBUG("No \"%s\" attribute - user locked out", inst->userobj_access_attr); - rcode = RLM_MODULE_DISALLOW; + ret = LDAP_ACCESS_DISALLOWED; } - return rcode; + return ret; } /** Verify we got a password from the search diff --git a/src/tests/modules/ldap/access_state.unlang b/src/tests/modules/ldap/access_state.unlang new file mode 100644 index 00000000000..072090ea023 --- /dev/null +++ b/src/tests/modules/ldap/access_state.unlang @@ -0,0 +1,21 @@ +# Bill should be disabled +&Stripped-User-Name := 'bill' +ldap { + disallow = 1 +} +if (!disallow) { + test_fail +} + +# Test suspended profile application +&Stripped-User-Name := 'bobby' +ldap +if (!updated) { + test_fail +} + +if (&reply.Reply-Message != 'User-Suspended') { + test_fail +} + +test_pass diff --git a/src/tests/modules/ldap/module.conf b/src/tests/modules/ldap/module.conf index 1b97d3906d8..ea8dd945550 100644 --- a/src/tests/modules/ldap/module.conf +++ b/src/tests/modules/ldap/module.conf @@ -173,7 +173,7 @@ ldap { # If this is undefined, anyone is authorised. # If it is defined, the contents of this attribute # determine whether or not the user is authorised -# access_attribute = 'dialupAccess' + access_attribute = 'dialupAccess' # Control whether the presence of 'access_attribute' # allows access, or denys access. @@ -197,7 +197,10 @@ ldap { # userAccessAllowed: false # # Will result in the user being locked out. -# access_positive = yes + access_positive = yes + + access_value_negate = "disabled" + access_value_suspend = "suspended" } # @@ -272,6 +275,7 @@ ldap { # The 'User-Profile' attribute in the control list # will override this setting at run-time. attribute = 'radiusProfileDn' + attribute_suspend = "radiusProfileSuspendedDn" } # diff --git a/src/tests/salt-test-server/salt/ldap/base.ldif b/src/tests/salt-test-server/salt/ldap/base.ldif index 11b6c75d3e5..2f7bcb2a75d 100644 --- a/src/tests/salt-test-server/salt/ldap/base.ldif +++ b/src/tests/salt-test-server/salt/ldap/base.ldif @@ -79,6 +79,12 @@ radiusRequestAttribute: Service-Type := 'Framed-User' radiusControlAttribute: Framed-IP-Address == 1.2.3.4 radiusControlAttribute: Reply-Message := "Hello world" +dn: cn=suspended,ou=profiles,dc=example,dc=com +objectClass: freeradiusPolicy +objectClass: radiusprofile +cn: suspended +radiusReplyAttribute: Reply-Message := 'User-Suspended' + dn: uid=john,ou=people,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount @@ -97,6 +103,7 @@ radiusIdleTimeout: 3600 radiusAttribute: reply.Session-Timeout := 7200 radiusAttribute: control.NAS-IP-Address := 1.2.3.4 radiusProfileDN: cn=profile1,ou=profiles,dc=example,dc=com +dialupAccess: enabled dn: uid=bob,ou=people,dc=example,dc=com objectClass: inetOrgPerson @@ -113,11 +120,13 @@ uidNumber: 101 gidNumber: 101 homeDirectory: /home/bob radiusIdleTimeout: 7200 +dialupAccess: enabled dn: uid=jane,ou=people,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount objectClass: shadowAccount +objectClass: radiusprofile uid: jane sn: Davis givenName: Jane @@ -127,11 +136,13 @@ userPassword: secret uidNumber: 103 gidNumber: 103 homeDirectory: /home/jane +dialupAccess: enabled dn: uid=ann,ou=people,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount objectClass: shadowAccount +objectClass: radiusprofile uid: ann sn: Williams cn: Ann Williams @@ -140,11 +151,13 @@ userPassword: topsecret uidNumber: 104 gidNumber: 104 homeDirectory: /home/ann +dialupAccess: enabled dn: uid=bill,ou=people,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount objectClass: shadowAccount +objectClass: radiusprofile uid: bill sn: Brown cn: Bill Brown @@ -153,6 +166,24 @@ userPassword: NotTelling uidNumber: 105 gidNumber: 105 homeDirectory: /home/bill +dialupAccess: disabled + +dn: uid=bobby,ou=people,dc=example,dc=com +objectClass: inetOrgPerson +objectClass: posixAccount +objectClass: shadowAccount +objectClass: radiusprofile +uid: bobby +sn: Brown +cn: Bobby Brown +displayName: Bobby Brown +userPassword: NotTelling +uidNumber: 106 +gidNumber: 106 +homeDirectory: /home/bobby +dialupAccess: suspended +radiusProfileDN: cn=profile1,ou=profiles,dc=example,dc=com +radiusProfileSuspendedDN: cn=suspended,ou=profiles,dc=example,dc=com dn: ou=clients,dc=example,dc=com objectClass: organizationalUnit