]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
ldap: Add support for custom access attribute negation values
authorArran Cudbard-Bell <a.cudbardb@freeradius.org>
Fri, 1 Sep 2023 01:04:03 +0000 (19:04 -0600)
committerArran Cudbard-Bell <a.cudbardb@freeradius.org>
Fri, 1 Sep 2023 23:34:54 +0000 (17:34 -0600)
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.

doc/schemas/ldap/openldap/freeradius-radius.schema
raddb/mods-available/ldap
src/lib/ldap/base.h
src/modules/rlm_ldap/rlm_ldap.c
src/modules/rlm_ldap/rlm_ldap.h
src/modules/rlm_ldap/user.c
src/tests/modules/ldap/access_state.unlang [new file with mode: 0644]
src/tests/modules/ldap/module.conf
src/tests/salt-test-server/salt/ldap/base.ldif

index 4bbd90acbaf7a0b7578b2bd11c26094435ea6544..09030859de36c12f55a97caed9ac802c67e94743 100644 (file)
@@ -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 )
  )
 
 #
index f5c2fbfc93a8e1e31bd04b65d8fb98d28dd3f281..a0b944344d0f08ef6b3323a6af2abfa179878cbd 100644 (file)
@@ -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'
        }
 
        #
index 923acdc970dec46702193858d180e5cb368d6ed6..477ecd0dbe6851ea9fda88f0b50b7f69d05bd342 100644 (file)
@@ -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
index 331ca21757428c19d890d620a10552d77c9a28e9..68a5ebe2e58ec3af7401bdd214727bd7644ffd65 100644 (file)
@@ -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;
 
index f8d4301b5de583e519fb85d5f0d74bcca8be0080..b55d634475c0faf16e11fba44230701d6631b88b 100644 (file)
@@ -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);
 
index 9813d75a519412772e01fffa583bca2856d90fca..74282049163fbfc56060ab3a4480a753b096fe3f 100644 (file)
@@ -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 (file)
index 0000000..072090e
--- /dev/null
@@ -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
index 1b97d3906d8a6334df08d1efa4d0096d590b80c6..ea8dd945550b3f02fc4304badeb3bd713e6de724 100644 (file)
@@ -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"
        }
 
        #
index 11b6c75d3e536d7151f3e9e1149577b03f78c496..2f7bcb2a75d3f166eaeb0d6e968d031fc9d15a22 100644 (file)
@@ -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