Files
netdisco/lib/App/Netdisco/Web/Auth/Provider/DBIC.pm
Oliver Gorwits dff26abc5c API implementation (#712)
* initial v0 creator

* working json api for generic reports

* add require login

* move report swagger into plugin, and set new default layout of noop

* require proper role and also use new util func

* start to tidy authn

* some work on cleaning up web authn

* clean up the authN checks

* fix bug

* fix the auth for api

* fixes to json handling

* set swagger sort order

* enable most reports for api endpoints

* fix doc

* add paramters to reports

* add missed report

* allow api_parameters in reports config

* reorganise api

* add vlan search

* add port search

* make sure to enable layout processing

* add device search

* add v1 to api paths

* add Node Search

* support api_responses

* add device object search; fix spurious ports field in device result class

* handle some plugins just returning undef if search fails

* errors from api seamlessley

* fix error in date range default

* more sensible default for prefix

* change order of endpoints in swagger-ui

* all db row classes can now TO_JSON

* add device_port api endpoint

* add device ports endpoint

* do not expand docs

* add swagger ui json tree formatter

* add all relations from Device table

* add port relations

* add nodes retrieve on device or vlan

* rename to GetAPIKey

* update config for previous commit
2020-04-15 21:15:52 +01:00

280 lines
8.0 KiB
Perl
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package App::Netdisco::Web::Auth::Provider::DBIC;
use strict;
use warnings;
use base 'Dancer::Plugin::Auth::Extensible::Provider::Base';
# with thanks to yanick's patch at
# https://github.com/bigpresh/Dancer-Plugin-Auth-Extensible/pull/24
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Passphrase;
use Digest::MD5;
use Net::LDAP;
use Authen::Radius;
use Authen::TacacsPlus;
use Try::Tiny;
sub authenticate_user {
my ($self, $username, $password) = @_;
return unless defined $username;
my $user = $self->get_user_details($username) or return;
return unless $user->in_storage;
return $self->match_password($password, $user);
}
sub get_user_details {
my ($self, $username) = @_;
my $settings = $self->realm_settings;
my $database = schema($settings->{schema_name})
or die "No database connection";
my $users_table = $settings->{users_resultset} || 'User';
my $username_column = $settings->{users_username_column} || 'username';
my $user = try {
$database->resultset($users_table)->find({
$username_column => { -ilike => quotemeta($username) },
});
};
# each of these settings permits no user in the database
# so create a pseudo user entry instead
if (not $user and not setting('validate_remote_user')
and (setting('trust_remote_user')
or setting('trust_x_remote_user')
or setting('no_auth'))) {
$user = $database->resultset($users_table)
->new_result({username => $username});
}
return $user;
}
sub validate_api_token {
my ($self, $token) = @_;
return unless defined $token;
my $settings = $self->realm_settings;
my $database = schema($settings->{schema_name})
or die "No database connection";
my $users_table = $settings->{users_resultset} || 'User';
my $token_column = $settings->{users_token_column} || 'token';
$token =~ s/^Apikey //i; # should be there but swagger-ui doesn't add it
my $user = try {
$database->resultset($users_table)->find({ $token_column => $token });
};
return $user->username
if $user and $user->in_storage and $user->token_from
and $user->token_from > (time - setting('api_token_lifetime'));
return undef;
}
sub get_user_roles {
my ($self, $username) = @_;
return unless defined $username;
my $settings = $self->realm_settings;
my $database = schema($settings->{schema_name})
or die "No database connection";
# Get details of the user first; both to check they exist, and so we have
# their ID to use.
my $user = $self->get_user_details($username)
or return;
my $roles = $settings->{roles_relationship} || 'roles';
my $role_column = $settings->{role_column} || 'role';
return [ try {
$user->$roles->search({}, { bind => [setting('api_token_lifetime')] })
->get_column( $role_column )->all;
} ];
}
sub match_password {
my($self, $password, $user) = @_;
return unless $user;
my $settings = $self->realm_settings;
my $username_column = $settings->{users_username_column} || 'username';
my $pwmatch_result = 0;
my $username = $user->$username_column;
if ($user->ldap) {
$pwmatch_result = $self->match_with_ldap($password, $username);
}
elsif ($user->radius) {
$pwmatch_result = $self->match_with_radius($password, $username);
}
elsif ($user->tacacs) {
$pwmatch_result = $self->match_with_tacacs($password, $username);
}
else {
$pwmatch_result = $self->match_with_local_pass($password, $user);
}
return $pwmatch_result;
}
sub match_with_local_pass {
my($self, $password, $user) = @_;
my $settings = $self->realm_settings;
my $password_column = $settings->{users_password_column} || 'password';
return unless $password and $user->$password_column;
if ($user->$password_column !~ m/^{[A-Z]+}/) {
my $sum = Digest::MD5::md5_hex($password);
if ($sum eq $user->$password_column) {
if (setting('safe_password_store')) {
# upgrade password if successful, and permitted
$user->update({password => passphrase($password)->generate});
}
return 1;
}
else {
return 0;
}
}
else {
return passphrase($password)->matches($user->$password_column);
}
}
sub match_with_ldap {
my($self, $pass, $user) = @_;
return unless setting('ldap') and ref {} eq ref setting('ldap');
my $conf = setting('ldap');
my $ldapuser = $conf->{user_string};
$ldapuser =~ s/\%USER\%?/$user/egi;
# If we can bind as anonymous or proxy user,
# search for user's distinguished name
if ($conf->{proxy_user}) {
my $user = $conf->{proxy_user};
my $pass = $conf->{proxy_pass};
my $attrs = ['distinguishedName'];
my $result = _ldap_search($ldapuser, $attrs, $user, $pass);
$ldapuser = $result->[0] if ($result->[0]);
}
# otherwise, if we can't search and aren't using AD and then construct DN
# by appending base
elsif ($ldapuser =~ m/=/) {
$ldapuser = "$ldapuser,$conf->{base}";
}
foreach my $server (@{$conf->{servers}}) {
my $opts = $conf->{opts} || {};
my $ldap = Net::LDAP->new($server, %$opts) or next;
my $msg = undef;
if ($conf->{tls_opts} ) {
$msg = $ldap->start_tls(%{$conf->{tls_opts}});
}
$msg = $ldap->bind($ldapuser, password => $pass);
$ldap->unbind(); # take down session
return 1 unless $msg->code();
}
return undef;
}
sub _ldap_search {
my ($filter, $attrs, $user, $pass) = @_;
my $conf = setting('ldap');
return undef unless defined($filter);
return undef if (defined $attrs and ref [] ne ref $attrs);
foreach my $server (@{$conf->{servers}}) {
my $opts = $conf->{opts} || {};
my $ldap = Net::LDAP->new($server, %$opts) or next;
my $msg = undef;
if ($conf->{tls_opts}) {
$msg = $ldap->start_tls(%{$conf->{tls_opts}});
}
if ( $user and $user ne 'anonymous' ) {
$msg = $ldap->bind($user, password => $pass);
}
else {
$msg = $ldap->bind();
}
$msg = $ldap->search(
base => $conf->{base},
filter => "($filter)",
attrs => $attrs,
);
$ldap->unbind(); # take down session
my $entries = [$msg->entries];
return $entries unless $msg->code();
}
return undef;
}
sub match_with_radius {
my($self, $pass, $user) = @_;
return unless setting('radius') and ref {} eq ref setting('radius');
my $conf = setting('radius');
my $radius = Authen::Radius->new(Host => $conf->{server}, Secret => $conf->{secret});
# my $dict_dir = Path::Class::Dir->new( dist_dir('App-Netdisco') )
#  ->subdir('radius_dictionaries')->stringify;
Authen::Radius->load_dictionary(); # put $dict_dir in here once it's useful
$radius->add_attributes(
{ Name => 'User-Name', Value => $user },
{ Name => 'User-Password', Value => $pass },
{ Name => 'h323-return-code', Value => '0' }, # Cisco AV pair
{ Name => 'Digest-Attributes', Value => { Method => 'REGISTER' } }
);
$radius->send_packet(ACCESS_REQUEST);
my $type = $radius->recv_packet();
my $radius_return = ($type eq ACCESS_ACCEPT) ? 1 : 0;
return $radius_return;
}
sub match_with_tacacs {
my($self, $pass, $user) = @_;
return unless setting('tacacs') and ref {} eq ref setting('tacacs');
my $conf = setting('tacacs');
my $tacacs = new Authen::TacacsPlus(Host => $conf->{server}, Key => $conf->{key});
if (not $tacacs) {
debug sprintf('auth error: Authen::TacacsPlus: %s', Authen::TacacsPlus::errmsg());
return undef;
}
my $tacacs_return = $tacacs->authen($user,$pass);
if (not $tacacs_return) {
debug sprintf('error: Authen::TacacsPlus: %s', Authen::TacacsPlus::errmsg());
}
$tacacs->close();
return $tacacs_return;
}
1;