Commit c3b9b305 authored by Francesc Guasch's avatar Francesc Guasch
Browse files

Feature #904 ldap attributes (#911)

* Master is now release 0.3.1

* add indonesia translation (#875)

* test(frontend): list limited by LDAP attribute

issue #904

* wip(auth): check LDAP attributes on bases listing

issue #904

* test(ldap): check attribute access with 2 bases and clones

issue #904

* wip(ldap): fixed attributes fields in mysql tables

issue #904

* feature(auth): restrict access by LDAP attribute

closes issue #904
parent 2e1c3206
......@@ -1135,6 +1135,7 @@ sub _upgrade_tables {
);
$sth->execute;
}
$self->_upgrade_table('users','external_auth','char(32) DEFAULT NULL');
$self->_upgrade_table('networks','requires_password','int(11)');
$self->_upgrade_table('networks','n_order','int(11) not null default 0');
......
......@@ -51,7 +51,9 @@ sub login {
$login_ok = Ravada::Auth::LDAP->new(name => $name, password => $pass);
};
warn $@ if $@ && $LDAP_OK && !$quiet;
return $login_ok if $login_ok;
if ( $login_ok ) {
return $login_ok;
}
}
return Ravada::Auth::SQL->new(name => $name, password => $pass);
}
......
......@@ -313,6 +313,7 @@ sub login($self) {
if ($$CONFIG->{ldap}->{ravada_posix_group}) {
$allowed = search_user (name => $self->name, field => 'memberUid', base => $$CONFIG->{ldap}->{ravada_posix_group}) || 0;
$self->{_ldap_entry} = $allowed;
} else {
$allowed = 1;
}
......@@ -345,6 +346,7 @@ sub _login_bind {
my $mesg = $LDAP_ADMIN->bind($dn, password => $password);
if ( !$mesg->code() ) {
$self->{_auth} = 'bind';
$self->{_ldap_entry} = $user;
return 1;
}
warn "ERROR: ".$mesg->code." : ".$mesg->error. " : Bad credentials for $dn"
......@@ -353,6 +355,17 @@ sub _login_bind {
return 0;
}
=head2 ldap_entry
Returns the ldap entry as a Net::LDAP::Entry of the user if it has
LDAP external authentication
=cut
sub ldap_entry($self) {
return $self->{_ldap_entry};
}
sub _login_match {
my $self = shift;
my ($username, $password) = ($self->name , $self->password);
......@@ -373,7 +386,10 @@ sub _login_match {
# warn "ERROR: ".$mesg->code." : ".$mesg->error. " : Bad credentials for $username";
$user_ok = $self->_match_password($entry, $password);
warn $entry->dn." : $user_ok" if $Ravada::DEBUG;
last if $user_ok;
if ( $user_ok ) {
$self->{_ldap_entry} = $entry;
last;
}
}
if ($user_ok) {
......@@ -386,9 +402,15 @@ sub _login_match {
sub _check_user_profile {
my $self = shift;
my $user_sql = Ravada::Auth::SQL->new(name => $self->name);
return if $user_sql->id;
if ( $user_sql->id ) {
if ($user_sql->external_auth ne 'ldap') {
$user_sql->external_auth('ldap');
}
return;
}
Ravada::Auth::SQL::add_user(name => $self->name, is_external => 1, is_temporary => 0);
Ravada::Auth::SQL::add_user(name => $self->name, is_external => 1, is_temporary => 0
, external_auth => 'ldap');
}
sub _match_password {
......
......@@ -61,10 +61,11 @@ sub BUILD {
$self->_load_data();
return $self if !$self->password();
return if !$self->password();
die "ERROR: Login failed ".$self->name
if !$self->login();#$self->name, $self->password);
return $self;
}
......@@ -125,8 +126,9 @@ sub add_user {
my $is_admin = ($args{is_admin} or 0);
my $is_temporary= ($args{is_temporary} or 0);
my $is_external= ($args{is_external} or 0);
my $external_auth = $args{external_auth};
delete @args{'name','password','is_admin','is_temporary','is_external'};
delete @args{'name','password','is_admin','is_temporary','is_external', 'external_auth'};
confess "WARNING: Unknown arguments ".Dumper(\%args)
if keys %args;
......@@ -134,8 +136,8 @@ sub add_user {
my $sth;
eval { $sth = $$CON->dbh->prepare(
"INSERT INTO users (name,password,is_admin,is_temporary, is_external)"
." VALUES(?,?,?,?,?)");
"INSERT INTO users (name,password,is_admin,is_temporary, is_external, external_auth)"
." VALUES(?,?,?,?,?,?)");
};
confess $@ if $@;
if ($password) {
......@@ -143,7 +145,7 @@ sub add_user {
} else {
$password = '*LK* no pss';
}
$sth->execute($name,$password,$is_admin,$is_temporary, $is_external);
$sth->execute($name,$password,$is_admin,$is_temporary, $is_external, $external_auth);
$sth->finish;
$sth = $$CON->dbh->prepare("SELECT id FROM users WHERE name = ? ");
......@@ -321,6 +323,23 @@ sub remove_admin($self, $id) {
$self->grant_user_permissions($user);
}
=head2 external_auth
Sets or gets the external auth value of an user.
=cut
sub external_auth($self, $value=undef) {
if (!defined $value) {
return $self->{_data}->{external_auth};
}
my $sth = $$CON->dbh->prepare(
"UPDATE users set external_auth=? WHERE id=?"
);
$sth->execute($value, $self->id);
$self->_load_data();
}
=head2 is_admin
Returns true if the user is admin.
......@@ -997,6 +1016,24 @@ sub grants($self) {
return %{$self->{_grant}};
}
=head2 ldap_entry
Returns the ldap entry as a Net::LDAP::Entry of the user if it has
LDAP external authentication
=cut
sub ldap_entry($self) {
confess "Error: User ".$self->name." is not in LDAP external auth"
if $self->external_auth ne 'ldap';
return $self->{_ldap_entry} if $self->{_ldap_entry};
my @entries = Ravada::Auth::LDAP::search_user( name => $self->name );
$self->{_ldap_entry} = $entries[0];
return $self->{_ldap_entry};
}
sub AUTOLOAD($self, $domain=undef) {
......
......@@ -13,6 +13,9 @@ use Carp qw(confess croak);
use Data::Dumper;
use Moose::Role;
no warnings "experimental::signatures";
use feature qw(signatures);
requires 'add_user';
requires 'is_admin';
requires 'is_external';
......@@ -47,7 +50,10 @@ Internal OO builder
=cut
sub BUILD {
my $self = shift;
_init_connector();
$self->_load_allowed();
}
#####################################################
......@@ -290,4 +296,50 @@ sub _now {
return "$now[5]-$now[4]-$now[3] $now[2]:$now[1]:$now[0].0";
}
=head2 allowed_access
Return true if the user has access to clone a virtual machine
=cut
sub allowed_access($self,$id_domain) {
return 1 if $self->is_admin;
$self->_load_allowed();
# this domain has not access checks defined
return 1 if ! exists $self->{_allowed}->{$id_domain};
# return true if this user is allowed
return 1 if $self->{_allowed}->{$id_domain};
return 0;
}
sub _load_allowed {
my $self = shift;
my $refresh = shift;
return if !$refresh && $self->{_load_allowed}++;
return if !$self->external_auth || $self->external_auth ne 'ldap';
my $ldap_entry = $self->ldap_entry;
my $sth = $$CONNECTOR->dbh->prepare(
"SELECT id_domain, attribute, value, allowed "
." FROM access_ldap_attribute"
);
$sth->execute();
while ( my ($id_domain, $attribute, $value, $allowed) = $sth->fetchrow) {
if ($ldap_entry && defined $ldap_entry->get_value($attribute)
&& $ldap_entry->get_value($attribute) eq $value ) {
$self->{_allowed}->{$id_domain} = $allowed;
} else {
$self->{_allowed}->{$id_domain} = 0;
}
}
$sth->finish;
}
1;
......@@ -2997,4 +2997,31 @@ sub _post_change_controller {
my $self = shift;
$self->needs_restart(1) if $self->is_active;
}
=head2 Access restrictions
These methods implement access restrictions to clone a domain
=cut
=head2 allow_ldap_attribute
If specified, only the LDAP users with that attribute value can clone these
virtual machines.
$base->allow_ldap_attribute( attribute => 'value' );
Example:
$base->allow_ldap_attribute( tipology => 'student' );
=cut
sub allow_ldap_attribute($self, $attribute, $value) {
my $sth = $$CONNECTOR->dbh->prepare(
"INSERT INTO access_ldap_attribute "
."(id_domain, attribute, value, allowed) "
."VALUES(?,?,?,?)");
$sth->execute($self->id, $attribute, $value, 1);
}
1;
......@@ -15,6 +15,7 @@ use Hash::Util qw(lock_hash);
use JSON::XS;
use Moose;
use Ravada;
use Ravada::Auth::LDAP;
use Ravada::Front::Domain;
use Ravada::Front::Domain::KVM;
use Ravada::Network;
......@@ -141,6 +142,7 @@ sub list_machines_user {
my @list;
while ( $sth->fetch ) {
next if !$is_public && !$user->is_admin;
next if !$user->allowed_access($id);
my $is_active = 0;
my $clone = $self->search_clone(
id_owner =>$user->id
......
CREATE TABLE `access_ldap_attribute` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`id_domain` int(11),
`attribute` varchar(64),
`value` varchar(64),
`allowed` int not null default 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id_base` (`id_domain`,`attribute`,`value`)
);
CREATE TABLE `access_ldap_attribute` (
`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT
, `id_domain` integer
, `attribute` varchar(64)
, `value` varchar(64)
, `allowed` integer not null default 1
, UNIQUE (`id_domain`,`attribute`,`value`)
);
......@@ -71,11 +71,13 @@ sub test_user{
ok(!$mcnulty->is_admin,"User ".$mcnulty->name." should not be admin "
.Dumper($mcnulty->{_data}));
ok($mcnulty->ldap_entry,"Expecting User LDAP entry");
# try to login
my $mcnulty_login = Ravada::Auth::login($name,$password);
ok($mcnulty_login,"No login");
ok(ref $mcnulty_login && ref($mcnulty_login) eq 'Ravada::Auth::LDAP',
"ref should be Ravada::Auth::LDAP , got ".ref($mcnulty_login));
ok($mcnulty_login->ldap_entry,"Expecting User LDAP entry");
# check for the user in the SQL db
#
$sth = connector->dbh->prepare("SELECT * FROM users WHERE name=?");
......
use warnings;
use strict;
use Data::Dumper;
use Hash::Util qw(lock_hash);
use Test::More;
no warnings "experimental::signatures";
use feature qw(signatures);
use lib 't/lib';
use Test::Ravada;
use Ravada::Auth::LDAP;
my $CONFIG_FILE = 't/etc/ravada_ldap.conf';
init( $CONFIG_FILE );
delete $Ravada::CONFIG->{ldap}->{ravada_posix_group};
sub test_external_auth {
my ($name, $password) = ('jimmy','jameson');
create_ldap_user($name, $password);
my $login_ok;
eval { $login_ok = Ravada::Auth::login($name, $password) };
is($@, '');
ok($login_ok,"Expecting login with $name") or return;
ok($login_ok->ldap_entry,"Expecting a LDAP entry for user $name in object ".ref($login_ok));
my $user = Ravada::Auth::SQL->new(name => $name);
is($user->external_auth, 'ldap') or exit;
ok($user->ldap_entry,"Expecting a LDAP entry for user $name in object ".ref($user));
my $sth = connector->dbh->prepare(
"UPDATE users set external_auth = '' "
." WHERE id=?"
);
$sth->execute($user->id);
$user = Ravada::Auth::SQL->new(name => $name);
is($user->external_auth, '') or exit;
eval { $login_ok = Ravada::Auth::login($name, $password) };
is($@, '');
ok($login_ok,"Expecting login with $name") or return;
$user = Ravada::Auth::SQL->new(name => $name);
is($user->external_auth, 'ldap') or exit;
}
sub _create_users() {
my $data = {
student => { name => 'student', password => 'aaaaaaa' }
,teacher => { name => 'teacher', password => 'bbbbbbb' }
};
for my $type ( keys %$data) {
create_ldap_user($data->{$type}->{name}, $data->{$type}->{password});
my $login_ok;
eval { $login_ok = Ravada::Auth::login(
$data->{$type}->{name}
, $data->{$type}->{password})
};
is($@, '');
ok($login_ok,"Expecting login with $data->{$type}->{name}") or return;
$data->{$type}->{user} = Ravada::Auth::SQL->new(name => $data->{$type}->{name});
}
my $other = { name => 'other'.new_domain_name(), password => 'ccccccc' };
create_user($other->{name}, $other->{password});
$other->{user} = Ravada::Auth::SQL->new(name => $other->{name});
$data->{other} = $other;
ok($data->{other}->{user}->id);
return $data;
}
sub _refresh_users($data) {
for my $key (keys %$data) {
delete $data->{$key}->{user}->{_ldap_entry};
delete $data->{$key}->{user}->{_load_allowed};
}
}
sub _do_clones($data, $base, $do_clones) {
return if !$do_clones;
my $clone_student = $base->clone(
name => new_domain_name
,user => $data->{student}->{user}
);
my $clone_teacher= $base->clone(
name => new_domain_name
,user => $data->{teacher}->{user}
);
return ($clone_student, $clone_teacher);
}
sub test_access_by_attribute($vm, $do_clones=0) {
my $data = _create_users();
my $base = create_domain($vm->type);
$base->prepare_base(user_admin);
$base->is_public(1);
_do_clones($data, $base, $do_clones);
my $list_bases = rvd_front->list_machines_user($data->{student}->{user});
is(scalar (@$list_bases), 1);
#################################################################
#
# all should be allowed now
is($data->{student}->{user}->allowed_access( $base->id ), 1);
is($data->{teacher}->{user}->allowed_access( $base->id ), 1);
is($data->{other}->{user}->allowed_access( $base->id ), 1);
is(user_admin->allowed_access( $base->id ), 1);
$data->{student}->{user}->ldap_entry->replace( givenName => 'Jimmy');
my $mesg = $data->{student}->{user}->ldap_entry->update(Ravada::Auth::LDAP::_init_ldap_admin);
is($mesg->code,0, $mesg->error) or BAIL_OUT();
_refresh_users($data);
is($data->{student}->{user}->ldap_entry->get_value('givenName'),'Jimmy') or BAIL_OUT();
$base->allow_ldap_attribute( givenName => 'Jimmy');
#################################################################
#
# only students and admin should be allowed
is($data->{student}->{user}->allowed_access( $base->id ), 1);
is($data->{teacher}->{user}->allowed_access( $base->id ), 0);
is(user_admin->allowed_access( $base->id ), 1);
$list_bases = rvd_front->list_machines_user($data->{student}->{user});
is(scalar (@$list_bases), 1);
$list_bases = rvd_front->list_machines_user($data->{teacher}->{user});
is(scalar (@$list_bases), 0);
$list_bases = rvd_front->list_machines_user($data->{other}->{user});
is(scalar (@$list_bases), 1);
$list_bases = rvd_front->list_machines_user(user_admin);
is(scalar (@$list_bases), 1);
_remove_bases($base);
}
sub _create_bases($vm, $n=1) {
my @bases;
for (1 .. $n ) {
my $base = create_domain($vm->type);
$base->prepare_base(user_admin);
$base->is_public(1);
push @bases,($base);
}
return @bases;
}
sub _remove_bases(@bases) {
for my $base (@bases) {
for my $clone_data ($base->clones) {
my $clone = Ravada::Domain->open($clone_data->{id});
$clone->remove(user_admin);
}
$base->remove(user_admin);
}
}
sub test_access_by_attribute_2bases($vm, $do_clones=0) {
my $data = _create_users();
my @bases = _create_bases($vm,2);
_do_clones($data, $bases[0], $do_clones);
_do_clones($data, $bases[1], $do_clones);
my $list_bases = rvd_front->list_machines_user($data->{student}->{user});
is(scalar (@$list_bases), 2);
#################################################################
#
# all should be allowed now
for my $base ( @bases ) {
is($data->{student}->{user}->allowed_access( $base->id ), 1);
is($data->{teacher}->{user}->allowed_access( $base->id ), 1);
is(user_admin->allowed_access( $base->id ), 1);
}
$data->{student}->{user}->ldap_entry->replace( givenName => 'Jimmy');
my $mesg = $data->{student}->{user}->ldap_entry->update(Ravada::Auth::LDAP::_init_ldap_admin);
is($mesg->code,0, $mesg->error) or BAIL_OUT();
_refresh_users($data);
is($data->{student}->{user}->ldap_entry->get_value('givenName'),'Jimmy') or BAIL_OUT();
$bases[0]->allow_ldap_attribute( givenName => 'Jimmy');
#################################################################
#
# only students and admin should be allowed
is($data->{student}->{user}->allowed_access( $bases[0]->id ), 1);
is($data->{teacher}->{user}->allowed_access( $bases[0]->id ), 0);
is(user_admin->allowed_access( $bases[0]->id ), 1);
$list_bases = rvd_front->list_machines_user($data->{student}->{user});
is(scalar (@$list_bases), 2);
$list_bases = rvd_front->list_machines_user($data->{teacher}->{user});
is(scalar (@$list_bases), 1);
$list_bases = rvd_front->list_machines_user($data->{other}->{user});
is(scalar (@$list_bases), 1);
$list_bases = rvd_front->list_machines_user(user_admin);
is(scalar (@$list_bases), 2);
_remove_bases(@bases);
}
################################################################################
clean();
for my $vm_name ('KVM', 'Void') {
my $vm = rvd_back->search_vm($vm_name);
SKIP: {
my $msg = "SKIPPED: No virtual managers found";
if ($vm && $vm_name =~ /kvm/i && $>) {
$msg = "SKIPPED: Test must run as root";
$vm = undef;
}
skip($msg,10) if !$vm;
diag("Testing LDAP access for $vm_name");
test_external_auth();
test_access_by_attribute($vm);
test_access_by_attribute($vm,1); # with clones
}
}
clean();
done_testing();
......@@ -66,6 +66,7 @@ our $USER_ADMIN;
our $CHAIN = 'RAVADA';
our $RVD_BACK;
our $RVD_FRONT;
our %ARG_CREATE_DOM = (
KVM => []
......@@ -209,12 +210,15 @@ sub rvd_back($config=undef) {
return $rvd;
}
sub rvd_front() {
sub rvd_front($config=undef) {
return Ravada::Front->new(
return $RVD_FRONT if $RVD_FRONT;
$RVD_FRONT = Ravada::Front->new(