Unverified Commit 0e544a09 authored by Francesc Guasch's avatar Francesc Guasch Committed by GitHub
Browse files

Refactor(auth): groups (#1568)

feat(auth): manage LDAP groups and members

* refactor: show disabled buttons while loading
* feat(auth): manage posix and non posix groups
* fix: enforce group on match login
* refactor: polished frontend settings and groups as suggested by @wisaias
* test: group LDAP
* improved error messages
parent e4bdb5aa
......@@ -153,7 +153,7 @@ sub _get_gid() {
my ($group_users) = grep { $_->get_value('cn') eq 'users' } @group;
$group_users = $group[0] if !$group_users;
if (!$group_users) {
add_group(name => 'users');
add_group('users');
($group_users) = search_group(name => 'users');
confess "Error: I can create nor find LDAP group 'users'" if !$group_users;
}
......@@ -279,6 +279,8 @@ sub search_user {
my $escape_username = 1;
$escape_username = delete $args{escape_username} if exists $args{escape_username};
my $filter_orig = delete $args{filter};
my $sizelimit = (delete $args{sizelimit} or 100);
my $timelimit = (delete $args{timelimit} or 60);
confess "ERROR: Unknown fields ".Dumper(\%args) if keys %args;
confess "ERROR: I can't connect to LDAP " if!$ldap;
......@@ -299,7 +301,9 @@ sub search_user {
scope => 'sub',
filter => $filter,
typesonly => $typesonly,
attrs => ['*']
attrs => ['*'],
sizelimit => $sizelimit,
timelimit => $timelimit
);
warn "LDAP retry ".$mesg->code." ".$mesg->error if $retry > 1;
......@@ -317,6 +321,7 @@ sub search_user {
,retry => ++$retry
,typesonly => $typesonly
,filter => $filter_orig
,sizelimit => $sizelimit
);
}
......@@ -336,27 +341,27 @@ Add a group to the LDAP
=cut
sub add_group {
my $name = shift;
my $base = (shift or _dc_base());
my $class = ( shift or [
'groupOfUniqueNames','nsMemberOf','posixGroup','top'
]);
sub add_group($name, $base=_dc_base(), $class=['groupOfUniqueNames','nsMemberOf','posixGroup','top' ]) {
$base = _dc_base() if !defined $base;
$name = escape_filter_value($name);
my $oc_posix_group;
$oc_posix_group = grep { /^posixGroup$/ } @$class;
my $mesg = $LDAP_ADMIN->add(
cn => $name
,dn => "cn=$name,ou=groups,$base"
, attrs => [ cn=>$name
my @attrs =( cn=>$name
,objectClass => $class
,ou => 'Groups'
,description => "Group for $name"
,gidNumber => _search_new_gid()
]
);
push @attrs, (gidNumber => _search_new_gid()) if $oc_posix_group;
my @data = (
dn => "cn=$name,ou=groups,$base"
, cn => $name
, attrs => \@attrs
);
my $mesg = $LDAP_ADMIN->add(@data);
if ($mesg->code) {
die "Error creating group $name : ".$mesg->error."\n";
die "Error creating group $name : ".$mesg->error."\n".Dumper(\@data);
}
}
......@@ -408,22 +413,38 @@ sub remove_group {
sub search_group {
my %args = @_;
my $name = delete $args{name};
my $name = delete $args{name} or confess "Error: missing name";
my $base = ( delete $args{base} or "ou=groups,"._dc_base() );
my $ldap = ( delete $args{ldap} or _init_ldap_admin());
my $retry =( delete $args{retry} or 0);
confess "ERROR: Unknown fields ".Dumper(\%args) if keys %args;
confess "ERROR: I can't connect to LDAP " if!$ldap;
my $filter = "cn=$name";
my $mesg = $ldap ->search (
filter => "cn=$name"
filter => $filter
,base => $base
,sizelimit => 100
);
warn "LDAP retry ".$mesg->code." ".$mesg->error if $retry > 1;
warn "LDAP retry ".$mesg->code." ".$mesg->error." [filter: $filter , base: $base]" if $retry > 1;
if ($mesg->code == 4 ) {
if ( $name eq '*' ) {
$name = 'a*';
} elsif ($name eq 'a*' ) {
$name = 'a*a*';
} else {
die "LDAP error: ".$mesg->code." ".$mesg->error;
}
return search_group(
name => $name
,base => $base
,ldap => $ldap
,retry => $retry+1
);
}
if ( $retry <= 3 && $mesg->code){
warn "LDAP error ".$mesg->code." ".$mesg->error."."
warn "LDAP error ".$mesg->code." ".$mesg->error.". [cn=$name] "
."Retrying ! [$retry]" if $retry;
$LDAP_ADMIN = undef;
sleep ($retry + 1);
......@@ -437,7 +458,6 @@ sub search_group {
my @entries = $mesg->entries;
return @entries if wantarray;
return @entries if wantarray;
return $entries[0];
}
......@@ -445,28 +465,78 @@ sub search_group {
Adds user to group
add_to_group($uid, $group_name);
add_to_group($dn, $group_name);
=cut
sub add_to_group {
my ($uid, $group_name) = @_;
my @user = search_user(name => $uid) or die "No such user $uid";
warn "Found ".scalar(@user)." users $uid , getting the first one ".Dumper(\@user)
if scalar(@user)>1;
my $user = $user[0];
my ($dn, $group_name) = @_;
if ( $dn !~ /=.*,/ ) {
my $user = search_user(name => $dn) or confess "Error: user '$dn' not found";
$dn = $user->dn;
}
my $group = search_group(name => $group_name, ldap => $LDAP_ADMIN)
or die "No such group $group_name";
$group->add(uniqueMember=> $user->dn);
if ( grep {/^groupOfNames$/} $group->get_value('objectClass') ) {
$group->add(member => $dn)
} elsif ( grep {/^posixGroup$/} $group->get_value('objectClass') ) {
my ($cn) = $dn =~ /^cn=(.*?),/;
($cn) = $dn =~ /^uid=(.*?),/ if !$cn;
die "Error: I can't find cn in $dn" if !$cn;
my @attributes = $group->attributes;
my $attribute;
for (qw(uniqueMember memberUid)) {
$attribute = $_ if grep /^$_$/,@attributes;
}
($attribute) = grep /member/i,@attributes if !$attribute;
if ($attribute eq 'memberUid') {
$group->add($attribute => $cn);
} else {
$group->add($attribute => $dn);
}
} else {
die "Error: group $group_name class unknown ".Dumper($group->get_value('objectClass'));
}
my $mesg = $group->update($LDAP_ADMIN);
die $mesg->error if $mesg->code;
die "Error: adding member ".$dn." ".$mesg->error if $mesg->code;
}
=head2 remove_from_group
Removes user from group
add_to_group($dn, $group_name);
=cut
sub remove_from_group {
my ($dn, $group_name) = @_;
my $group = search_group(name => $group_name, ldap => $LDAP_ADMIN)
or die "No such group $group_name";
my $found = 0;
for my $attribute ( $group->attributes() ) {
next if $attribute !~ /member/i;
my $uid = $dn;
$uid =~s/.*?=(.*?),.*/$1/ if $attribute eq 'memberUid';
my $mesg = $group->delete($attribute => $uid )->update(_init_ldap_admin());
die "Error: [".$mesg->code."] removing $uid from $group_name - $attribute ".$mesg->error
if $mesg->code;
$found++;
}
die "Error: group $group_name class unknown ".Dumper($group->get_value('objectClass'))
if !$found;
}
=head2 login
$user->login($name, $password);
......@@ -495,10 +565,35 @@ sub _search_posix_group($self, $name) {
return $posix_group[0];
}
sub login($self) {
my $user_ok;
my $allowed;
sub group_members {
return _group_members(@_);
}
sub _group_members($group_name = $$CONFIG->{ldap}->{group}) {
my $group = $group_name;
if (!ref($group)) {
$group = search_group(name => $group_name);
if (!$group) {
confess "Warning: group $group_name not found";
return;
}
}
confess "Error: invalid object ".ref($group) if ref($group)!~ /^Net::LDAP/;
my @oc = $group->get_value('objectClass');
my @members;
for my $attribute ($group->attributes) {
next if $attribute !~ /member/i;
push @members, $group->get_value($attribute);
}
my %members = map { $_ => 1 } @members;
@members = sort keys %members;
return @members;
}
sub _check_posix_group($self) {
my $posix_group_name = $$CONFIG->{ldap}->{ravada_posix_group};
return 1 if !$posix_group_name;
if ($posix_group_name) {
my $posix_group = $self->_search_posix_group($posix_group_name);
......@@ -516,6 +611,13 @@ sub login($self) {
}
$self->{_ldap_entry} = $posix_group;
}
}
sub login($self) {
my $user_ok;
my $allowed;
return if !$self->_check_posix_group();
$user_ok = $self->_login_bind()
if !exists $$CONFIG->{ldap}->{auth} || $$CONFIG->{ldap}->{auth} =~ /bind|all/i;
......@@ -543,19 +645,28 @@ sub _login_bind {
@user = (search_user(name => $self->name, field => 'uid')
,search_user(name => $self->name, field => 'cn'));
}
for my $user (@user) {
my $dn = $user->dn;
my %user = map { $_->dn => $_ } @user;
my @error;
for my $dn ( sort keys %user ) {
if ($$CONFIG->{ldap}->{group} && !is_member($dn,$$CONFIG->{ldap}->{group})) {
push @error, ("Warning: $dn does not belong to group $$CONFIG->{ldap}->{group}");
next;
}
$found++;
my $ldap;
eval { $ldap = _connect_ldap($dn, $password) };
warn "ERROR: Bad credentials for $dn"
if $Ravada::DEBUG && $@;
if ( $ldap ) {
$self->{_auth} = 'bind';
$self->{_ldap_entry} = $user;
$self->{_ldap_entry} = $user{$dn} if $user{$dn};
return 1;
}
warn "ERROR: Bad credentials for $dn"
if $Ravada::DEBUG && $@;
push @error,("ERROR: Bad credentials for $dn");
}
warn Dumper(\@error)
if $Ravada::DEBUG && scalar (@error);
return 0;
}
......@@ -580,9 +691,13 @@ sub _login_match {
my @entries = search_user($username);
my @error;
for my $entry (@entries) {
if ($$CONFIG->{ldap}->{group} && !is_member($entry->dn,$$CONFIG->{ldap}->{group})) {
push @error, ("Warning: ".$entry->dn.." does not belong to group $$CONFIG->{ldap}->{group}");
next;
}
# my $mesg;
# eval { $mesg = $LDAP->bind( $user_dn, password => $password )};
# return 1 if $mesg && !$mesg->code;
......@@ -601,6 +716,8 @@ sub _login_match {
$self->{_auth} = 'match';
}
warn Dumper(\@error)
if $Ravada::DEBUG && scalar (@error);
return $user_ok;
}
......@@ -727,6 +844,8 @@ sub _connect_ldap {
die "ERROR: ".$mesg->code." : ".$mesg->error. " : Bad credentials for $dn\n"
if $mesg->code;
} else {
return;
}
return $ldap;
......@@ -784,10 +903,44 @@ sub is_admin {
};
my ($user) = search_user($self->name);
my $dn = $user->dn;
return grep /^$dn$/,$group->get_value('uniqueMember');
return is_member($self->name, $admin_group);
}
=head2 is_member
Returns if an user is member of a group
if (is_member($group, $cn)) {
}
=cut
sub is_member($cn, $group) {
my $user;
my $dn;
if (ref($cn) && ref($cn) =~ /Net::LDAP::Entry/) {
$user = $cn;
$cn = $user->get_value('cn');
$dn = $user->dn;
} elsif($cn=~/=.*,/) {
$dn = $cn;
$cn =~ s/.*?=(.*?),.*/$1/;
}
my @members = _group_members($group);
return 1 if grep /^$cn$/, @members;
$user = search_user($cn) or confess "Error: unknown user '$cn'"
if !$user;
$dn = $user->dn if !$dn;
return 1 if grep /^$dn$/, @members;
my $group_name = $group;
$group_name = $group->dn if ref($group);
return 0;
}
=head2 is_external
......
......@@ -791,6 +791,7 @@ ravadaApp.directive("solShowMachine", swMach)
function admin_groups_ctrl($scope, $http) {
var group;
$scope.group_filter = '';
$scope.username_filter = 'a';
$scope.list_ldap_groups = function() {
$http.get('/list_ldap_groups/'+$scope.group_filter)
.then(function(response) {
......@@ -805,9 +806,14 @@ ravadaApp.directive("solShowMachine", swMach)
});
};
$scope.list_users = function() {
$http.get('/list_ldap_users')
$scope.loading_users = true;
$scope.error = '';
$http.get('/list_ldap_users/'+$scope.username_filter)
.then(function(response) {
$scope.users=response.data;
$scope.loading_users = false;
$scope.error = response.data.error;
$scope.users = response.data.entries;
console.log(response.data.error);
});
};
$scope.add_member = function(cn) {
......@@ -821,11 +827,11 @@ ravadaApp.directive("solShowMachine", swMach)
$scope.error = response.data.error;
});
};
$scope.remove_member = function(cn) {
$scope.remove_member = function(dn) {
$http.post("/ldap/group/remove_member/"
,JSON.stringify(
{ 'group': group
,'cn': cn
,'dn': dn
})
).then(function(response) {
$scope.list_group_members(group);
......
......@@ -693,7 +693,7 @@ sub test_ldap {
print "password: ";
my $password = <STDIN>;
chomp $password;
my $ok= Ravada::Auth::login( $name, $password);
my $ok= Ravada::Auth::LDAP->new(name => $name, password => $password);
if ($ok) {
if (!$ok->{_ldap_entry}) {
warn "No LDAP data found ".Dumper($ok->{_data});
......
......@@ -128,6 +128,8 @@ our $SESSION_TIMEOUT_ADMIN2 = ($CONFIG_FRONT->{session_timeout_admin2} or 60 * 6
my $WS = Ravada::WebSocket->new(ravada => $RAVADA);
my %ALLOWED_ANONYMOUS_WS = map { $_ => 1 } qw(list_bases_anonymous list_alerts);
my %LDAP_ATTRIBUTES;
my %LDAP_USERS;
# TOODO: config this variable
my $LIMIT_SHOW_USERS = 25;
......@@ -311,6 +313,8 @@ get '/admin/group/#name' => sub($c) {
push @{$c->stash->{css}}, '/css/admin.css';
push @{$c->stash->{js}}, '/js/admin.js';
my $group = Ravada::Auth::LDAP::search_group(name => $c->stash('name'));
$c->stash(object_class => [$group->get_value('objectClass')]);
return $c->render( template => "/main/admin_group");
};
......@@ -1581,6 +1585,14 @@ get '/list_ldap_users' => sub($c) {
return _list_ldap_users($c,'*');
};
get '/list_ldap_users/#name' => sub($c) {
return access_denied($c) unless $USER->can_manage_users || $USER->can_manage_groups;
my $name = $c->stash('name');
$name .= '*' unless $name =~ m{\*$};
return _list_ldap_users($c,$name);
};
get '/list_ldap_groups/:name' => sub($c) {
my $name = $c->stash('name');
......@@ -1603,21 +1615,15 @@ post '/ldap/group/add_member' => sub($c) {
my $arg = decode_json($c->req->body);
my $login = delete $arg->{cn};
my $new_group = delete $arg->{group};
my $group = delete $arg->{group};
my @groups = Ravada::Auth::LDAP::search_group( name => $new_group);
my ($group_ldap) = grep {$_->get_value('cn') eq $new_group } @groups;
my $error = '';
if (! $group_ldap) {
$error = "Error: group $new_group doesn't exist\n"
} else {
my $mesg = $group_ldap->add(memberUid => $login)
->update(Ravada::Auth::LDAP::_init_ldap_admin());
return $c->render(json => { error => "Error: unknown args ".Dumper($arg)}) if keys %$arg;
if ($mesg->code) {
$error = "Error: adding $login to $new_group ".$mesg->error;
}
}
eval {
Ravada::Auth::LDAP::add_to_group($login, $group);
};
my $error = $@;
$error =~ s/(.*) at lib.*/$1/ if $error;
return $c->render(json => { error => $error } );
};
......@@ -1626,21 +1632,15 @@ post '/ldap/group/remove_member' => sub($c) {
my $arg = decode_json($c->req->body);
my $login = delete $arg->{cn};
my $new_group = delete $arg->{group};
my $dn = delete $arg->{dn};
my $group = delete $arg->{group};
my @groups = Ravada::Auth::LDAP::search_group( name => $new_group);
my ($group_ldap) = grep {$_->get_value('cn') eq $new_group } @groups;
die "Error: group $new_group doesn't exist\n"
unless $group_ldap;
return $c->render(json => { error => "Error: unknown args ".Dumper($arg)}) if keys %$arg;
my $mesg = $group_ldap->delete(memberUid => $login)
->update(Ravada::Auth::LDAP::_init_ldap_admin());
if ($mesg->code) {
return $c->render(json => { error => "Error: adding $login to $new_group ".$mesg->error });
}
return $c->render(json => { error => '' } );
eval {
Ravada::Auth::LDAP::remove_from_group($dn, $group);
};
return $c->render(json => { error => ($@ or '') } );
};
......@@ -3301,16 +3301,37 @@ sub _list_ldap_groups($c, $name='*') {
};
sub _list_ldap_users($c, $filter='*' ) {
$filter = '*' if $filter eq 'undefined';
$filter = '*'.$filter if length($filter) && $filter !~ /^\*/;
if ( exists $LDAP_USERS{$filter} ) {
my $data_json = $LDAP_USERS{$filter};
my $data;
eval { $data = decode_json($data_json) };
return $c->render(json => $data) if $data;
}
$LDAP_USERS{$filter} = encode_json({ error => '', entries => [] });
my @users;
eval { @users = Ravada::Auth::LDAP::search_user(name => $filter, escape_username => 0) };
my $error = ($@ or '');
if ($@ =~ /Sizelimit exceeded/) {
$error = "Warning: Too many results, type in the search box to filter.";
}
if (scalar @users>100) {
$error = "Warning: Too many results, type in the search box to filter.";
$#users= 100;
}
my @entries;
for my $entry (Ravada::Auth::LDAP::search_user(name => $filter, escape_username => 0)) {
push @entries,({name => $entry->get_value('cn') });
for my $entry (@users) {
push @entries,({name => $entry->dn});
}
return $c->render(json => [sort { $a->{name} cmp $b->{name} } @entries]);
my $ret = {error => $error, entries => \@entries };
$LDAP_USERS{$filter} = encode_json($ret);
return $c->render(json => $ret );
}
sub _list_ldap_group_members($c, $name) {
my $group = Ravada::Auth::LDAP::search_group( name => $name );
return $c->render(json => [ sort $group->get_value('memberUid') ] );
return $c->render(json => [ Ravada::Auth::LDAP::_group_members($name) ] );
}
......
......@@ -77,6 +77,7 @@ sub _check_html_lint($url, $content, $option = {}) {
|| $error->errtext =~ /Unknown attribute "(charset|crossorigin|integrity)/
|| $error->errtext =~ /Unknown attribute "image.* for tag <div/
|| $error->errtext =~ /Unknown attribute "ipaddress"/
|| $error->errtext =~ /Unknown attribute "sizes" for tag .link/
) {
next;
}
......
......@@ -216,7 +216,8 @@ sub test_manage_group {
skip("No admin group defined",1);
}
ok($user->is_admin,"User $uid (".(ref $user).") "
."should be admin, he was added to $name group") or exit;
."should be admin, he was added to $name group")
or die Dumper([Ravada::Auth::LDAP::_group_members($name)]);
};
}
......
use warnings;
use strict;
use Carp qw(confess);
use Data::Dumper;
use Test::More;
use YAML qw(LoadFile DumpFile);
no warnings "experimental::signatures";
use feature qw(signatures);
use lib 't/lib';
use Test::Ravada;
use_ok('Ravada::Auth::LDAP');
sub _create_group($oc=['top','groupOfNames']) {
my $g_name="group_".new_domain_name();
my $group = Ravada::Auth::LDAP::search_group(name => $g_name);
if ($group) {
Ravada::Auth::LDAP::remove_group($g_name);
$group = undef;
}
if (!$group) {
Ravada::Auth::LDAP::add_group( $g_name
,undef
,$oc
);
}
$group = Ravada::Auth::LDAP::search_group(name => $g_name);
ok($group);
return $group;
}