From 35534f7e26d0c20347f1159b9a4bda203024db8e Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 5 Feb 2024 16:59:16 +0100 Subject: [PATCH] Feat: hide clones (#2020) manage base options and non-public access closes #2012 --- lib/Ravada.pm | 1 + lib/Ravada/Domain.pm | 6 + lib/Ravada/Front.pm | 16 ++- script/rvd_front | 23 ++-- t/front/40_list_domains.t | 71 ++++++++++++ t/mojo/15_list_bases.t | 158 ++++++++++++++++++++++++++ templates/main/admin_machines.html.ep | 10 ++ templates/main/vm_base.html.ep | 6 +- templates/main/vm_base_policy.html.ep | 4 +- templates/main/vm_base_public.html.ep | 24 ++++ templates/main/vm_rebase.html.ep | 20 ++-- templates/main/vm_remove_base.html.ep | 6 +- 12 files changed, 317 insertions(+), 28 deletions(-) create mode 100644 t/mojo/15_list_bases.t create mode 100644 templates/main/vm_base_public.html.ep diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 04ca89227..2b00371f6 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -2888,6 +2888,7 @@ sub _upgrade_tables { $self->_upgrade_table('domains','has_backups','int not null default 0'); $self->_upgrade_table('domains','auto_compact','int default NULL'); $self->_upgrade_table('domains','date_status_change' , 'datetime'); + $self->_upgrade_table('domains','show_clones' , 'int not null default 1'); $self->_upgrade_table('domains_network','allowed','int not null default 1'); diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 342549496..1098668b4 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2069,6 +2069,8 @@ sub info($self, $user) { id => $self->id ,name => $self->name ,is_base => $self->is_base + ,is_public => $self->is_public + ,show_clones => $self->show_clones ,id_base => $self->id_base ,is_active => $is_active ,is_hibernated => $self->is_hibernated @@ -4490,6 +4492,10 @@ sub is_public { return $self->_data('is_public'); } +sub show_clones($self,$value=undef) { + return $self->_data('show_clones',$value); +} + =head2 is_volatile Returns if the domain is volatile, so it will be removed on shutdown diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index d949c05ca..e244a6cef 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -130,14 +130,14 @@ Returns: listref of machines sub list_machines_user($self, $user, $access_data={}) { my $sth = $CONNECTOR->dbh->prepare( - "SELECT id,name,alias,is_public, description, screenshot, id_owner, is_base, date_changed" + "SELECT id,name,alias,is_public, description, screenshot, id_owner, is_base, date_changed, show_clones" ." FROM domains " ." WHERE ( is_base=1 OR ( id_base IS NULL AND id_owner=?))" ." ORDER BY alias" ); - my ($id, $name, $alias, $is_public, $description, $screenshot, $id_owner, $is_base, $date_changed); + my ($id, $name, $alias, $is_public, $description, $screenshot, $id_owner, $is_base, $date_changed, $show_clones); $sth->execute($user->id); - $sth->bind_columns(\($id, $name, $alias, $is_public, $description, $screenshot, $id_owner, $is_base, $date_changed)); + $sth->bind_columns(\($id, $name, $alias, $is_public, $description, $screenshot, $id_owner, $is_base, $date_changed,$show_clones)); my $bookings_enabled = $self->setting('/backend/bookings'); my @list; @@ -154,7 +154,13 @@ sub list_machines_user($self, $user, $access_data={}) { 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); + + next unless + $clone && $show_clones + || $user->is_admin + || ($is_public && $user->allowed_access($id)) + || ($id_owner == $user->id); + $name = $alias if defined $alias; my $base = { id => $id, name => Encode::decode_utf8($name) , alias => Encode::decode_utf8($alias or $name) @@ -322,7 +328,7 @@ sub list_domains($self, %args) { my $query = "SELECT d.name,d.alias, d.id, id_base, is_base, id_vm, status, is_public " ." ,vms.name as node , is_volatile, client_status, id_owner " - ." ,comment, is_pool" + ." ,comment, is_pool, show_clones" ." ,d.date_changed" ." FROM domains d LEFT JOIN vms " ." ON d.id_vm = vms.id "; diff --git a/script/rvd_front b/script/rvd_front index fadf3e42c..c49b9850c 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -1074,13 +1074,20 @@ get '/machine/view/(:id).(:type)' => sub { my ($domain) = _search_requested_machine($c); return access_denied($c) if !$domain; - return access_denied($c) unless $USER->is_admin - || $domain->id_owner == $USER->id - || $USER->can_view_all - || $USER->can_start_machine($domain) - ; - - return view_machine($c); + return view_machine($c) if $USER->is_admin + || $USER->can_view_all; + + if ( $domain->id_owner == $USER->id ) { + if ( $domain->id_base) { + my $base = Ravada::Front::Domain->open($domain->id_base); + if ($base->is_public || $base->show_clones()) { + return view_machine($c); + } + } else { + return view_machine($c); + } + } + return access_denied($c); }; any '/machine/clone/(:id).(:type)' => sub { @@ -1091,6 +1098,8 @@ any '/machine/clone/(:id).(:type)' => sub { if ( $USER && $USER->can_clone() && !$USER->is_temporary() ) { my $base = Ravada::Front::Domain->open($c->stash('id')); if (!$base->is_public) { + return access_denied($c) if !$base->show_clones; + my @clones = $base->clones(); my ($clone) = grep { $_->{id_owner} == $USER->id } @clones; return access_denied($c) if !$clone; diff --git a/t/front/40_list_domains.t b/t/front/40_list_domains.t index 9068fa289..248a1eb1a 100644 --- a/t/front/40_list_domains.t +++ b/t/front/40_list_domains.t @@ -187,6 +187,75 @@ sub test_list_bases_many_clones($vm) { } +sub test_list_bases_show_clones($vm) { + my $base = create_domain($vm); + + my $list = rvd_front->list_machines_user(user_admin); + + $base->prepare_base(user_admin); + + my $clone1 = $base->clone(user => user_admin + , name => new_domain_name); + + $list = rvd_front->list_machines_user(user_admin); + my ($entry) = grep { $_->{id} == $base->id} @$list; + + ok($entry); + + $base->is_public(0); + $base->show_clones(1); + + $list = rvd_front->list_machines_user(user_admin); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok($entry); + + $base->show_clones(0); + + $list = rvd_front->list_machines_user(user_admin); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok($entry); + + my $user = create_user(); + $list = rvd_front->list_machines_user($user); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok(!$entry); + + $base->is_public(1); + $list = rvd_front->list_machines_user($user); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok($entry); + + is(scalar(@{$entry->{list_clones}}),0); + + my $clone2 = $base->clone(user => $user + , name => new_domain_name); + + $list = rvd_front->list_machines_user($user); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok($entry); + ok($entry->{list_clones}->[0]); + is($entry->{list_clones}->[0]->{name},$clone2->name) or die Dumper($entry); + is($entry->{list_clones}->[0]->{id},$clone2->id) or die Dumper($entry); + + $base->is_public(0); + $base->show_clones(1); + + $list = rvd_front->list_machines_user($user); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok($entry) or die Dumper($list); + + ok($entry->{list_clones}->[0]);+ is($entry->{list_clones}->[0]->{name},$clone2->name) or die Dumper($entry); + is($entry->{list_clones}->[0]->{id},$clone2->id) or die Dumper($entry); + + $base->show_clones(0); + + $list = rvd_front->list_machines_user($user); + ($entry) = grep { $_->{id} == $base->id} @$list; + ok(!$entry); + + remove_domain($base); +} + ######################################################### remove_old_domains(); @@ -219,6 +288,8 @@ for my $vm_name (reverse sort @VMS) { use_ok($CLASS); + test_list_bases_show_clones($vm); + test_list_bases_many_clones($vm); my $domain = test_create_domain($vm_name); diff --git a/t/mojo/15_list_bases.t b/t/mojo/15_list_bases.t new file mode 100644 index 000000000..7bfca48e7 --- /dev/null +++ b/t/mojo/15_list_bases.t @@ -0,0 +1,158 @@ +use warnings; +use strict; + +use Carp qw(confess); +use Data::Dumper; +use HTML::Lint; +use Test::More; +use Test::Mojo; +use Mojo::File 'path'; +use Mojo::JSON qw(decode_json); + +use lib 't/lib'; +use Test::Ravada; + +no warnings "experimental::signatures"; +use feature qw(signatures); + +my $SECONDS_TIMEOUT = 15; + +my $t; + +my $URL_LOGOUT = '/logout'; +my ($USERNAME, $PASSWORD); +my $SCRIPT = path(__FILE__)->dirname->sibling('../script/rvd_front'); + +######################################################################################## + +sub _create_base($vm_name) { + my $name = new_domain_name(); + + my $iso_name = 'Alpine%'; + mojo_check_login($t); + $t->post_ok('/new_machine.html' => form => { + backend => $vm_name + ,id_iso => search_id_iso($iso_name) + ,name => $name + ,disk => 1 + ,ram => 1 + ,swap => 1 + ,start => 0 + ,submit => 1 + } + )->status_is(302); + die $t->tx->res->body if $t->tx->res->code() != 302; + wait_request(); + my $base=_wait_domain($name); + + die "Error: $name not created" if !$base; + mojo_request($t, 'prepare_base',{ id_domain => $base->id }); + for ( 1 .. 120 ) { + return $base if $base->is_base; + sleep 1; + } + die "Error: $name not prepared" if !$base->is_base; + return $base; +} + +sub test_list_machines_user($vm_name) { + mojo_check_login($t, $USERNAME, $PASSWORD); + my $base = _create_base($vm_name); + $base->is_public(1); + + $t->ua->get($URL_LOGOUT); + my $user = create_user(); + + mojo_login($t, $user->name,$$); + + $t->get_ok("/list_machines_user.json")->status_is(200); + my $body = $t->tx->res->body; + my $bases0; + eval { $bases0 = decode_json($body) }; + is($@, '') or return; + + my ($base_f) = grep { $_->{id} == $base->id } @$bases0; + ok($base_f) or die "Expecting ".$base->name." in listing"; + + $t->get_ok("/machine/clone/".$base->id.".html") + ->status_is(200); + + return if $t->tx->res->code != 200; + + $base->is_public(0); + $base->show_clones(0); + + $t->get_ok("/machine/clone/".$base->id.".html") + ->status_is(403); + + my $clone = _wait_domain($base->name."-".$user->name); + ok($clone) or exit; + + $t->get_ok("/machine/view/".$clone->id.".html") + ->status_is(403); + + $base->show_clones(1); + $t->get_ok("/machine/clone/".$base->id.".html") + ->status_is(200); + + $t->get_ok("/machine/view/".$clone->id.".html") + ->status_is(200); + + my $user2 = create_user(); + + mojo_login($t, $user2->name,$$); + + $t->get_ok("/list_machines_user.json")->status_is(200); + $body = $t->tx->res->body; + eval { $bases0 = decode_json($body) }; + is($@, '') or return; + + my ($base_f2) = grep { $_->{id} == $base->id } @$bases0; + ok(!$base_f2) or die "Expecting ".$base->name." in listing"; + + $t->get_ok("/machine/clone/".$base->id.".html") + ->status_is(403); +} + +sub _wait_domain($name) { + for ( 1 .. 120 ) { + my $domain = rvd_front->search_domain($name); + return $domain if $domain; + sleep 1; + diag("waiting for $name"); + } +} + +######################################################################################## + +$ENV{MOJO_MODE} = 'development'; +init('/etc/ravada.conf',0); +my $connector = rvd_back->connector; +like($connector->{driver} , qr/mysql/i) or BAIL_OUT; + +if (!ping_backend()) { + diag("SKIPPED: no backend"); + done_testing(); + exit; +} +$Test::Ravada::BACKGROUND=1; + +remove_old_domains_req(1); # 0=do not wait for them + +$t = Test::Mojo->new($SCRIPT); +$t->ua->inactivity_timeout(900); +$t->ua->connect_timeout(60); + +$USERNAME = user_admin->name; +$PASSWORD = "$$ $$"; + +for my $vm_name (reverse @{rvd_front->list_vm_types} ) { + + diag("Testing new machine in $vm_name"); + + test_list_machines_user($vm_name); +} +remove_old_domains_req(0); # 0=do not wait for them +remove_old_users(); + +done_testing(); diff --git a/templates/main/admin_machines.html.ep b/templates/main/admin_machines.html.ep index c7b937066..bdd5985ba 100644 --- a/templates/main/admin_machines.html.ep +++ b/templates/main/admin_machines.html.ep @@ -228,6 +228,16 @@