App-Dochazka-REST
view release on metacpan or search on metacpan
lib/App/Dochazka/REST/Auth.pm view on Meta::CPAN
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 {
$log->notice( "Employee $nick not found in LDAP; reverting to internal auth" );
# - check if this employee exists in database
my $emp = nick_exists( $dbix_conn, $nick );
if ( ! defined( $emp ) or ! $emp->isa( 'App::Dochazka::REST::Model::Employee' ) ) {
$log->notice( "Rejecting login attempt from unknown user $nick" );
$self->mrest_declare_status( explanation => "Authentication failed for user $nick", permanent => 1 );
return $CELL->status_not_ok;
}
# - the password might be empty
$password = '' unless defined( $password );
my $passhash = $emp->passhash;
$passhash = '' unless defined( $passhash );
# - check password against passhash
my ( $ppr, $status );
try {
$ppr = Authen::Passphrase::SaltedDigest->new(
algorithm => "SHA-512",
salt_hex => $emp->salt,
hash_hex => $emp->passhash,
);
} catch {
$status = $CELL->status_err( 'DOCHAZKA_PASSPHRASE_EXCEPTION', args => [ $_ ] );
};
if ( ref( $ppr ) ne 'Authen::Passphrase::SaltedDigest' ) {
$log->crit( "employee $nick has invalid passhash and/or salt" );
return $CELL->status_not_ok( 'DOCHAZKA_EMPLOYEE_AUTH' );
}
if ( $ppr->match( $password ) ) {
$log->notice( "Internal auth successful for employee $nick" );
return $CELL->status_ok( 'DOCHAZKA_EMPLOYEE_AUTH', payload => $emp );
} else {
$self->mrest_declare_status( explanation =>
"Internal auth failed for known employee $nick (mistyped password?)"
);
return $CELL->status_not_ok;
}
}
}
=head2 forbidden
This overrides the L<Web::Machine> method of the same name.
Authorization (ACL check) method.
First, parse the path and look at the method to determine which controller
action the user is asking us to perform. Each controller action has an ACL
associated with it, from which we can determine whether employees of each of
the four different privilege levels are authorized to perform that action.
Requests for non-existent resources will always pass the ACL check.
=cut
sub forbidden {
my ( $self ) = @_;
$log->debug( "Entering " . __PACKAGE__ . "::forbidden" );
my $method = $self->context->{'method'};
my $resource_name = $self->context->{'resource_name'};
# if there is no handler on the context, the URL is invalid so we
# just pass on the request
if ( not exists $self->context->{'handler'} ) {
$log->debug("forbidden: no handler on context, passing on this request");
return 0;
}
my $resource_def = $resources->{$resource_name}->{$method};
# now we get the ACL profile. There are three possibilities:
# 1. acl_profile property does not exist => fail
# 2. single ACL profile for the entire resource
# 3. separate ACL profiles for each HTTP method
my ( $acl_profile_prop, $acl_profile );
SKIP: {
# check acl_profile property
if ( exists( $resource_def->{'acl_profile'} ) ) {
$acl_profile_prop = $resource_def->{'acl_profile'};
} else {
$log->notice( "Resource $resource_name has no acl_profile property; ACL check will fail" );
last SKIP;
}
# got the property, process it
if ( ! ref( $acl_profile_prop ) ) {
$acl_profile = $acl_profile_prop;
$log->debug( "ACL profile for all methods is " . ( $acl_profile || "undefined" ) );
} elsif ( ref( $acl_profile_prop ) eq 'HASH' ) {
$acl_profile = $acl_profile_prop->{$method};
$log->debug( "ACL profile for $method requests is " . ( $acl_profile || "undefined" ) );
} else {
$self->mrest_declare_status( code => 500, explanation =>
( run in 1.035 second using v1.01-cache-2.11-cpan-d7f47b0818f )