NAME

Class::User::DBI - A User class: Login credentials, roles, privileges, domains.

VERSION

Version 0.10

SYNOPSIS

This module models a "User" class, with login credentials, and Roles Based Access Control. Additionally, IP whitelists may be used as an additional validation measure. Domain (locality) based access control is also provided independently of role based access control.

A brief description of authentication: Passphrases are stored as randomly salted SHA2-512 hashes. Optional whitelisting of IP's is also available.

A brief description of this RBAC implementation: Users have roles and domains (localities). Roles carry privileges. Roles with privileges, and domains act independently, allowing for sophisticated access control.

# Set up a connection using DBIx::Connector:
# MySQL database settings:

my $conn = DBIx::Connector->new(
    'dbi:mysql:database=cudbi_tests, 'testing_user', 'testers_pass',
    {
        RaiseError => 1,
        AutoCommit => 1,
    }
);


# Now we can play with Class::User::DBI:

Class::User::DBI->configure_db( $conn );  # Set up the tables for a user DB.

my @user_list = Class::User::DBI->list_users;

my $user = new( $conn, $userid );

my $user_id  = $user->add_user(
    {
        password => $password,
        ip_req   => $bool_ip_req,
        ips      => [ '192.168.0.100', '201.202.100.5' ], # aref ip's.
        username => $full_name,
        email    => $email,
        role     => $role,
    }
);

my $userid      = $user->userid;
my $validated   = $user->validated;
my $invalidated = $user->validated(0);           # Cancel authentication.
my $is_valid    = $user->validate( $pass, $ip ); # Validate including IP.
my $is_valid    = $user->validate( $pass );      # Validate without IP.
my $info_href   = $user->load_profile;
my $credentials = $user->get_credentials;        # Returns a useful hashref.
my @valid_ips   = $user->get_valid_ips;
my $ip_required = $user->get_ip_required;
my $success     = $user->set_ip_required(1);
my $ exists     = $user->exists_user;
my $success     = $user->delete_user;
my $del_count   = $user->delete_ips( @ips );
my $add_count   = $user->add_ips( @ips );
my $success     = $user->set_email( 'new@email.address' );
my $success     = $user->set_username( 'Cool New User Name' );
my $success     = $user->update_password( 'Old Pass', 'New Pass' );
my $success     = $user->update_password( 'New Pass' );
my $success     = $user->set_role( $role );
my $has         = $user->is_role( $role );
my $role        = $user->get_role;

# Accessors for the RolePrivileges and UserDomains classes.
my $rp          = $user->role_privileges;
my $has_priv    = $user->role_privileges->has_privilege( 'some_privilg' );
my $ud          = $user->user_domains;
my $has_domain  = $user->user_domains->has_domain( 'some_domain' );

DESCRIPTION

The module is designed to simplify user logins, authentication, role based access control (authorization), as well as domain (locality) constraint access control.

It stores user credentials, roles, and basic user information in a database via a DBIx::Connector database connection.

User passphrases are salted with a 512 bit random salt (unique per user) using a cryptographically strong random number generator, and converted to a SHA2-512 digest before being stored in the database. All subsequent passphrase validation checks test against the salt and passphrase SHA2 hash.

IP whitelists may be maintained per user. If a user is set to require an IP check, then the user validates only if his passphrase authenticates AND his IP is found in the whitelist associated with his user id.

Users may be given a role, which is conceptually similar to a Unix 'group'. Roles are simple strings. Furthermore, multiple privileges (also simple strings) are granted to roles.

Users may be given multiple domains, which might be used to model localities or jurisdictions. Domains act independently from roles and privileges, but are a convenient way of constraining a role and its privileges to a specific set of localities.

EXPORT

Nothing is exported. There are many object methods, and three class methods, described in the next section.

SETTING UP AN AUTHENTICATION AND ROLES BASED ACCESS CONTROL MODEL

First, use Class::User::DBI::Roles to set up a list of roles and their corresponding descriptions.

Next, use Class::User::DBI::Privileges to set up a list of privileges and their corresponding descriptions.

Use Class::User::DBI::RolePrivileges to associate one or more privileges with each role.

Use Class::User::DBI::Domains to create a list of domains (localities), along with their descriptions.

Use Class::User::DBI (This module) to create a set of users, establish login credentials such as passphrases and optional IP whitelists, and assign them roles.

Use Class::User::DBI::UserDomains to associate one or more localities (domains) with each user.

USING AN AUTHENTICATION AND ROLES BASED ACCESS CONTROL MODEL

Use Class::User::DBI (This module) to instantiate a user, and validate him by passphrase and optional whitelist.

Use the instantiated user object to get the user's 'RolePrivileges' object. Use the instantiated user object to get the user's 'UserDomains' object.

Use the Class::User::DBI::RolePrivileges object obtained via a call to $user->get_role_privilege_object to verify that a user has a given access privilege.

Use the Class::User::DBI::UserDomains object obtained via a call to $user->user_domains to verify that a user has a given domain/jurisdiction/locality.

SUBROUTINES/METHODS

All methods will be listed alphabetically, class methods first, object methods thereafter.

CLASS METHODS

new (The constructor -- Class method.)

my $user_obj = Class::User::DBI->new( $connector, $userid );

Instantiates a new Class::User::DBI object in behalf of a target user on a database handled by the DBIx::Connector.

The user object may be accessed and manipulated through the methods listed below.

list_users (Class method)

my @users = Class::User::DBI->list_users( $connector );
foreach my $listed_user ( @users ) {
    my( $userid, $username, $email ) = @{$listed_user};
    print "userid: ($userid).  username: ($username).  email: ($email).\n";
}

This is a class method. Pass a valid DBIx::Connector as a parameter. Returns a list of arrayrefs. Each anonymous array contains userid, username, email, role, and ip_required.

configure_db (Class method)

Class::User::DBI->configure_db( $connector );

This is a class method. Pass a valid DBIx::Connector as a parameter. Builds a minimal set of database tables in support of the Class::User::DBI.

The tables created will be users, user_ips, and user_roles.

USER OBJECT METHODS

add_ips

my $quantity_added = $user->add_ips ( @whitelisted_ips );

Pass a list of IP's to add to the IP whitelist for this user. Any IP's that are already in the database will be silently skipped.

Returns a count of how many were added.

add_user

my $success = $self->add_user( {
    username    => $user_full_name,     # Optional field. Default q{}.

    password    => $user_passphrase,    # Clear text password.
                                        # Required field. No length limit.

    email       => $user_email,         # Optional field.  Default q{}.

    ip_req      => $ip_validation_reqd  # Boolean value determining whether
                                        # this user requires IP whitelist
                                        # validation. ( 0 = no, 1 = yes ).
                                        # Optional field.  Default 0.

    ips_aref    => [                    # Optional field.  Default is empty
        '127.0.0.1',                    # list.  If 'ip_req' is set and no
        '192.168.0.1',                  # list is provided here, then valid
    ],                                  # IP's will need to be added later
                                        # before user can validate.
    role        => $role,               # A string representing user's role.
} );

This method creates a new user in the database with the userid supplied when the user object was instantiated. The password field is the only required field. It must contain a clear-text passphrase. There is no length limitation.

Other fields are optional, but convenient. If IP whitelisting is needed for this user, the ip_req field must be supplied, and must be set to 1 (true).

If ip_req is set to 1 (true), a list of valid IP's may also be provided in an arrayref keyed off of ips_aref. As a convenience, the ips key is synonymous with ips_aref. The IP's provided will then be added to the user_ips database table. If an IP is required but none are added via add_user, they will have to be added manually with add_ips before the user can be validated.

The user's passphrase will be salted with a cryptographically sound random salt of 512 bits (128 hex digits). It will then be digested using a SHA2-512 hash, and both the salt and the digest will be stored in the users database.

This is a reliable and secure means of storing a passphrase. In fact, the passphrase is not stored at all. Just a salt and the digest. Even if the salt and hash were to be discovered by an attacker, they would not be useful in side-stepping user validation, as they cannot be used to decrypt the passphrase. SHA512 is the strongest of the SHA2 family. A salt length of 512 bits guarantees a maximum entropy for any given passphrase.

Though it is beyond the scope of this module to enforce, users should be encouraged to use passphrases that are both resistant to dictionary attacks, and dissimilar to passphrases used in other applications. No minimum passphrase size is enforced by this module. But a strong passphrase should be of ample length, and should contain characters beyond the standard alphabet.

Users may also be assigned a role that will be used in RBAC.

delete_ips

my $quantity_deleted = $user->delete_ips( @ips_to_remove );

Pass a list of IP's to remove from the IP whitelist for this user. Any IP's that weren't found in the database will be silently skipped.

Returns a count of how many IP's were dropped.

delete_user

$user->delete_user;

Removes the user from the database, along with the user's IP whitelist, and any associated domains. Also sets the $user->validated, and $user->exists_user flags to false.

exists_user

Checks the database to verify that the user exists. As this method is used internally frequently its positive result is cached to minimize database queries. Methods that would invalidate the existence of the user in the database, such as $user->delete_user will remove the cache entry, and subsequent tests will access the database on each call to exists_user(), until such time that the result flips to positive again.

get_credentials

my $credentials_href = $user->get_credentials;
my @fields = qw( userid salt_hex pass_hex ip_required );
foreach my $field ( @fields ) {
    print "$field => $credentials_href->{$field}\n";
}
my @valid_ips = @{$valid_ips};
foreach my $ip ( @valid_ips ) {
    print "Whitelisted IP: $ip\n";
}

Accepts no parameters. Returns a hashref holding a small datastructure that describes the user's credentials. The structure looks like this:

$href = {
    userid      => $userid,     # The target user's userid.

    salt_hex    => $salt,       # A 128 hex-character representation of
                                # the user's random salt.

    pass_hex    => $pass,       # A 128 hex-character representation of
                                # the user's SHA2-512 digested passphrase.

    ip_required => $ip_req,     # A Boolean value indicating whether this
                                # user requires IP whitelist validation.

    valid_ips   => [            # Whitelisted IP's for user. (optional)
        '127.0.0.1',            # Some example whitelisted IP's.
        '129.168.0.10',
    ],
};

A typical usage probably won't require calling this function directly very often, if at all. In most cases where it would be useful to look at the salt, the passphrase digest, and IP whitelists, the $user->validate( $passphrase, $ip ) method is easier to use and less prone to error. But for those cases I haven't considered, the get_credentials() method exists.

get_role

my $user_role = $user->get_role;

Returns the user's assigned role. If no role is assigned, returns an empty string.

role_privileges

Returns a Class::User::DBI::RolePrivileges object associated with this user's role. See Class::User::DBI::RolePrivileges to read how to manipulate the object.

user_domains

Returns a Class::User::DBI::UserDomains object associated with this user. See Class::User::DBI::UserDomains to read how to manipulate the object.

get_valid_ips

my @valid_ips = $user->get_valid_ips;

Returns a list containing the whitelisted IP's for this user. Each IP will be a string in the form of 192.168.0.198. If the user doesn't use IP validation, or there are no IP's stored for this user, the list will be empty.

is_role

my $is = $user->is_role( 'worker' );

Returns true if this user's role matches the parameter.

load_profile

my $user_info_href = $user->load_profile;
foreach my $field ( qw/ userid username email role / ) {
    print "$field   => $user_info_href->{$field}\n";
}

Returns a reference to an anonymous hash containing the user's basic profile and RBAC information. The datastructure looks like this:

my $user_info_href = {
    userid      => $userid,     # The primary user ID.
    username    => $username,   # The full user name as stored in the DB.
    email       => $email,      # The email stored in the DB for this user.
    role        => $role,       # The user's assigned role (may be blank).
    privileges  => [ @privs ],  # A reference to an array of user's privs.
    domains     => [ @doms  ],  # A reference to an array of user's domains.
    ip_required => $required,   # 0 or 1.
};

The privileges, and domains array refs will always contain a reference to an anonymous array, but that array may be empty if the user has no assigned domains or privileges.

get_ip_required

my $required = $user->get_ip_required;

Returns 0 if this user doesn't require IP verification. 1 if user requires IP verification. undef if the user doesn't exist in the database.

set_ip_required

my $success = $user->set_ip_required( $new_setting );

Pass 1 to set this user for IP validation. Pass 0 to turn off IP validation for this user. Returns 1 on success. Croaks if user isn't in the database.

set_email

my $success = $user->set_email( $new_email_address );

Email addresses are not verified for validity in any way. However, the default database field used for storing email addresses provides 320 bytes of storage, which is the maximum length possible for a valid email address.

set_role

my $success = $user->set_role( $new_role );

Set's (or changes) the user's role. There is a validity check: The role must have been already defined via Class::User::DBI::Roles.

set_username

my $success = $user->set_username( $new_user_full_name );

There's probably not much need for explaining this method. The default database table's username field accepts user names up to fourty characters.

update_password

# Update with validation of old password first.
my $success = $user->update_password( $new_pass, $old_pass );

# Update without validation of old password first.
my $success = $user->update_password( $new_pass );

Using the same algorithms of add_user( { password = $passphrase } ); >>, creates a new password for the user. If the old passphrase is supplied as a second parameter, the update will only take place if the old passphrase validates.

The "with validation" method is useful for allowing a user to update her own password. The "without validation" version is useful for allowing an administrator (or automated process) to reset a user's forgotten password.

userid

my $userid = $user->userid;

A simple accessor returning the userid that is the target of the Class::User::DBI object.

validate

# If no IP whitelist verification is required:
my $is_valid  = $user->validate( $passphrase );

# If IP whitelist verification is required:
my $is_valid = $user->validate( $passphrase, $current_ip );

my $forced_revalidate
    = $user->validate( $passphrase, $current_ip, 1 ); # 3rd arg true.

Returns Boolean true if and only if the user can be validated. What that means will be described in the paragraphs below. If the user cannot be validated, the return value will be Boolean false. It doesn't matter what the reason for failure to authenticate might have been: Invalid user ID, invalid password, or invalid IP address; all three reasons result in a return value of Boolean false.

This design decision encourages the best practice of not divulging to the user why his authentication failed. The less information provided, the less an attacker can user to narrow the field. If it is necessary to explicitly test whether a userid actually exists, or whether the user's IP matches the whitelist, separate accessors are provided to facilitate that requirement.

If you wish to force a revalidation (assume a dirty cache) you have two options:

$user->validated(0);    # Invalidate the cache.
print "Ok.\n" if $user->validate( $pass, $ip );

Or you can do that as a single operation:

print "Ok.\n" if $user->validate( $pass, $ip, 1 );

What Validation (or Authentication) Means To This Module

If the user has been configured for no IP testing, validation means that the userid exists (case insensitively) within the database, and that the passphrase passed to validate(), when salted with the stored salt and digested using a SHA2-512 hashing algorithm results in the same 512 bit hash as the one generated when the passphrase was originally set up.

If the user has been configured to require IP testing, validation also means that the IP supplied to the validate() method matches one of the IP's stored in the database for this user. IP's are stored in the clear, which shouldn't matter. User input should never be used for the IP field of validate(). It is assumed that within the application, the user's IP will be detected, and that IP will be passed for cross-checking with the whitelist database.

The validate() method caches its positive result. Any action that might change the authentication status will remove the cached status. Actions that will result in validate() to perform all tests again include delete_user(), update_password(), or validated(0) (passing the validated() method a '0'.

validated

# Test.
my $has_been_validated = $user->validated;

# Invalidate.
$user->validated(0);

Returns true if the user has been validated, as described above. Does not perform a full validation; simply tests whether the previous call to validate() succeeded, and that nothing has happened to remove that "is valid" status.

Pass a parameter of '0' to force all future calls to validated() to return false. Also, after resetting validated() to false, future calls to validate() will go through the full authentication process again until such time as the authentication is successful.

EXAMPLE

Please refer to the contents of the examples/ directory from this distribution's build directory. There you will find a working example that creates some roles, some privileges, assigns the privileges to a role, creates some domains, creates a user, and associates a role and several domains to that user. Though the example doesn't exercise every method contained in the distribution, it provides a nice concise demonstration of setting up the basic elements of Authentication and RBAC, and using them.

DEPENDENCIES

This module requires DBIx::Connector, Authen::Passphrase::SaltedSHA512, and List::MoreUtils. It also requires a database connection. The test suite will use DBD::SQLite, but it has also been tested with DBD::mysql. None of these dependencies with the exception of List::MoreUtils could be considered light-weight. The dependency chain of this module is indicative of the difficulty in assuring cryptographically strong random salt generation, reliable SHA2-512 hashing of passphrases, fork-safe database connectivity, and transactional commits for inserts and updates spanning multiple tables.

CONFIGURATION AND ENVIRONMENT

The database used will need seven tables to be set up.

For convenience, a class method has been provided with each of this distribution's classes that will auto-generate the minimal schema within a SQLite or MySQL database. The SQLite database is probably only useful for testing, as it lacks many of the security measures present in web-stack quality databases.

Within the ./scripts/ directory of this distribution you will find a script that accepts a database type (mysql or sqlite), database name, database username, and database password on the command line. It then opens the given database and creates the appropriate tables. The script is named cudbi-configdb. Run it once without any command line parameters to see details on usage.

After creating the database framework, it might be useful to alter the tables that have been generated by customizing field widths, text encoding, and so on. It may be advisable to enable UTF8 for the userid, email, username fields, and possibly even for the role field.

There is no explicit size requirement for the userid, username, and role fields. They could be made wider if it's deemed useful. Don't be tempted to reduce the size of the email address field: The best practice of coding to the standard dictates that the field needs to be 320 characters wide.

The salt and password fields are used to store a 128 hex-digit representation of the 512 bit salt and 512 bit SHA2 hash of the user's passphrase. More digits is not useful, and less won't store the full salt and hash.

DIAGNOSTICS

If you find that your particular database engine is not playing nicely with the test suite from this module, it may be necessary to provide the database login credentials for a test database using the same engine that your application will actually be using. You may do this by setting $ENV{CUDBI_TEST_DSN}, $ENV{CUDBI_TEST_DATABASE}, $ENV{CUDBI_TEST_USER}, and $ENV{CUDBI_TEST_PASS}.

Currently the test suite tests against a SQLite database since it's such a lightweight dependency for the testing. The author also uses this module with several MySQL databases. As you're configuring your database, providing its credentials to the tests and running the test scripts will offer really good diagnostics if some aspect of your database tables proves to be at odds with what this module needs.

Be advised that the the test suite drops its tables after completion, so be sure to run the test suite only against a database set up explicitly for testing purposes.

INCOMPATIBILITIES

This module has only been tested on MySQL and SQLite database engines. If you are successful in using it with other engines, please send me an email detailing any additional configuration changes you had to make so that I can document the compatibility, and improve the documentation for the configuration process.

BUGS AND LIMITATIONS

This module is still in beta testing. The API of any version number in the form of 'xxx.yyy_zzz' could still change. Once the version reaches the form of 'xxx.yyy', the API may be considered stable.

AUTHOR

David Oswald, <davido at cpan.org>

BUGS

Please report any bugs or feature requests to bug-class-user-dbi at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Class-User-DBI. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc Class::User::DBI

You can also look for information at:

ACKNOWLEDGEMENTS

LICENSE AND COPYRIGHT

Copyright 2012 David Oswald.

This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.