Unverified Commit 76cf708b authored by Francesc Guasch's avatar Francesc Guasch Committed by GitHub
Browse files

refactor(frontend): responsive grants and show errors (#1583)

refactor(frontend): responsive grants and show errors
parent dd3f4e1d
......@@ -1457,7 +1457,7 @@ sub _add_grants($self) {
$self->_add_grant('reboot_all', 0,"Can reboot all virtual machines.");
$self->_add_grant('reboot_clones', 0,"Can reboot clones own virtual machines.");
$self->_add_grant('screenshot', 1,"Can get a screenshot of own virtual machines.");
$self->_add_grant('start_many',0,"Can have more than one machine started.");
$self->_add_grant('start_many',0,"Can have an unlimited amount of machines started.");
$self->_add_grant('expose_ports',0,"Can expose virtual machine ports.");
$self->_add_grant('view_groups',0,'Can view groups.');
$self->_add_grant('manage_groups',0,'Can manage groups.');
......
......@@ -649,7 +649,7 @@ Returns if the user is allowed to perform a privileged action
sub can_do($self, $grant) {
$self->_load_grants();
confess "Wrong grant '$grant'\n".Dumper($self->{_grant_alias})
confess "Permission '$grant' invalid\n".Dumper($self->{_grant_alias})
if $grant !~ /^[a-z_]+$/;
$grant = $self->_grant_alias($grant);
......@@ -739,6 +739,8 @@ sub _load_grants($self) {
my $grant_alias = $self->_grant_alias($name);
$self->{_grant}->{$grant_alias} = $allowed if $enabled;
$self->{_grant_disabled}->{$grant_alias} = !$enabled;
$self->{_grant_type}->{$grant_alias} = 'boolean';
$self->{_grant_type}->{$grant_alias} = 'int' if $is_int;
}
$sth->finish;
}
......@@ -876,6 +878,14 @@ sub grant($self,$user,$permission,$value=1) {
.Dumper(\@perms);
}
if ( $value eq 'false' || !$value ) {
$value = 0;
} else {
$value = 1;
}
return 0 if !$value && !$user->can_do($permission);
my $value_sql = $user->can_do($permission);
return 0 if !$value && !$value_sql;
return $value if defined $value_sql && $value_sql eq $value;
......@@ -944,6 +954,10 @@ sub list_all_permissions($self) {
return @list;
}
sub grant_type($self, $permission) {
return $self->{_grant_type}->{$permission};
}
=head2 list_permissions
Returns a list of all the permissions granted to the user
......@@ -1107,6 +1121,16 @@ sub grants($self) {
return %{$self->{_grant}};
}
sub grants_info($self) {
my %grants = $self->grants();
my %grants_info;
for my $key ( keys %grants ) {
$grants_info{$key}->[0] = $grants{$key};
$grants_info{$key}->[1] = $self->{_grant_type}->{$key};
}
return %grants_info;
}
=head2 ldap_entry
Returns the ldap entry as a Net::LDAP::Entry of the user if it has
......
......@@ -176,6 +176,7 @@ sub send_message($self, $subject, $message='') {
"INSERT INTO messages (id_user, subject, message) "
." VALUES(?, ? , ? )");
$subject = substr($subject,0,120) if length($subject)>120;
$sth->execute($self->id, $subject, $message);
}
......
......@@ -307,7 +307,7 @@ msgid "Password"
msgstr "Contrasenya"
msgid "Start session"
msgstr "Inicieu la Sessió\n"
msgstr "Inicieu la Sessió"
msgid "bits) in your computer."
msgstr "bits) al teu ordinador."
......@@ -972,3 +972,10 @@ msgstr "Esperant que surti la xarxa"
msgid "Press SHIFT + F12 to exit"
msgstr "Premeu SHIFT + F12 per sortir"
msgid "Permission granted to user"
msgstr "Permís concedit a l'usuari"
msgid "Permission revoked from user"
msgstr "Permís denegat a l'usuari"
......@@ -440,8 +440,44 @@ ravadaApp.directive("solShowMachine", swMach)
});
};
$scope.load_grants = function(id) {
id_user=id;
$http.get("/user/grants/"+id_user).then(function(response) {
$scope.perm = response.data;
});
$http.get("/user/info/"+id_user).then(function(response) {
$scope.user= response.data;
});
};
$scope.toggle_grant = function(grant) {
$scope.perm[grant] = !$scope.perm[grant];
$http.get("/user/grant/"+id_user+"/"+grant+"/"+$scope.perm[grant]).then(function(response) {
$scope.error = response.data.error;
$scope.info = response.data.info;
});
};
$scope.update_grant = function(grant) {
$http.get("/user/grant/"+id_user+"/"+grant+"/"+$scope.perm[grant]).then(function(response) {
$scope.error = response.data.error;
$scope.info = response.data.info;
});
};
$scope.change_user = function(data) {
$http.post('/user/set/'+id_user
,JSON.stringify(data)
).then(function(response) {
$scope.load_grants(id_user);
});
};
$scope.init = function(id) {
$scope.load_grants(id);
$scope.list_user_groups(id);
};
$scope.list_groups();
};
var id_user;
};
function messagesPageC($scope, $http, $interval, request) {
$scope.getMessages = function() {
......
......@@ -1109,7 +1109,7 @@ any '/group/new' => sub {
any '/admin/user/(#id).(:type)' => sub {
my $c = shift;
return access_denied($c) if !$USER->can_manage_users() && !$USER->can_grant();
return access_denied($c) unless $USER->can_manage_users() && $USER->can_grant();
push @{$c->stash->{js}}, '/js/admin.js';
my $id = $c->stash('id');
......@@ -1138,35 +1138,121 @@ any '/admin/user/(#id).(:type)' => sub {
}
}
if ($c->param('make_admin')) {
$USER->make_admin($c->stash('id')) if $c->param('is_admin');
$USER->remove_admin($c->stash('id'))if !$c->param('is_admin');
$user = Ravada::Auth::SQL->search_by_id($c->stash('id'));
}
if ($c->param('grant')) {
return access_denied($c) if !$USER->can_grant();
my %grant;
for my $param_name (@{$c->req->params->names}) {
if ( $param_name =~ /^perm_(.*)/ ) {
my $max = defined($c->req->params->param('max_perm_' . $1)) ? $c->req->params->param('max_perm_' . $1) : 1;
$grant{$1} = $max > 1 ? $max : 1;
} elsif ($param_name =~ /^off_perm_(.*)/) {
$grant{$1} = 0 if !exists $grant{$1};
}
push @{$c->stash->{js}}, '/js/admin.js';
$c->stash(user => $user);
return $c->render(template => 'main/manage_user');
};
get '/user/grants/(:id)' => sub($c) {
return access_denied($c) unless $USER->can_manage_users() && $USER->can_grant();
my $user = Ravada::Auth::SQL->search_by_id($c->stash('id'));
my %grants = $user->grants_info;
for my $key (keys %grants) {
my ($value, $type) = @{$grants{$key}};
if ( $type eq 'boolean' ) {
$grants{$key} = \1 if $value;
$grants{$key} = \0 if !$value;
} else {
$grants{$key} = $value;
}
for my $perm (keys %grant) {
if ( $grant{$perm} ) {
$USER->grant($user, $perm, $grant{$perm});
} else {
$USER->revoke($user, $perm);
}
}
return $c->render(json => \%grants);
};
get '/user/info/(:id)' => sub ($c) {
return access_denied($c) unless $USER->can_manage_users();
my $user = Ravada::Auth::SQL->search_by_id($c->stash('id'));
my %info = %{$user->{_data}};
for my $key (keys %info) {
next if $key !~ /^is_/;
if($info{$key}) {
$info{$key} = \1;
} else {
$info{$key} = \0;
}
}
return $c->render(json => \%info);
};
post '/user/set/(:id)' => sub ($c) {
return access_denied($c) unless $USER->can_manage_users() && $USER->can_grant();
my $user = Ravada::Auth::SQL->search_by_id($c->stash('id'));
my $args = decode_json($c->req->body);
lock_hash(%$args);
my $sql = "";
my @values;
my $error = '';
for my $key (sort keys %$args ) {
if ($key !~ /^[a-z][a-z_]+$/) {
my $error = "Permission '$key' invalid";
$USER->send_message($error);
return $c->render(json => { error => $error });
}
if ($key eq 'is_admin') {
eval {
if ( $args->{$key} eq 'true' || $args->{$key} eq '1' ) {
$USER->send_message("Granting admin permissions to user ".$user->name);
$USER->make_admin($user->id);
} elsif ( $args->{$key} eq 'false' || !$args->{$key}) {
$USER->send_message("Revoking admin persmissions from user ".$user->name);
$USER->remove_admin($user->id);
}
};
my $subj = $error;
$USER->send_message($subj, $error) if $error;
next;
}
$sql .= " , " if $sql;
$sql .= "$key= ?";
push @values,($args->{$key});
}
if ($c->param('set_password')) {
$user->change_password($c->param('password'), $c->param('force_change_password'));
if ($sql) {
$sql = "UPDATE users set $sql WHERE id=?";
eval {
my $sth = $RAVADA->_dbh->prepare($sql);
$sth->execute(@values, $c->stash('id'));
};
}
$c->stash(user => $user);
return $c->render(template => 'main/manage_user');
$error = $@;
my $subj = $error;
$subj =~ s/ at.*//;
$USER->send_message($subj, $error) if $error;
return $c->render(json => { error => $error });
};
get '/user/grant/(:id_user)/(:grant)/(:value)' => sub($c) {
return access_denied($c) unless $USER->can_grant();
my $user = Ravada::Auth::SQL->search_by_id($c->stash('id_user'));
my $error = '';
my $value = $c->stash('value');
if ($value eq 'false' || !$value) {
$value = 0;
} else {
$value = 1;
}
eval { $USER->grant($user,$c->stash('grant'),$value) };
$error = $@;
my $info = '';
if ($error) {
$USER->send_message($error);
} else {
my $grant = $c->stash('grant');
$grant =~ s/_/ /g;
if ( $value ) {
$info = "Permission granted to user"
} else {
$info = "Permission revoked from user";
}
$info = $c->stash->{i18n}->localize($info);
$USER->send_message($info." ".$user->name." : $grant");
}
return $c->render(json => { error => $error, info => $info });
};
get '/user/list_groups/(#id_user)' => sub($c) {
......@@ -3489,3 +3575,15 @@ Hi <%= $name %>,
</h2>
</body>
</html>
@@ exception.development.html.ep
<!DOCTYPE html>
<html>
<head><title>Ravada Server error</title></head>
<body>
<h1>Exception</h1>
<p><%= $exception->message %></p>
<h1>Stash</h1>
<pre><%= dumper $snapshot %></pre>
</body>
</html>
......@@ -189,6 +189,22 @@ sub test_login_fail {
$t->get_ok("/admin/machines")->status_is(401);
like($t->tx->res->dom->at("button#submit")->text,qr'Login') or exit;
$t->get_ok("/admin/users")->status_is(401);
is($t->tx->res->dom->at("button#submit")->text,'Login') or exit;
}
sub test_copy_without_prepare($clone) {
is ($clone->is_base,0) or die "Clone ".$clone->name." is supposed to be non-base";
my $n_clones = 3;
mojo_request($t, "clone", { id_domain => $clone->id, number => $n_clones });
wait_request(debug => 1, check_error => 1, background => 1, timeout => 120);
my @clones = $clone->clones();
is(scalar @clones, $n_clones) or exit;
remove_machines($clone);
}
sub test_validate_html($url) {
......
use warnings;
use strict;
use Data::Dumper;
use HTML::Lint;
use Test::More;
use Test::Mojo;
use Mojo::File 'path';
use lib 't/lib';
use Test::Ravada;
no warnings "experimental::signatures";
use feature qw(signatures);
my $t;
my $URL_LOGOUT = '/logout';
my ($USERNAME, $PASSWORD);
my $SCRIPT = path(__FILE__)->dirname->sibling('../script/rvd_front');
$ENV{MOJO_MODE} = 'development';
init('/etc/ravada.conf',0);
my $connector = rvd_back->connector;
like($connector->{driver} , qr/mysql/i) or BAIL_OUT;
$t = Test::Mojo->new($SCRIPT);
$t->ua->inactivity_timeout(900);
$t->ua->connect_timeout(60);
sub test_non_admin() {
my ($username, $password) = ( new_domain_name(),$$);
my $user_db = Ravada::Auth::SQL->new( name => $username);
$user_db->remove();
my $user = create_user( $username, $password);
mojo_login($t, $user->name, $password);
_test_user_grants($user, 403);
$user->remove();
}
sub test_admin() {
my ($username, $password) = ( new_domain_name(),$$);
my $user_db = Ravada::Auth::SQL->new( name => $username);
$user_db->remove();
my $user_login = create_user( $username, $password, 1);
mojo_login($t, $user_login->name, $password);
($username, $password) = ( new_domain_name(),$$);
$user_db = Ravada::Auth::SQL->new( name => $username);
$user_db->remove();
my $user= create_user( $username, $password);
_test_user_grants($user, 200);
$user->remove();
}
sub _test_user_grants($user, $expected_code) {
$t->get_ok("/admin/users");
is($t->tx->res->code(),$expected_code);
$t->get_ok("/admin/user/".$user->id.".html");
is($t->tx->res->code(),$expected_code);
$t->get_ok("/user/grants/".$user->id);
is($t->tx->res->code(),$expected_code);
$t->get_ok("/user/info/".$user->id);
is($t->tx->res->code(),$expected_code);
_test_set_admin($user, $expected_code);
_test_grant($user, $expected_code);
}
sub _test_set_admin($user,$expected_code) {
$t->post_ok("/user/set/".$user->id => json => {is_admin => 'true'});
is($t->tx->res->code(),$expected_code);
if ( $expected_code == 200 ) {
is($t->tx->res->json->{error},'');
$user->_load_data();
is($user->is_admin,1,"Expected ".$user->name." is admin");
}
$t->post_ok("/user/set/".$user->id => json => {is_admin => 'false'});
is($t->tx->res->code(),$expected_code);
if ( $expected_code == 200 ) {
is($t->tx->res->json->{error},'');
$user->_load_data();
is($user->is_admin,0);
}
$t->post_ok("/user/set/".$user->id => json => {is_admin => 1 });
is($t->tx->res->code(),$expected_code);
if ( $expected_code == 200 ) {
is($t->tx->res->json->{error},'');
$user->_load_data();
is($user->is_admin,1,"Expected ".$user->name." is admin");
}
$t->post_ok("/user/set/".$user->id => json => {is_admin => 0 });
is($t->tx->res->code(),$expected_code);
if ( $expected_code == 200 ) {
is($t->tx->res->json->{error},'');
$user->_load_data();
is($user->is_admin,0);
}
}
sub _test_grant($user, $expected_code) {
$t->get_ok("/user/grant/".$user->id."/missing grant/true");
is($t->tx->res->code(),$expected_code);
if ( $expected_code == 200 ) {
my $error = $t->tx->res->json->{error};
like($error,qr/Permission.*invalid/);
}
$t->get_ok("/user/grant/".$user->id."/clone/false");
is($t->tx->res->code(),$expected_code);
if ($expected_code == 200 ) {
is($t->tx->res->json->{error},'');
$user->_load_data();
is($user->can_clone, 0,"Expecting user ".$user->name." ".$user->id." can not clone") or die;
} else {
ok($user->can_clone);
}
$t->get_ok("/user/grant/".$user->id."/clone/1");
is($t->tx->res->code(),$expected_code);
if ($expected_code == 200 ) {
is($t->tx->res->json->{error},'');
$user->_reload_grants();
is($user->can_clone,1,"Expecting user ".$user->name." ".$user->id." can clone") or die;
}
$t->get_ok("/user/grant/".$user->id."/clone_all/true");
is($t->tx->res->code(),$expected_code);
if ($expected_code == 200 ) {
is($t->tx->res->json->{error},'');
$user->_reload_grants();
is($user->can_clone_all, 1,"Expecting user ".$user->name." ".$user->id." can clone all") or die;
} else {
ok(!$user->can_clone_all);
}
$user->remove();
}
test_non_admin();
test_admin();
done_testing();
......@@ -64,6 +64,18 @@ sub test_defaults {
}
}
my %grants = $user->grants();
my %grants_info = $user->grants_info();
for my $key ( keys %grants ) {
is($grants_info{$key}->[0],$grants{$key}, $key);
if ($key eq 'start_limit') {
is($grants_info{$key}->[1],"int" , $key);
} else {
is($grants_info{$key}->[1],"boolean" , $key);
}
}
$user->remove();
}
......
<html>
<html>
%= include 'bootstrap/header'
<body id="page-top" data-spy="scroll" data-target=".fixed-top" role="document" ng-app="ravada.app">
<div id="wrapper">
%= include 'bootstrap/navigation'
<div id="page-wrapper" ng-controller="usersPage" ng-init="list_user_groups(<%= $user->id %>)">
<div id="page-wrapper" ng-controller="usersPage" ng-init="init(<%= $user->id %>)">
<div class="page-header">
<div class="card">
......
<div class="card">
<div class="card-body">
<form method="post" action="/admin/user/<%= $user->id %>.html">
% my $checked = '';
% $checked = 'checked' if $user->is_admin;
<input type="checkbox" name="is_admin" value="1" <%= $checked %>>
<i ng-show="!user" class="fas fa-sync-alt fa-spin"></i>
<form ng-show="user">
<input type="checkbox" name="is_admin"
ng-click="change_user({ 'is_admin': !user.is_admin})"
ng-model="user.is_admin"
/>
<label for="is_admin"><%=l 'is admin' %></label><br/>
<button type="reset" class="btn btn-outline-secondary" onclick = "location='/admin/users'"><%=l 'Cancel' %></button>
<input type="submit" class="btn btn-primary" name="make_admin" value="<%=l 'Submit' %>">
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<form method="post" action="/admin/user/<%= $user->id %>.html">
<form method="post" action="/admin/user/<%= $user->id %>.html" ng-show="perm">
% for my $perm ($_user->list_all_permissions) {
% my $can_do = $user->can_do($perm->{name});
% my $checked = $can_do ? 'checked' : '';
<input type="checkbox" <%= $checked %>
name="perm_<%= $perm->{name} %>">
<input type="hidden" name="off_perm_<%= $perm->{name} %>" value="off">
<label for="perm_<%= $perm->{name} %>"><%= $perm->{name} %>: <%=l($perm->{description}) %></label><br/>
% if ($perm->{is_int} != 0) {
<input type="number" name="max_perm_<%= $perm->{name} %>" min="1" value="<%= $can_do %>" max="999" style="margin-left: 15px; margin-bottom: 10px"><br>
% if (!$perm->{is_int} ) {
<input type="checkbox" ng-model="perm['<%= $perm->{name} %>']"
ng-click="toggle_grant('<%= $perm->{name} %>')"
>
% }
% if ($perm->{is_int}) {
<input type="number" name="perm_<%= $perm->{name} %>" min="1" ng-model="perm['<%= $perm->{name} %>']" max="999" style="margin-left: 15px; margin-bottom: 10px"
size="4"
ng-hide="<%= $perm->{name} eq 'start_limit' %> && perm['start_many']"
ng-change="update_grant('<%= $perm->{name} %>')"
>
% }
% my $hide = $perm->{name} eq 'start_limit';
% $hide = 0 if !$hide;
<label for="perm_<%= $perm->{name} %>"
ng-hide="<%= $hide %> && perm['start_many']"
>
<span ng-show="<%= $perm->{name} ne 'start_limit'%>"><%= $perm->{name} %>:</span>
<%=l($perm->{description}) %>
</label>
<br ng-hide="<%= $hide %> && perm['start_many']"/>
% }
<button type="reset" class="btn btn-outline-secondary" onclick = "location='/admin/users'"><%=l 'Cancel' %></button>
<input type="submit" class="btn btn-primary" name="grant" value="<%=l 'Submit' %>">
</form>
</div>
</div>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment