diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 5078253fe..04ca89227 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -1641,6 +1641,10 @@ sub _add_indexes_generic($self) { ,"UNIQUE (name)" ] + ,domain_share => [ + "index(id_domain)" + ,"unique(id_user, id_domain)" + ] ,virtual_networks => [ "unique(id_vm,internal_id)" ,"unique(id_vm,name)" @@ -2293,6 +2297,12 @@ sub _sql_create_tables($self) { } ] , + [ domain_share => { + id => 'INTEGER PRIMARY KEY AUTO_INCREMENT' + ,id_domain => 'integer NOT NULL references `domains` (`id`) ON DELETE CASCADE', + ,id_user => 'int not null references `users` (`id`) ON DELETE CASCADE' + } + ], [ bookings => { id => 'INTEGER PRIMARY KEY AUTO_INCREMENT' @@ -4995,9 +5005,14 @@ sub _cmd_remove_base { my $user = Ravada::Auth::SQL->search_by_id( $uid); my $domain = $self->search_domain_by_id($id_domain); - die "Unknown domain id '$id_domain'\n" if !$domain; + die "User ".$user->name." [".$user->id."] not allowed to remove base " + .$domain->name."\n" + unless $user->is_admin || ( + $domain->id_owner == $user->id && $user->can_create_base()); + + $domain->remove_base($user); } @@ -5671,6 +5686,8 @@ sub _cmd_list_cpu_models($self, $request) { my $id_domain = $request->args('id_domain'); my $domain = Ravada::Domain->open($id_domain); + return [] if !$domain->_vm->can_list_cpu_models(); + my $info = $domain->get_info(); my $vm = $domain->_vm->vm; diff --git a/lib/Ravada/Auth/SQL.pm b/lib/Ravada/Auth/SQL.pm index c9d80f6e2..e737dd168 100644 --- a/lib/Ravada/Auth/SQL.pm +++ b/lib/Ravada/Auth/SQL.pm @@ -699,11 +699,17 @@ sub can_do_domain($self, $grant, $domain) { my %valid_grant = map { $_ => 1 } qw(change_settings shutdown reboot rename expose_ports); confess "Invalid grant here '$grant'" if !$valid_grant{$grant}; + return 1 if ( $grant eq 'shutdown' || $grant eq 'reboot' ) + && $self->can_shutdown_machine($domain); + return 0 if !$self->can_do($grant) && !$self->_domain_id_base($domain); return 1 if $self->can_do("${grant}_all"); return 1 if $self->_domain_id_owner($domain) == $self->id && $self->can_do($grant); + return 1 if $grant eq 'change_settings' + && $self->_machine_shared($domain); + if ($self->can_do("${grant}_clones") && $self->_domain_id_base($domain)) { my $base; my $id_base = $self->_domain_id_base($domain); @@ -1067,6 +1073,7 @@ sub can_manage_machine($self, $domain) { return 1 if $self->can_clone_all || $self->can_change_settings($domain) + || $self->_machine_shared($domain) || $self->can_rename_all || $self->can_remove_all || ($self->can_remove_clone_all && $domain->id_base) @@ -1160,6 +1167,8 @@ sub can_shutdown_machine($self, $domain) { return 1 if $self->id == $domain->id_owner; + return 1 if $self->_machine_shared($domain->id); + if ($domain->id_base && $self->can_shutdown_clone()) { my $base = Ravada::Front::Domain->open($domain->id_base); return 1 if $base->id_owner == $self->id; @@ -1168,6 +1177,54 @@ sub can_shutdown_machine($self, $domain) { return 0; } +=head2 can_start_machine + +Return true if the user can shutdown this machine + +Arguments: + +=over + +=item * domain + +=back + +=cut + +sub can_start_machine($self, $domain) { + + return 1 if $self->can_view_all(); + + $domain = Ravada::Front::Domain->open($domain) if !ref $domain; + + return 1 if $self->id == $domain->id_owner; + +=pod + #TODO missing can_start_clones + if ($domain->id_base && $self->can_start_clone()) { + my $base = Ravada::Front::Domain->open($domain->id_base); + return 1 if $base->id_owner == $self->id; + } +=cut + + return 1 if $self->_machine_shared($domain->id); + + return 0; +} + +sub _machine_shared($self, $id_domain) { + $id_domain = $id_domain->id if ref($id_domain); + my $sth = $$CON->dbh->prepare( + "SELECT id FROM domain_share " + ." WHERE id_domain=? AND id_user=?" + ); + $sth->execute($id_domain, $self->id); + my ($id) = $sth->fetchrow; + return 1 if $id; + return 0; +} + + =head2 grants Returns a list of permissions granted to the user in a hash diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 37e533f13..342549496 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1150,7 +1150,8 @@ sub _access_denied_error($self,$user) { confess "User ".$user->name." [".$user->id."] not allowed to access ".$self->name ." owned by ".($owner_name or '')." [".($id_owner or '')."]" - if (defined $id_owner && $id_owner != $user->id ); + unless (defined $id_owner && $id_owner == $user->id ) + || $user->can_start_machine($self); confess $err if $err; @@ -7679,4 +7680,36 @@ sub remove_backup($self, $backup, $remove_file=0) { $sth->execute($backup->{id}); } +sub share($self, $user) { + my $sth = $$CONNECTOR->dbh->prepare( + "INSERT INTO domain_share " + ."(id_domain, id_user)" + ." VALUES(?,?)" + ); + $sth->execute($self->id, $user->id); +} + +sub remove_share($self, $user) { + my $sth = $$CONNECTOR->dbh->prepare( + "DELETE FROM domain_share " + ." WHERE id_domain=? AND id_user=?" + ); + $sth->execute($self->id, $user->id); +} + + +sub list_shares($self) { + my $sth = $$CONNECTOR->dbh->prepare( + "SELECT u.name FROM users u,domain_share ds " + ." WHERE u.id=ds.id_user " + ." AND ds.id_domain=?" + ); + $sth->execute($self->id); + my @shares; + while (my ($name) = $sth->fetchrow) { + push @shares,($name); + } + return @shares; +} + 1; diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index fd3e8639e..d949c05ca 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -151,6 +151,8 @@ sub list_machines_user($self, $user, $access_data={}) { id_owner =>$user->id ,id_base => $id ); + push @clones,$self->_search_shared($id, $user->id); + my ($clone) = ($clones[0] or undef); next unless $clone || $user->is_admin || ($is_public && $user->allowed_access($id)) || ($id_owner == $user->id); $name = $alias if defined $alias; @@ -211,9 +213,9 @@ sub _get_clone_info($user, $base, $clone = Ravada::Front::Domain->open($base->{i $c->{is_locked} = $clone->is_locked; $c->{description} = ( $clone->_data('description') or $base->{description}); - $c->{can_remove} = 0; $c->{can_remove} = ( $user->can_remove() && $user->id == $clone->_data('id_owner')); + $c->{can_remove} = 0 if !$c->{can_remove}; if ($clone->is_active && !$clone->is_locked && $user->can_screenshot) { @@ -256,12 +258,16 @@ sub _init_available_actions($user, $m) { eval { $m->{can_shutdown} = $user->can_shutdown($m->{id}) }; $m->{can_start} = 0; - $m->{can_start} = 1 if $m->{id_owner} == $user->id || $user->is_admin; + $m->{can_start} = 1 if $m->{id_owner} == $user->id || $user->is_admin + || $user->_machine_shared($m->{id}) + ; $m->{can_reboot} = $m->{can_shutdown} && $m->{can_start}; $m->{can_view} = 0; - $m->{can_view} = 1 if $m->{id_owner} == $user->id || $user->is_admin; + $m->{can_view} = 1 if $m->{id_owner} == $user->id || $user->is_admin + || $user->_machine_shared($m->{id}) + ; $m->{can_manage} = ( $user->can_manage_machine($m->{id}) or 0); eval { @@ -1000,6 +1006,25 @@ sub search_clone($self, %args) { } +sub _search_shared($self, $id_base, $id_user) { + my $sth = $CONNECTOR->dbh->prepare( + "SELECT d.id, d.name FROM domains d, domain_share ds" + ." WHERE id_base=? " + ." AND ds.id_user=? " + ." AND ds.id_domain=d.id " + ); + $sth->execute($id_base, $id_user); + + my @clones; + while ( my ($id_domain, $name) = $sth->fetchrow ) { + push @clones,($self->search_domain($name)); + } + $sth->finish; + + return @clones; + +} + =head2 search_domain Searches a domain by name @@ -1766,6 +1791,30 @@ sub _filter_active($pools, $active) { } +=head2 list_users_share + +Returns a list of users to share + +=cut + +sub list_users_share($self, $name=undef,@skip) { + my $users = $self->list_users(); + my @found = @$users; + if ($name) { + @found = grep { $_->{name} =~ /$name/ } @$users; + } + if (@skip) { + my %skip = map { $_->id => 1} @skip; + my @pre=@found; + @found = (); + for my $user (@pre) { + next if $skip{$user->{id}}; + push @found,($user); + } + } + return \@found; +} + =head2 upload_users Upload a list of users to the database diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 7bd1d023c..d958a6355 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -2835,6 +2835,16 @@ sub list_unused_volumes($self) { return @vols; } +=head2 can_list_cpu_models + +Default for Virtual Managers that can list cpu models is 0 + +=cut + +sub can_list_cpu_models($self) { + return 0; +} + sub _around_copy_file_storage($orig, $self, $file, $storage) { my $sth = $self->_dbh->prepare("SELECT id,info FROM volumes" ." WHERE file=? " diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm index ff0dbe70f..c6a25320f 100644 --- a/lib/Ravada/VM/KVM.pm +++ b/lib/Ravada/VM/KVM.pm @@ -2975,6 +2975,10 @@ sub get_library_version($self) { return $self->vm->get_library_version(); } +sub can_list_cpu_models($self) { + return 1; +} + sub list_virtual_networks($self) { my @networks; for my $net ($self->vm->list_all_networks()) { diff --git a/public/js/ravada.js b/public/js/ravada.js index 89120e38c..258e43de5 100644 --- a/public/js/ravada.js +++ b/public/js/ravada.js @@ -320,6 +320,7 @@ $scope.lock_info = false; $scope.topology = false; $scope.searching_ldap_attributes = true; + $scope.shared_user_found=false; $scope.storage_pools=['default']; $scope.getUnixTimeFromDate = function(date) { @@ -595,6 +596,8 @@ $scope.storage_pools[i]=response.data[i].name; } }); + + $scope.list_shares(); } list_interfaces(); if (is_admin) { @@ -1208,6 +1211,43 @@ }); }; + $scope.search_shared_user = function() { + $scope.searching_shared_user = true; + $scope.shared_user_found = ''; + $http.get("/search_user/"+$scope.user_share) + .then(function(response) { + $scope.shared_user_found = response.data.found; + $scope.shared_user_count = response.data.count; + $scope.searching_shared_user=false; + if ($scope.shared_user_count == 1) { + $scope.user_share = response.data.found; + } + }); + }; + + $scope.share_machine = function() { + $http.get("/machine/share/"+$scope.showmachine.id+"/" + +$scope.shared_user_found) + .then(function(response) { + $scope.list_shares(); + }); + }; + + $scope.remove_share_machine = function(user) { + $http.get("/machine/remove_share/"+$scope.showmachine.id+"/" + +user) + .then(function(response) { + $scope.list_shares(); + }); + }; + + $scope.list_shares = function() { + $http.get("/machine/list_shares/"+$scope.showmachine.id) + .then(function(response) { + $scope.shares = response.data; + }); + }; + $scope.message = []; $scope.disk_remove = []; $scope.pending_before = 10; diff --git a/script/rvd_front b/script/rvd_front index 1e4d1793a..3354e4b6e 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -945,6 +945,62 @@ any '/machine/manage/(:id).(:type)' => sub { return manage_machine($c); }; +get '/machine/list_shares/#id' => sub($c) { + my $id = $c->stash('id'); + + my $machine = Ravada::Front::Domain->open($id); + return json_error($c, "Unknown machine id=$id" ) if !$machine; + + return access_denied($c) unless $USER->id == $machine->id_owner + || $USER->is_admin; + + return $c->render(json => [$machine->list_shares]); +}; + +get '/machine/share/#id/#name' => sub($c) { + _add_share($c); +}; + +get '/machine/remove_share/#id/#name' => sub($c) { + _add_share($c,1); +}; + +sub _add_share($c, $remove=0) { + my $id = $c->stash('id'); + my $name = $c->stash('name'); + + my $machine = Ravada::Front::Domain->open($id); + + return json_error($c, "Unknown machine id=$id" ) if !$machine; + + return access_denied_json($c) unless $machine->id_owner == $USER->id + || $USER->is_admin; + + my $user_share = Ravada::Auth::SQL->new(name => $name); + + return json_error($c,"Unknown user name=$name" ) + if !$user_share || !$user_share->id; + + if ($remove) { + return json_error($c,"Machine not shared with user $name" ) + if !$user_share->_machine_shared($id); + } else { + return json_error($c,"Machine already shared with user $name" ) + if $user_share->_machine_shared($id); + + } + + if ($remove) { + $machine->remove_share($user_share); + $USER->send_message("Removed shared access from user $name"); + } else { + $machine->share($user_share); + $USER->send_message("Machine shared with user $name"); + } + + return $c->render(json => {id => $id, name => $name}); +}; + any '/(:item)/settings/(:id).html' => sub($c) { _add_admin_libs($c); @@ -1020,7 +1076,9 @@ get '/machine/view/(:id).(:type)' => sub { return access_denied($c) unless $USER->is_admin || $domain->id_owner == $USER->id - || $USER->can_view_all; + || $USER->can_view_all + || $USER->can_start_machine($domain) + ; return view_machine($c); }; @@ -2223,6 +2281,29 @@ get '/list_users.json' => sub($c) { return $c->render(json => $RAVADA->list_users ); }; +get '/search_user/#name'=> sub($c) { + my $name = $c->stash('name'); + return _search_user($c, $name); +}; + +get '/search_user/'=> sub($c) { + + return _search_user($c); +}; + + +sub _search_user($c,$name='') { + + my $list_found = $RAVADA->list_users_share($name,$USER); + my $found; + if (scalar(@$list_found) == 1 ) { + ($found) = @$list_found ; + } else { + ($found) = grep { $_->{name} eq $name } @$list_found; + } + $found = $found->{name} if $found; + return $c->render(json => {found => $found , count => scalar(@$list_found) } ); +}; get '/list_ldap_groups' => sub($c) { return access_denied($c) unless $USER->can_view_groups || $USER->can_manage_groups; @@ -3251,6 +3332,11 @@ sub access_denied { sub _access_denied { return access_denied(@_) } +sub json_error($c, $error) { + $USER->send_message($error); + $c->render(json => {error => $error } ); +} + sub base_id { my $name = shift; my $base = $RAVADA->search_domain($name); diff --git a/t/mojo/10_login.t b/t/mojo/10_login.t index fcb4713b6..b1ee549ca 100644 --- a/t/mojo/10_login.t +++ b/t/mojo/10_login.t @@ -739,9 +739,18 @@ sub test_new_machine_default($t, $vm_name, $empty_iso_file=undef) { mojo_check_login($t); $t->post_ok('/new_machine.html' => form => $args)->status_is(302); + like($t->tx->res->code(),qr/^(200|302)$/) + or die $t->tx->res->body; + wait_request(); - my $domain = rvd_front->search_domain($name); + my $domain; + for ( 1 .. 10 ) { + $domain = rvd_front->search_domain($name); + last if $domain; + sleep 1; + wait_request(); + } my $disks = $domain->info(user_admin)->{hardware}->{disk}; @@ -1006,6 +1015,56 @@ sub test_clone_same_name($t, $base) { } +sub _create_clone($t, $base) { + mojo_check_login($t); + wait_request(); + + $base->is_public(1); + + my ($name, $pass) = (new_domain_name(),"$$ $$"); + my $user = _create_user($name, $pass); + login($name, $pass); + + $t->get_ok("/machine/clone/".$base->id.".html") + ->status_is(200); + like($t->tx->res->code(),qr/^(200|302)$/) + or die $t->tx->res->body; + + wait_request(debug => 1, check_error => 1, background => 1, timeout => 120); + mojo_check_login($t, $name, $pass); + + my ($clone) = grep { $_->{id_owner} == $user->id } $base->clones; + + return $clone; + +} + +sub _create_user($name, $pass) { + my $user = Ravada::Auth::SQL->new(name => $name); + $user->remove(); + return create_user($name, $pass); +} + +sub test_grant_access($t, $base) { + my $clone0 = _create_clone($t, $base); + + my ($name, $pass) = (new_domain_name(),"$$ $$"); + my $user2 = _create_user($name, $pass); + + my $clone = Ravada::Front::Domain->open($clone0->{id}); + $clone->share($user2); + login($name, $pass); + + $t->get_ok("/machine/view/".$clone->id.".html")->status_is(200); + + like($t->tx->res->code(),qr/^(200|302)$/) + or die $t->tx->res->body; + + $t->get_ok("/machine/reboot/".$clone->id.".json")->status_is(200); + $t->get_ok("/machine/shutdown/".$clone->id.".json")->status_is(200); + +} + ######################################################################################## $ENV{MOJO_MODE} = 'development'; @@ -1061,6 +1120,8 @@ for my $vm_name (reverse @{rvd_front->list_vm_types} ) { test_clone_same_name($t, $base0); + test_grant_access($t, $base0); + if ($vm_name eq 'KVM') { test_new_machine_default($t, $vm_name); test_new_machine_default($t, $vm_name, 1); # with empty iso file diff --git a/t/user/20_grants.t b/t/user/20_grants.t index 9c2d7b000..41f952a58 100644 --- a/t/user/20_grants.t +++ b/t/user/20_grants.t @@ -957,7 +957,8 @@ sub test_view_all($vm) { wait_request( check_error => 0, debug => 0); for my $req ($req_prepare, $req_remove_base, $req_shutdown) { is($req->status,'done'); - like($req->error,qr'User.* (can.t |not allowed)', $req->command); + like($req->error,qr'User.* (can.t |not allowed)', $req->command) + or exit; } for my $req ( $req_start_admin, $req_prepare_admin, $req_start ,$req_refresh, $req_refresh_ports) { diff --git a/t/user/35_share.t b/t/user/35_share.t new file mode 100644 index 000000000..3de03f747 --- /dev/null +++ b/t/user/35_share.t @@ -0,0 +1,145 @@ +#!perl + +use strict; +use warnings; + +use Data::Dumper; +use Test::More; + +use lib 't/lib'; +use Test::Ravada; + +no warnings "experimental::signatures"; +use feature qw(signatures); + +############################################################## + +sub test_share($vm) { + my $base = create_domain($vm->type); + + $base->prepare_base( user_admin ); + $base->is_public(1); + + my $user1 = create_user(new_domain_name(),$$); + my $user2 = create_user(new_domain_name(),$$); + is($user1->is_admin,0); + + my $req = Ravada::Request->clone( + uid => $user1->id + ,id_domain => $base->id + ); + wait_request(); + my ($clone0) = grep { $_->{id_owner} == $user1->id } $base->clones; + ok($clone0); + my $clone = Ravada::Front::Domain->open($clone0->{id}); + + my $list_bases_u1 = rvd_front->list_machines_user($user1); + my ($clone_user1) = grep { $_->{name } eq $base->name } @$list_bases_u1; + is(scalar(@{$clone_user1->{list_clones}}),1); + + my $list_bases_u2 = rvd_front->list_machines_user($user2); + my ($clone_user2) = grep { $_->{name } eq $base->name } @$list_bases_u2; + is(scalar(@{$clone_user2->{list_clones}}),0); + + test_users_share($clone); + $clone->share($user2); + test_users_share($clone,$user2); + + is($user2->can_shutdown($clone),1); + + my $req2 = Ravada::Request->start_domain( + uid => $user2->id + ,id_domain => $clone->id + ); + wait_request(); + is($req2->status,'done'); + is($req2->error,''); + + + $list_bases_u2 = rvd_front->list_machines_user($user2); + ($clone_user2) = grep { $_->{name } eq $base->name } @$list_bases_u2; + is(scalar(@{$clone_user2->{list_clones}}),1); + is($clone_user2->{list_clones}->[0]->{can_remove},0); + is($clone_user2->{list_clones}->[0]->{can_shutdown},1); + + is($user2->can_view_all,undef); + is($user2->can_start_machine($clone->id),1) or exit; + + is($user2->can_manage_machine($clone->id),1,"should manager machine"); + is($user2->can_change_settings($clone->id),1); + + test_machine_info_shared($user2,$clone); + + test_requests_shared($user2, $clone); + + $clone->remove_share($user2); + + is($user2->can_shutdown($clone),0); +} + +sub test_users_share($clone, @users) { + my $all_users = rvd_front->list_users(); + my @expected; + for my $user (@$all_users) { + next if grep { $_->id == $user->{id} } @users; + next if $user->{id} == $clone->id_owner; + + push @expected,($user); + } + my $owner = Ravada::Auth::SQL->search_by_id($clone->id_owner); + my $users_share = rvd_front->list_users_share('',$owner,@users); + is_deeply($users_share,\@expected) or die Dumper($users_share,\@expected); + +} + +sub test_requests_shared($user, $clone) { + my $req3 = Ravada::Request->start_domain( + uid => $user->id + ,id_domain => $clone->id + ); + wait_request(); + is($req3->status,'done'); + is($req3->error,''); + + my $req4 = Ravada::Request->list_cpu_models( + uid => $user->id + ,id_domain => $clone->id + ); + wait_request(); + is($req4->status,'done'); + is($req4->error,''); + +} + +sub test_machine_info_shared($user, $clone) { + my $info = $clone->info($user); + is($info->{can_start},1); + is($info->{can_view},1); +} + +############################################################## + +clean(); +for my $vm_name ( vm_names() ) { + + my $vm; + eval { $vm = rvd_back->search_vm($vm_name) }; + + SKIP: { + my $msg = "SKIPPED test: No $vm_name VM found "; + if ($vm && $vm_name =~ /kvm/i && $>) { + $msg = "SKIPPED: Test must run as root"; + $vm = undef; + } + + diag($msg) if !$vm; + skip $msg,10 if !$vm; + + diag("Testing share on $vm_name"); + + test_share($vm); + } +} + +end(); +done_testing(); diff --git a/templates/main/settings_machine_tabs_body.html.ep b/templates/main/settings_machine_tabs_body.html.ep index 80d382c48..fba2de471 100644 --- a/templates/main/settings_machine_tabs_body.html.ep +++ b/templates/main/settings_machine_tabs_body.html.ep @@ -86,6 +86,9 @@ %= include 'main/vm_hostdev' % } +
+ %= include 'main/vm_share' +
diff --git a/templates/main/settings_machine_tabs_head.html.ep b/templates/main/settings_machine_tabs_head.html.ep index 85ecbcb43..5792e5946 100644 --- a/templates/main/settings_machine_tabs_head.html.ep +++ b/templates/main/settings_machine_tabs_head.html.ep @@ -46,4 +46,6 @@ % if ( $monitoring && $USER->is_admin && $domain->is_active ) { <%=l 'System overview' %> % } + + <%=l 'Share' %> diff --git a/templates/main/vm_share.html.ep b/templates/main/vm_share.html.ep new file mode 100644 index 000000000..3d53b27cf --- /dev/null +++ b/templates/main/vm_share.html.ep @@ -0,0 +1,41 @@ +
+ +
+
+ <%=l 'User name' %> +
+
+ + + + {{shared_user_count}} <%=l 'found' %> + + +
+
+
+
+
+ +
+
+ +
+
+

<%=l 'These users have access to this virtual machine' %>

+
+
+ + {{user}} +
+
+ +