RSS Git Download  Clone
Raw Blame History
package Dancer2::Plugin::Auth::Extensible::Provider::DB;

use Moo;
with "Dancer2::Plugin::Auth::Extensible::Role::Provider";
use Dancer2::Core::Types qw/HashRef Str/;
use namespace::clean;

has dbix => ( is => 'lazy' );

# these are passed in from config.yml (provider: DB):
has db_name => (
    is       => 'ro',
    isa      => Str,
    required => 1,
);
has db_cols=> (
    is       => 'ro',
    isa      => HashRef,
    required => 1,
);
has roles_key => (
    is      => 'ro',
    default => 'user_roles',
);
# has disable_roles => (); # causes fatal error, seems to be automatically picked up
no Moo;

our $VERSION = '0.705';

=begin
Provider class for Dancer2::Plugin::Auth::Extensible - uses DBIx::Simple and
SQL::Abstract::More for db queries; based on DPAE::Provider::Database

Expected methods: authenticate_user, get_user_details, get_user_roles

Can skip password if development env and param SKIP_PASSWORD used

How it works: DPAE authenticate_user() calls each realm in turn (hash so cannot
control the order!) and each realm provider (eg config, database, etc) uses its
own authenticate_user() to match username/password against its resource. 1st one
to get successful match wins.

native behaviour is user_details called x3 & user_roles x2 per request, but cannot
use global cache or a Moo attr as user data will persist between requests, so
adapted to use app session store for user data to save multiple db lookups

app settings = $self->plugin->app->settings
app session  = $self->plugin->app->session
=cut

use SQL::Abstract::More;
use Modern::Perl;
use DBIx::Simple; # don't need persistent dbh from Local::DB
use Data::Printer use_prototypes => 0, alias => 'ddp';
use Data::Dumper;

use Local::Utils; # sha1 digest
use Local::DB; # for dsn, not dbix

sub app_settings { shift->plugin->app->settings }
sub session_get  { shift->plugin->app->session->read(@_) }
sub session_set  { shift->plugin->app->session->write(@_) }
sub debug        { ddp $_[1] if $_[0]->app_settings->{environment} =~ /^dev/ }


#===============================================================================
sub authenticate_user { # warn 'here';
    my ($self, $username, $password) = @_; $self->debug('authenticating user');

    # Look up the user or return empty:
    my $user = $self->get_user_details($username) || return 0;

    # allow dev env to skip password:
    return 1 if $self->app_settings->{environment} eq 'development'
        && $ENV{SKIP_PASSWORD};

    # OK, we found a user, let match_password (from our base class) take care of
    # working out if the password is correct
    my $pwd_column_name = $self->db_cols->{users_password_column} || 'password';
	my $correct_pwd = $user->{$pwd_column_name};

    # have to do our own SHA as DPAE::Provider::Base uses Crypt::SaltedHash
    # then "straightforward comparison" of strings:
    my $sha_digest = Local::Utils::sha1_digest($password); # ddp $digest;
    return $self->match_password($sha_digest, $correct_pwd);
}

#===============================================================================
sub get_user_details { # warn 'here';
    my ($self, $username) = @_; # p $username;
    return unless defined $username; $self->debug('getting user_details');

    # return user details from session if exists:
    if ( my $user = $self->session_get('user_profile') ) { # ddp $user;
        $self->debug('returning user from session');
        return $user;
    } # warn 'here';

    my $settings = $self->db_cols; # ddp $settings;

    my $username_col = $settings->{users_username_col} || 'username';
    my $users_table  = $settings->{users_table}        || 'users';

    my $dbix = $self->dbix();
    my @cols = qw(id username last_name first_name password);

	my $user = $dbix->select( $users_table, \@cols,
        { $username_col => $username } )->hash; # ddp $user;

    if (! $user) {
        $self->debug("No such user $username");
        return 0;
    } # warn 'here';

    { # add roles for use in .tt (eg IF user_profile.$roles_key.foo):
        my $user_roles = $self->get_user_roles($username); # ddp $user_roles; # arrayref
        my $roles_key  = $self->roles_key; # ddp $roles_key;

        my %roles = map { $_ => 1 } @$user_roles;
        $user->{$roles_key} = \%roles;
    }
    # add user data to session:
    $self->session_set( user_profile => $user );
        # ddp $self->session_get('user_profile');
    return $user;
}

#===============================================================================
sub get_user_roles { # needs to return arrayref of roles;
    my ($self, $username) = @_; $self->debug('getting user_roles'); # ddp $username;

    # return roles from session if exists:
    if ( my $session = $self->session_get('user_profile') ) { # warn 'here';
        my $roles_key = $self->roles_key; # warn $roles_key;
        if ( my $roles = $session->{$roles_key} ) { # warn 'here'; # href
            $self->debug('returning user_roles from session'); # ddp $roles;
            # warn Dumper [ keys %$roles ]; # caller expects arrayref:
            return [ keys %$roles ];
        }
        else {
            warn qq!session didn't contain "$roles_key"!; # will continue to query db again
        }
    }

	my @roles = ();
	unless ( $self->disable_roles ) { # warn 'here';
		my ($sql, @bind) = $self->_get_query_params($username);
		@roles = $self->dbix->query($sql, @bind)->flat; # ddp \@roles;
	}
    return \@roles;
}

#===============================================================================
sub _get_query_params { # returns $sql & @bind
    my ($self, $username) = @_; # ddp $username;

    my $cols = $self->db_cols; # ddp $cols; # from config.yml db_cols
    # using views.user_permission in place of roles table:
    my $user_roles_user_id_col = $cols->{user_roles_user_id_col} || 'user_id';
#   my $user_roles_role_id_col = $cols->{user_roles_role_id_col} || 'role_id';
    my $users_username_column  = $cols->{users_username_column}  || 'username';
    my $user_roles_table       = $cols->{user_roles_table}       || 'user_role';
#   my $roles_role_id_col      = $cols->{roles_id_column}        || 'id';
    my $roles_role_col         = $cols->{roles_role_column}      || 'role';
    my $users_id_col           = $cols->{users_id_column}        || 'id';
#   my $roles_table            = $cols->{roles_table}            || 'roles';
    my $users_table            = $cols->{users_table}            || 'users';

    my @cols  = ( "t1.${roles_role_col}" );
    my @joins = (
        "$user_roles_table|t1"  =>
                        "t1.${user_roles_user_id_col}=t2.${users_id_col}",
        "$users_table|t2"
    );
    my %where = ( "t2.${users_username_column}" => $username );

    my @params = (
        -columns => \@cols,
        -from    => [ -join => @joins ],
        -where   => \%where,
    ); # ddp \@params;
    my ($sql, @bind) = SQL::Abstract::More->new->select(@params); # ddp [$sql, \@bind];
    return ($sql, @bind);
}

#===============================================================================
sub _build_dbix {
    my $self = shift; $self->debug('building dbix object');
    my $db   = $self->db_name;

=begin # use Local::DB->dbix to share db connection for test scripts & SQLite
    my @dsn  =
        $db eq 'test' # return in memory sqlite dbix object
        ? 'dbi:SQLite:dbname=:memory:'
        : Local::DB->dsn({ dbname => $db }); # ddp \@dsn;
    return DBIx::Simple->connect(@dsn);
=cut

    my %args = ();
    $db eq 'test' # return in memory sqlite dbix object
        ? ( $args{dsn} = 'dbi:SQLite:dbname=:memory:' )
        : ( $args{dbname} = $db ); # warn Dumper \%args;
    my $dbix = Local::DB->dbix(\%args);
    return $dbix;
}

1;