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.
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
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 )
)
#
# 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'
}
#
# 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'
}
#
//!< 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
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
};
{ 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
};
* 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;
}
}
/*
* 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:
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;
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.
//!< 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
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
*
*/
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
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);
* - #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
--- /dev/null
+# 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
# 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.
# userAccessAllowed: false
#
# Will result in the user being locked out.
-# access_positive = yes
+ access_positive = yes
+
+ access_value_negate = "disabled"
+ access_value_suspend = "suspended"
}
#
# The 'User-Profile' attribute in the control list
# will override this setting at run-time.
attribute = 'radiusProfileDn'
+ attribute_suspend = "radiusProfileSuspendedDn"
}
#
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
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
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
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
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
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