App-Dochazka-REST
view release on metacpan or search on metacpan
lib/App/Dochazka/REST/Auth.pm view on Meta::CPAN
use App::CELL qw( $CELL $log $meta $site );
use App::Dochazka::REST;
use App::Dochazka::REST::ConnBank qw( $dbix_conn conn_status );
use App::Dochazka::REST::ACL qw( check_acl );
use App::Dochazka::REST::LDAP qw( ldap_exists ldap_search ldap_auth );
use App::Dochazka::REST::Model::Employee qw( autocreate_employee nick_exists );
use Authen::Passphrase::SaltedDigest;
use Data::Dumper;
use Params::Validate qw(:all);
use Try::Tiny;
use Web::Machine::Util qw( create_header );
use Web::MREST::InitRouter qw( $resources );
# methods/attributes not defined in this module will be inherited from:
use parent 'Web::MREST::Entity';
=head1 NAME
App::Dochazka::REST::Auth - HTTP request authentication and authorization
=head1 DESCRIPTION
This package, which is between L<Web::MREST::Entity> and
L<Web::Dochazka::REST::Dispatch> in the chain of inheritance, provides the
'is_authorized' and 'forbidden' methods called by L<Web::Machine> on each
incoming HTTP request.
=head1 METHODS
=head2 is_authorized
This overrides the L<Web::Machine> method of the same name.
Authenticate the originator of the request, using HTTP Basic Authentication.
Upon successful authentication, check that the user (employee) exists in
the database (create if necessary) and retrieve her EID. Push the EID and
current privilege level onto the context. Get the user's L<DBIx::Connector>
object and push that onto the context, too.
=cut
sub is_authorized {
my ( $self, $auth_header ) = @_;
$log->debug( "Entering " . __PACKAGE__ . "::is_authorized" );
# get database connection for this HTTP request
App::Dochazka::REST::ConnBank::init_singleton();
if ( ! $meta->META_DOCHAZKA_UNIT_TESTING ) {
return 1 if $self->_validate_session;
}
if ( $auth_header ) {
$log->debug("is_authorized: auth header is $auth_header" );
my $username = $auth_header->username;
my $password = $auth_header->password;
my $auth_status = $self->_authenticate( $username, $password );
if ( $auth_status->ok ) {
my $emp = $auth_status->payload;
$self->push_onto_context( {
current => $emp->TO_JSON,
current_obj => $emp,
current_priv => $emp->priv( $dbix_conn ),
dbix_conn => $dbix_conn,
} );
$self->_init_session( $emp ) unless $meta->META_DOCHAZKA_UNIT_TESTING;
return 1;
} else {
$log->error(
"_authenticate returned non-OK status. The entire status object is " .
Dumper( $auth_status )
);
}
}
return create_header(
'WWWAuthenticate' => [
'Basic' => (
realm => $site->DOCHAZKA_BASIC_AUTH_REALM
)
]
);
}
=head3 _init_session
Initialize the session. Takes an employee object.
=cut
sub _init_session {
my $self = shift;
$log->debug( "Entering " . __PACKAGE__ . "::_init_session" );
my ( $emp ) = validate_pos( @_, { type => HASHREF, can => 'eid' } );
my $r = $self->request;
my $ip_addr = $r->{'env'}->{'REMOTE_ADDR'};
my $session = $r->{'env'}->{'psgix.session'};
my $eid = $emp->eid;
$session->{'eid'} = $eid;
$session->{'ip_addr'} = $ip_addr;
$session->{'last_seen'} = time;
$log->info( "Initialized new session, EID $eid" );
return;
}
=head3 _validate_session
Validate the session
=cut
sub _validate_session {
my ( $self ) = @_;
$log->debug( "Entering " . __PACKAGE__ . "::_validate_session" );
my $r = $self->request;
my $remote_addr = $r->{'env'}->{'REMOTE_ADDR'};
my $session = $r->{'env'}->{'psgix.session'};
$log->debug( "Session is " . Dumper( $session ) );
return 0 unless %$session;
return 0 unless _is_fresh( $session->{'last_seen'} );
return 0 unless $session->{'ip_addr'} eq $remote_addr;
return 0 unless exists( $session->{'eid'} ) and $session->{'eid'};
$log->info( "Detected valid existing session" .
", EID " . $session->{'eid'} .
", last seen " . $session->{'last_seen'}
);
$session->{'last_seen'} = time;
my $emp = App::Dochazka::REST::Model::Employee->load_by_eid( $dbix_conn, $session->{'eid'} )->payload;
die "missing employee object in session management"
unless $emp->isa( "App::Dochazka::REST::Model::Employee" );
$self->push_onto_context( {
current => $emp->TO_JSON,
current_obj => $emp,
current_priv => $emp->priv( $dbix_conn ),
dbix_conn => $dbix_conn,
} );
return 1;
}
=head3 _is_fresh
Takes a single argument, which is assumed to be number of seconds since
epoch when the session was last seen. This is compared to "now" and if the
difference is greater than the DOCHAZKA_REST_SESSION_EXPIRATION_TIME site
parameter, the return value is false, otherwise true.
=cut
sub _is_fresh {
$log->debug( "Entering " . __PACKAGE__ . "::_is_fresh" );
my ( $last_seen ) = validate_pos( @_, { type => SCALAR } );
if ( time - $last_seen > $site->DOCHAZKA_REST_SESSION_EXPIRATION_TIME ) {
$log->error( "Session expired!" );
return 0;
}
return 1;
}
=head3 _authenticate
Authenticate the nick associated with an incoming REST request. Takes a nick
and a password (i.e., a set of credentials). Returns a status object, which
will have level 'OK' on success (with employee object in the payload), 'NOT_OK'
on failure. In the latter case, there will be a declared status.
=cut
sub _authenticate {
my ( $self, $nick, $password ) = @_;
my ( $status, $emp );
$log->debug( "Entering " . __PACKAGE__ . "::_authenticate" );
# empty credentials: fall back to demo/demo
if ( $nick ) {
$log->notice( "Login attempt from $nick" );
} else {
$log->notice( "Login attempt from (anonymous) -- defaulting to demo/demo" );
$nick = 'demo';
$password = 'demo';
}
$log->debug( "\$site->DOCHAZKA_LDAP is " . $site->DOCHAZKA_LDAP );
# check if LDAP is enabled and if the employee exists in LDAP
if ( ! $meta->META_DOCHAZKA_UNIT_TESTING and
$site->DOCHAZKA_LDAP and
ldap_exists( $nick )
) {
$log->info( "Detected authentication attempt from $nick, a known LDAP user" );
#$log->debug( "Password provided: $password" );
# - authenticate by LDAP bind
if ( ldap_auth( $nick, $password ) ) {
# successful LDAP auth: if the employee doesn't already exist in
# the database, possibly autocreate
$status = autocreate_employee( $dbix_conn, $nick );
return $status unless $status->ok;
} else {
return $CELL->status_not_ok( 'DOCHAZKA_EMPLOYEE_AUTH' );
}
# load the employee object
my $emp = App::Dochazka::REST::Model::Employee->load_by_nick( $dbix_conn, $nick )->payload;
die "missing employee object in _authenticate" unless ref($emp) eq "App::Dochazka::REST::Model::Employee";
return $CELL->status_ok( 'DOCHAZKA_EMPLOYEE_AUTH', payload => $emp );
}
# if not, authenticate against the password stored in the employee object.
else {
( run in 0.493 second using v1.01-cache-2.11-cpan-df04353d9ac )