Class-User-DBI

 view release on metacpan or  search on metacpan

META.json  view on Meta::CPAN

{
   "abstract" : "A User class: Login credentials, roles, privileges, domains.",
   "author" : [
      "David Oswald <davido [@at] cpan [d.o.t] org>"
   ],
   "dynamic_config" : 1,
   "generated_by" : "ExtUtils::MakeMaker version 6.6302, CPAN::Meta::Converter version 2.120921",
   "license" : [
      "perl_5"
   ],
   "meta-spec" : {
      "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",

META.yml  view on Meta::CPAN

---
abstract: 'A User class: Login credentials, roles, privileges, domains.'
author:
  - 'David Oswald <davido [@at] cpan [d.o.t] org>'
build_requires:
  DBD::SQLite: 0
  DBIx::Connector: 0
  List::MoreUtils: 0
  Test::Exception: 0
  Test::More: 0.98
configure_requires:
  ExtUtils::MakeMaker: 6.62

README  view on Meta::CPAN

Class-User-DBI

A User class: Login credentials and roles.

Through a DBIx::Connector object, this module models a "User" class, with
login credentials, and access roles.  Login credentials include a passphrase,
and optionally per user IP whitelisting.

The module is designed to simplify user logins, and basic administrative user
maintenance.  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

examples/cudbi-example  view on Meta::CPAN

print " He never sleeps!\n"
    if ! $user->role_privileges->has_privilege( 'rest' );


my $profile = $user->load_profile;
print "He is $profile->{username}!\n";

print "And here is everything you need to know about him:\n";
print Dumper $profile;

my $credentials = $user->get_credentials;
print "And these are his secrets:\n";
print Dumper $credentials;

print "But eventually everyone kicks the bucket... " if $user->delete_user;

print "Loses validity,\n"
    if ! $user->validated;
    
print "And ceases to exist.\n"
    if ! $user->exists_user;

lib/Class/User/DBI.pm  view on Meta::CPAN

# Quick check whether a userid exists inf the database.
# Return 0 if user doesn't exist.  Caches result.
sub exists_user {
    my $self = shift;
    return 1 if $self->{exists_user};
    my $sth = $self->_db_run( $USER_QUERY{SQL_exists_user}, $self->userid );
    return defined $sth->fetchrow_array ? 1 : 0;
}

# Fetch user's salt_hex, pass_hex, ip_required, and valid ip's from database.
sub get_credentials {
    my $self = shift;
    my $sth = $self->_db_run( $USER_QUERY{SQL_get_credentials}, $self->userid );
    my ( $salt_hex, $pass_hex, $ip_required ) = $sth->fetchrow_array;
    return if not defined $salt_hex;    # User wasn't found.
    my @valid_ips = $self->get_valid_ips;
    return {
        userid      => $self->userid,
        salt_hex    => $salt_hex,
        pass_hex    => $pass_hex,
        ip_required => $ip_required,
        valid_ips   => [@valid_ips],
    };

lib/Class/User/DBI.pm  view on Meta::CPAN

sub validate {
    my ( $self, $password, $ip, $force_revalidate ) = @_;
    croak 'Cannot validate without a passphrase.'
      if !defined $password || !length $password;
    return 0 if !$self->exists_user;

    # Save ourselves work if user is already authenticated.
    if ( !$force_revalidate && $self->validated ) {
        return 1;
    }
    my $credentials = $self->get_credentials;
    my $auth        = Authen::Passphrase::SaltedSHA512->new(
        salt_hex => $credentials->{salt_hex},
        hash_hex => $credentials->{pass_hex}
    );

    if ( !$auth->match($password) ) {
        $self->validated(0);
        return 0;
    }

    # Return 0 if an IP is required, and IP param is not in whitelist,
    # or no IP parameter passed.
    if ( $credentials->{ip_required} ) {
        if (   !defined $ip
            || !any { $ip eq $_ } @{ $credentials->{valid_ips} } )
        {
            $self->validated(0);
            return 0;
        }
    }

    # We passed! Authenticate.
    $self->{validated} = 1;    # Set in object that we're authenticated.
    return 1;
}

lib/Class/User/DBI.pm  view on Meta::CPAN

    return @rv;
}

sub update_password {
    my ( $self, $newpass, $oldpass ) = @_;

    return if !$self->exists_user;

    # If an old passphrase is supplied, only update if it validates.
    if ( defined $oldpass ) {
        my $credentials = $self->get_credentials;
        my $auth        = Authen::Passphrase::SaltedSHA512->new(
            salt_hex => $credentials->{salt_hex},
            hash_hex => $credentials->{pass_hex}
        );

        # Return undef if password doesn't authenticate for the user.
        return unless $auth->match($oldpass);    ## no critic (postfix)
    }

    my $passgen =
      Authen::Passphrase::SaltedSHA512->new( passphrase => $newpass );
    my $salt_hex = $passgen->salt_hex;
    my $hash_hex = $passgen->hash_hex;

lib/Class/User/DBI.pm  view on Meta::CPAN

    }
    return 1;
}

1;

__END__

=head1 NAME

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

=head1 VERSION

Version 0.10

=head1 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.

lib/Class/User/DBI.pm  view on Meta::CPAN

            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' );

lib/Class/User/DBI.pm  view on Meta::CPAN

    my $ud          = $user->user_domains;
    my $has_domain  = $user->user_domains->has_domain( 'some_domain' );


=head1 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.

lib/Class/User/DBI.pm  view on Meta::CPAN

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

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

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

Use L<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 L<Class::User::DBI::UserDomains> to associate one or more localities
(domains) with each user.

=head1 USING AN AUTHENTICATION AND ROLES BASED ACCESS CONTROL MODEL

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

lib/Class/User/DBI.pm  view on Meta::CPAN

=head2  exists_user

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


=head2  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.

lib/Class/User/DBI.pm  view on Meta::CPAN

            '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
C<< $user->validate( $passphrase, $ip ) >> method is easier to use and less
prone to error.  But for those cases I haven't considered, the
C<get_credentials()> method exists.


=head2 get_role

    my $user_role = $user->get_role;

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

=head2 role_privileges

lib/Class/User/DBI.pm  view on Meta::CPAN

The C<salt> and C<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.


=head1 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 C<$ENV{CUDBI_TEST_DSN}>,
C<$ENV{CUDBI_TEST_DATABASE}>, C<$ENV{CUDBI_TEST_USER}>, 
and C<$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.

=head1 INCOMPATIBILITIES

This module has only been tested on MySQL and SQLite database engines.  If you

lib/Class/User/DBI/DB.pm  view on Meta::CPAN


use Carp;

our $VERSION = '0.10';
# $VERSION = eval $VERSION;    ## no critic (eval)

# ---------------- SQL queries for Class::User::DBI --------------------------

our %USER_QUERY = (
    SQL_get_valid_ips   => 'SELECT ip FROM user_ips WHERE userid = ?',
    SQL_get_credentials => 'SELECT salt, password, ip_required '
      . 'FROM users WHERE userid = ?',
    SQL_exists_user => 'SELECT userid FROM users WHERE userid = ?',
    SQL_load_profile =>
      'SELECT userid, username, email, role, ip_required FROM users WHERE userid = ?',
    SQL_add_ips    => 'INSERT INTO user_ips ( userid, ip ) VALUES( ?, ? )',
    SQL_delete_ips => 'DELETE FROM user_ips WHERE userid = ? AND ip = ?',
    SQL_add_user => 'INSERT INTO users ( userid, salt, password, ip_required, '
      . 'username, email, role ) VALUES( ?, ?, ?, ?, ?, ?, ? )',
    SQL_delete_user     => 'DELETE FROM users WHERE userid = ?',
    SQL_delete_user_ips => 'DELETE FROM user_ips WHERE userid = ?',

lib/Class/User/DBI/DB.pm  view on Meta::CPAN


All of the SQL for this entire distribution is found in the 
L<Class::User::DBI::DB> module.  Any adjustments required to suit your database
engine may be made here.  This module's SQL is known to run unaltered with 
SQLite and MySQL.

=head1 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 C<$ENV{CUDBI_TEST_DSN}>,
C<$ENV{CUDBI_TEST_DATABASE}>, C<$ENV{CUDBI_TEST_USER}>, 
and C<$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.


=head1 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.

lib/Class/User/DBI/Domains.pm  view on Meta::CPAN

Please refer to the C<configure_db()> class method for this module for a
simple means of creating the table that supports this class.

All SQL for this distribution is contained in the L<Class::User::DBI::DB> 
module.

=head1 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 C<$ENV{CUDBI_TEST_DSN}>,
C<$ENV{CUDBI_TEST_DATABASE}>, C<$ENV{CUDBI_TEST_USER}>, 
and C<$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.


=head1 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.

lib/Class/User/DBI/Privileges.pm  view on Meta::CPAN

simple means of creating the table that supports this class.

All SQL for this distribution is contained in the L<Class::User::DBI::DB> 
module.


=head1 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 C<$ENV{CUDBI_TEST_DSN}>,
C<$ENV{CUDBI_TEST_DATABASE}>, C<$ENV{CUDBI_TEST_USER}>, 
and C<$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.


=head1 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.

lib/Class/User/DBI/RolePrivileges.pm  view on Meta::CPAN

simple means of creating the table that supports this class.

All SQL for this distribution is contained in the L<Class::User::DBI::DB> 
module.


=head1 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 C<$ENV{CUDBI_TEST_DSN}>,
C<$ENV{CUDBI_TEST_DATABASE}>, C<$ENV{CUDBI_TEST_USER}>, 
and C<$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.


=head1 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.

lib/Class/User/DBI/Roles.pm  view on Meta::CPAN

simple means of creating the table that supports this class.

All SQL for this distribution is contained in the L<Class::User::DBI::DB> 
module.


=head1 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 C<$ENV{CUDBI_TEST_DSN}>,
C<$ENV{CUDBI_TEST_DATABASE}>, C<$ENV{CUDBI_TEST_USER}>, 
and C<$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.


=head1 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.

lib/Class/User/DBI/UserDomains.pm  view on Meta::CPAN

simple means of creating the table that supports this class.

All SQL for this distribution is contained in the L<Class::User::DBI::DB> 
module.


=head1 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 C<$ENV{CUDBI_TEST_DSN}>,
C<$ENV{CUDBI_TEST_DATABASE}>, C<$ENV{CUDBI_TEST_USER}>, 
and C<$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.


=head1 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.

t/21-cu_dbi.t  view on Meta::CPAN

my $test_ip        = '192.168.0.198';
my $test_ip2       = '192.168.0.199';
my $appuser_pass   = 'Morerugs';

subtest 'Class::User::DBI use and can tests.' => sub {
    my $user = use_ok( 'Class::User::DBI', [ $conn, $appuser ] );
    can_ok(
        'Class::User::DBI', qw(
          _db_conn          _db_run         add_ips         add_user
          configure_db      delete_ips      delete_user     exists_user
          get_credentials get_valid_ips get_role        is_role
          list_users        load_profile    new             set_role
          set_email         update_password set_username
          userid            validate        validated
          )
    );
    done_testing();
};

# Prepare the database environment.
# Drop tables if they exist (in case we're testing against a non-memory

t/21-cu_dbi.t  view on Meta::CPAN

    isa_ok( $user, 'Class::User::DBI', 'new():         ' );

    is( $user->userid, $appuser, 'userid():     Returns correct user ID.' );
    is( $user->validated, 0,
        'validated():    Returns false if user has not been validated yet.' );
    isa_ok( $user->_db_conn, 'DBIx::Connector', '_db_conn():     ' );

    my $query_handle = $user->_db_run( 'SELECT * FROM users', () );
    isa_ok( $query_handle, 'DBI::st', '_db_run():  ' );

    my $rv = $user->get_credentials();

    is( ref($rv), 'HASH', 'get_credentials():   Returns a hashref.' );
    ok( exists( $rv->{valid_ips} ),
        'get_credentials():   valid_ips   field found.' );
    ok( exists( $rv->{ip_required} ),
        'get_credentials():   ip_required field found.' );
    ok( exists( $rv->{salt_hex} ),
        'get_credentials():   salt_hex    field found.' );
    ok( exists( $rv->{pass_hex} ),
        'get_credentials():  pass_hex    field found.' );
    ok( exists( $rv->{userid} ), 'get_credentials():  userid    field found.' );
    is( $rv->{userid}, $appuser, 'get_credentials():  Correct userid found.' );
    is( ref( $rv->{valid_ips} ),
        'ARRAY', 'get_credentials():  valid_ips contains aref.' );
    is( $rv->{ip_required} == 0 || $rv->{ip_required} == 1,
        1, 'get_credentials():  ip_required is a Boolean value.' );
    like( $rv->{salt_hex}, qr/^[[:xdigit:]]{128}$/x,
        'get_credentials():  salt_hex has 128 hex digits.' );
    like( $rv->{pass_hex}, qr/^[[:xdigit:]]{128}$/x,
        'get_credentials():  pass_hex has 128 hex digits.' );
    is( scalar( $user->get_valid_ips ),
        0, "get_valid_ips():  $appuser has no IP's." );
    is( $user->exists_user, 1, "exists_user(): $appuser exists in DB." );
    is( $user->validate('wrong pass'),
        0, 'validate: Reject incorrect password with 0.' );
    is( $user->validated, 0,
        'validated():   Flag still false after rejected validation.' );

    is( $user->validate($appuser_pass),
        1, "validate(): $appuser validates by password." );

t/22-cudbi_working_example.t  view on Meta::CPAN


my $profile;

is( ref( $profile = $user->load_profile ),
    'HASH', 'load_profile returns a hashref.' );

foreach my $key (qw( username email domains role privileges )) {
    ok( exists $profile->{$key}, "$key profile attribute exists." );
}

my $credentials;

is( ref( $credentials = $user->get_credentials ),
    'HASH', 'get_credentials returns a hashref.' );

foreach my $key (qw( valid_ips ip_required salt_hex userid pass_hex )) {
    ok( exists $credentials->{$key}, "$key credentials attribute exists." );
}

ok( $user->delete_user, 'User deleted.' );

ok( !$user->validated, 'User is no longer valid.' );

ok( !$user->exists_user, 'User no longer exists.' );

done_testing();



( run in 0.336 second using v1.01-cache-2.11-cpan-4d50c553e7e )