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;