Unverified Commit 3aadd59f authored by Francesc Guasch's avatar Francesc Guasch Committed by GitHub
Browse files

Fix private base (#1652)

* test: do not clone private bases
* fix(frontend): deny access for private bases
* fix(backend): validate request on creation
* test(auth): deny clone for no public base
* test(auth): allow and deny clone request
* fix(auth): validate access to create machine
parent 1769ded5
......@@ -103,7 +103,7 @@ sub add_user($name, $password, $storage='rfc2307', $algorithm=undef ) {
my $mesg = $LDAP_ADMIN->add($dn, attr => [%entry]);
if ($mesg->code) {
die "Error afegint $name to $dn ".$mesg->error;
die "Error creating $dn: ".$mesg->error;
}
}
......@@ -307,7 +307,9 @@ sub search_user {
);
if ( $retry <= 3 && $mesg->code && $mesg->code != 4 && $mesg->code != 32) {
if ( $retry <= 3 && $mesg->code && $mesg->code != 4
&& $mesg->code != 32
) {
warn "LDAP error ".$mesg->code." ".$mesg->error."."
."Retrying ! [$retry]" if $retry;
$LDAP_ADMIN = undef;
......
......@@ -11,6 +11,7 @@ Ravada::Auth::User - User management and tools library for Ravada
use Carp qw(confess croak);
use Data::Dumper;
use Mojo::JSON qw(decode_json);
use Moose::Role;
no warnings "experimental::signatures";
......@@ -431,4 +432,37 @@ sub _load_allowed_groups($self) {
}
}
=head2 list_requests
List the requests for this user. It returns requests from the last hour
by default.
Arguments: optionally pass the date to start search for requests.
=cut
sub list_requests($self, $date_req=Ravada::Utils::date_now(3600)) {
my $sth = $$CONNECTOR->dbh
->prepare("SELECT id,args FROM requests WHERE date_req > ?"
." ORDER BY date_req DESC");
my ($id, $args_json);
$sth->execute($date_req);
$sth->bind_columns(\($id, $args_json));
my @req;
while ( $sth->fetch ) {
my $args = decode_json($args_json);
next if !length $args;
my $uid = ($args->{uid} or $args->{id_owner}) or next;
next if $uid != $self->id;
my $req = Ravada::Request->open($id);
push @req, ( $req );
}
return @req;
}
1;
......@@ -209,6 +209,12 @@ our %COMMAND = (
);
lock_hash %COMMAND;
our %CMD_VALIDATE = (
clone => \&_validate_clone
,create => \&_validate_create_domain
,create_domain => \&_validate_create_domain
);
sub _init_connector {
$CONNECTOR = \$Ravada::CONNECTOR;
$CONNECTOR = \$Ravada::Front::CONNECTOR if !$$CONNECTOR;
......@@ -724,11 +730,65 @@ sub _new_request {
my $request = $self->open($self->{id});
$request->status('requested');
$request->_validate();
$request->status('requested') if $request->status ne'done';
return $request;
}
sub _validate($self) {
return if !exists $CMD_VALIDATE{$self->command};
my $method = $CMD_VALIDATE{$self->command};
return if !$method;
$method->($self);
}
sub _validate_create_domain($self) {
my $base;
my $id_base = $self->defined_arg('id_base');
my $id_owner = $self->defined_arg('id_owner') or confess "ERROR: Missing id_owner";
my $owner = Ravada::Auth::SQL->search_by_id($id_owner) or confess "Unknown user id: $id_owner";
$self->_validate_clone($id_base, $id_owner) if $id_base;
return if $owner->is_admin
|| $owner->can_create_machine()
|| ($id_base && $owner->can_clone);
return $self->_status_error("done","Error: access denied to user ".$owner->name);
}
sub _validate_clone($self
, $id_base= $self->args('id_domain')
, $uid=$self->args('uid')) {
my $base = Ravada::Front::Domain->open($id_base);
if ( !$uid ) {
$self->status('done');
$self->error("Error: missing uid");
return;
}
my $user = Ravada::Auth::SQL->search_by_id($uid);
if ( !$user ) {
$self->status('done');
$self->error("Error: user id='$uid' does not exist");
return;
}
return if $user->is_admin;
return if $user->can_clone_all;
return $self->_status_error('done'
,"Error: user ".$user->name." can not clone.")
if !$user->can_clone();
return $self->_status_error('done'
,"Error: ".$base->name." is not public.")
if !$base->is_public;
}
sub _last_insert_id {
return Ravada::Utils::last_insert_id($$CONNECTOR->dbh);
}
......@@ -778,6 +838,11 @@ sub status {
return $status;
}
sub _status_error($self, $status, $error) {
$self->status($status);
return $self->error($error);
}
=head2 at
Sets the time when the request will be scheduled
......
......@@ -4,6 +4,8 @@ use warnings;
use strict;
use Carp qw(confess);
no warnings "experimental::signatures";
use feature qw(signatures);
no warnings "experimental::signatures";
use feature qw(signatures);
......@@ -19,12 +21,13 @@ our $USER_DAEMON_NAME = 'daemon';
=head2 now
Returns the current datetime
Returns the current datetime. Optionally you can pass seconds
to substract to the current time.
=cut
sub now {
my @now = localtime(time);
sub now($seconds=0) {
my @now = localtime(time - $seconds);
$now[5]+=1900;
$now[4]++;
for ( 0 .. 4 ) {
......@@ -34,6 +37,12 @@ sub now {
return "$now[5]-$now[4]-$now[3] $now[2]:$now[1]:$now[0].0";
}
sub date_now($seconds=0) {
my $date = now($seconds);
$date =~ s/\.\d+$//;
return $date;
}
=head2 random_name
Returns a random name.
......
......@@ -85,6 +85,7 @@ my $CONFIG_FRONT = plugin Config => { default => {
,file => $FILE_CONFIG
};
delete $CONFIG_FRONT->{login_custom} if $ENV{MOJO_MODE} && $ENV{MOJO_MODE} eq 'development';
#####
#####
#####
......@@ -706,7 +707,17 @@ get '/machine/view/(:id).(:type)' => sub {
any '/machine/clone/(:id).(:type)' => sub {
my $c = shift;
return clone_machine($c) if $USER && $USER->can_clone() && !$USER->is_temporary();
return clone_machine($c) if $USER->can_clone_all();
if ( $USER && $USER->can_clone() && !$USER->is_temporary() ) {
my $base = Ravada::Front::Domain->open($c->stash('id'));
if (!$base->is_public) {
my @clones = $base->clones();
my ($clone) = grep { $_->{id_owner} == $USER->id } @clones;
return access_denied($c) if !$clone;
}
return clone_machine($c);
}
my $bases_anonymous = $RAVADA->list_bases_anonymous(_remote_ip($c));
for (@$bases_anonymous) {
......@@ -2181,6 +2192,7 @@ sub login($c, $status=200) {
my $url = ($c->param('url') or $c->req->url->to_abs->path);
$url = '/' if $url =~ m{^/login};
my @error =();
if (((defined $login) || (defined $password) || ((defined $c->param('submit')) && ($c->param('submit') ne 'sso'))) && (! $ticket)) {
......@@ -2234,7 +2246,7 @@ sub login($c, $status=200) {
." url($CONFIG_FRONT->{login_bg_file})"
." no-repeat bottom center scroll;\n\t}"];
sleep 5 if scalar(@error);
sleep 5 if scalar(@error) && !$ENV{mode} && !$ENV{mode} eq 'development';
my @error_status = ( status => $status );
@error_status = ( status => 403) if @error;
......
......@@ -19,7 +19,7 @@ init( $CONFIG_FILE);
rvd_back();
my $RVD_FRONT;
my $USER_DATA = { name => 'jimmy.'.new_domain_name, password => 'jameson' };
my $USER_DATA = { name => new_domain_name.'.jimmy', password => 'jameson' };
#########################################################################
......
......@@ -15,7 +15,7 @@ use Test::Ravada;
use Ravada::Auth::LDAP;
sub test_external_auth {
my ($name, $password) = ('jimmy.'.new_domain_name(),'jameson');
my ($name, $password) = (new_domain_name().'.jimmy','jameson');
create_ldap_user($name, $password);
my $login_ok;
eval { $login_ok = Ravada::Auth::login($name, $password) };
......
......@@ -496,9 +496,10 @@ sub remove_old_domains_req($wait=1, $run_request=0) {
my @machines2 = _leftovers();
my @reqs;
for my $machine ( @$machines, @machines2) {
next if $machine->{name} !~ /^$base_name/;
remove_domain_and_clones_req($machine,$wait, $run_request);
next unless $machine->{name} =~ /$base_name/;
remove_domain_and_clones_req($machine,$wait);
}
}
sub remove_domain(@bases) {
......@@ -678,6 +679,7 @@ sub remove_old_domains {
}
sub mojo_init() {
$ENV{MOJO_MODE} = 'development';
my $script = path(__FILE__)->dirname->sibling('../../script/rvd_front');
my $t = Test::Mojo->new($script);
......@@ -713,7 +715,7 @@ sub mojo_login( $t, $user, $pass ) {
$t->post_ok('/login' => form => {login => $user, password => $pass});
like($t->tx->res->code(),qr/^(200|302)$/) or die $t->tx->res->body;
# ->status_is(302);
$MOJO_USER = $user;
$MOJO_USER = $user;
$MOJO_PASSWORD = $pass;
return $t->success;
......@@ -732,6 +734,7 @@ sub mojo_create_domain($t, $vm_name) {
,submit => 1
}
)->status_is(302);
die $t->tx->res->body if !$t->success;
wait_request(debug => 0, background => 1);
my $domain = rvd_front->search_domain($name);
......@@ -886,10 +889,7 @@ sub remove_old_disks {
_remove_old_disks_kvm() if !$>;
}
sub create_user {
my ($name, $pass, $is_admin) = @_;
$is_admin = 1 if $is_admin;
sub create_user($name=new_domain_name(), $pass=$$, $is_admin=0) {
my $login;
eval { $login = Ravada::Auth::SQL->new(name => $name, password => $pass ) };
return $login if $login;
......@@ -923,7 +923,10 @@ sub create_ldap_user($name, $password, $keep=0) {
}
}
my $user = Ravada::Auth::LDAP::search_user($name);
my $user;
eval { $user = Ravada::Auth::LDAP::search_user($name) };
die $@ if $@ && $@ !~ /No such object/;
ok(!$user,"I shouldn't find user $name in the LDAP server") or return;
my $user_db = Ravada::Auth::SQL->new( name => $name);
......@@ -943,7 +946,7 @@ sub create_ldap_user($name, $password, $keep=0) {
my @user = Ravada::Auth::LDAP::search_user(name => $name, filter => '');
# diag("Adding $name to ldap");
my $user_ldap = $user[0];
my $user_ldap = $user[0] or die "Error: ldap user '$name' not found";
my $user_sql
= Ravada::Auth::SQL::add_user(name => $name, is_external => 1, external_auth => 'ldap');
......
......@@ -55,6 +55,7 @@ sub login( $user=$USERNAME, $pass=$PASSWORD ) {
# ->status_is(302);
exit if !$t->success;
mojo_check_login($t, $user, $pass);
}
sub test_many_clones($base) {
......@@ -101,6 +102,8 @@ sub test_many_clones($base) {
}
}
sub test_different_mac(@domain) {
my %found;
for my $domain (@domain) {
......@@ -176,6 +179,141 @@ sub _init_mojo_client {
$t->get_ok('/')->status_is(200)->content_like(qr/choose a machine/i);
}
sub test_login_non_admin($t, $base, $clone){
mojo_check_login($t, $USERNAME, $PASSWORD);
if (! $clone->is_base) {
$t->get_ok("/machine/prepare/".$clone->id.".json")->status_is(200);
for ( 1 .. 10 ) {
my $clone2 = rvd_front->search_domain($clone->name);
last if $clone2->is_base || !$clone2->list_requests;
_wait_request(debug => 1, background => 1, check_error => 1);
mojo_check_login($t, $USERNAME, $PASSWORD);
}
is($clone->is_base,1) or next;
}
$clone->is_public(1);
my $name = new_domain_name();
my $pass = "$$ $$";
my $user = Ravada::Auth::SQL->new(name => $name);
$user->remove();
$user = create_user($name, $pass);
is($user->is_admin(),0);
$base->is_public(0);
login($name, $pass);
$t->get_ok('/')->status_is(200)->content_like(qr/choose a machine/i);
$t->get_ok("/machine/clone/".$clone->id.".html")
->status_is(200);
wait_request(debug => 1, check_error => 1, background => 1, timeout => 120);
mojo_check_login($t, $name, $pass);
my $clone_new_name = $base->name."-".$name;
my $clone_new = rvd_front->search_domain($clone_new_name);
ok(!$clone_new,"Expecting $clone_new_name does not exist") or exit;
$t->get_ok("/machine/clone/".$base->id.".html")
->status_is(403);
$clone_new = rvd_front->search_domain($clone_new_name);
ok(!$clone_new,"Expecting $clone_new_name does not exist") or exit;
$base->is_public(1);
$t->get_ok("/machine/clone/".$base->id.".html")
->status_is(200);
for ( 1 .. 60 ) {
my ($req) = grep { $_->status ne 'done' } $user->list_requests();
last if !$req;
wait_request(debug => 1, check_error => 1, background => 1, timeout => 120);
}
my ($req) = reverse $user->list_requests();
is($req->error, '');
for ( 1 .. 20 ) {
$clone_new = rvd_front->search_domain($clone_new_name);
last if $clone_new;
sleep 1;
}
ok($clone_new,"Expecting $clone_new_name does exist") or exit;
mojo_check_login($t, $name, $pass);
$base->is_public(0);
$t->get_ok("/machine/clone/".$base->id.".html")
->status_is(200);
exit if $t->tx->res->code() != 200;
}
sub test_login_non_admin_req($t, $base, $clone){
mojo_check_login($t, $USERNAME, $PASSWORD);
if (!$clone->is_base) {
$t->get_ok("/machine/prepare/".$clone->id.".json")->status_is(200);
die "Error preparing username='$USERNAME'"
if $t->tx->res->code() != 200;
for ( 1 .. 10 ) {
my $clone2 = rvd_front->search_domain($clone->name);
last if $clone2->is_base || !$clone2->list_requests;
_wait_request(debug => 1, background => 1, check_error => 1);
mojo_check_login($t, $USERNAME, $PASSWORD);
}
is($clone->is_base,1) or next;
}
$clone->is_public(1);
my $name = new_domain_name();
my $pass = "$$ $$";
my $user = Ravada::Auth::SQL->new(name => $name);
$user->remove();
$user = create_user($name, $pass);
is($user->is_admin(),0);
$base->is_public(0);
login($name, $pass);
$t->get_ok('/')->status_is(200)->content_like(qr/choose a machine/i);
my $clone_new_name = new_domain_name();
$t->post_ok('/request/clone' => json =>
{ id_domain => $base->id
,name => new_domain_name()
}
);
wait_request(debug => 1, check_error => 1, background => 1, timeout => 120);
mojo_check_login($t, $name, $pass);
my $clone_new = rvd_front->search_domain($clone_new_name);
ok(!$clone_new,"Expecting $clone_new_name does not exist") or exit;
$t->get_ok("/machine/clone/".$base->id.".html")
->status_is(403);
$clone_new_name = new_domain_name();
$base->is_public(1);
$t->post_ok('/request/clone' => json =>
{ id_domain => $base->id
,name => $clone_new_name
}
);
for ( 1 .. 10 ) {
wait_request(debug => 1, check_error => 1, background => 1, timeout => 120);
$clone_new = rvd_front->search_domain($clone_new_name);
last if $clone_new;
}
ok($clone_new,"Expecting $clone_new_name does exist") or exit;
mojo_check_login($t, $name, $pass);
$base->is_public(0);
$t->get_ok("/machine/clone/".$base->id.".html")
->status_is(200);
exit if $t->tx->res->code() != 200;
}
sub test_login_fail {
$t->post_ok('/login' => form => {login => "fail", password => 'bigtime'});
is($t->tx->res->code(),403);
......@@ -195,14 +333,35 @@ sub test_login_fail {
}
sub test_copy_without_prepare($clone) {
login();
delete_request('set_time','screenshot','refresh_machine_ports');
mojo_request($t,"remove_base", {id_domain => $clone->id })
if $clone->is_base;
is ($clone->is_base,0) or die "Clone ".$clone->name." is supposed to be non-base";
my $base = Ravada::Front::Domain->open($clone->_data('id_base'));
my $n_clones_clone= scalar($clone->clones());
my $n_clones = 3;
delete_request('set_time','screenshot','refresh_machine_ports');
mojo_request($t, "clone", { id_domain => $clone->id, number => $n_clones });
wait_request(debug => 0, check_error => 1, background => 1, timeout => 120);
mojo_check_login($t);
my @clones = $clone->clones();
is(scalar @clones, $n_clones) or exit;
is(scalar @clones, $n_clones_clone+$n_clones,"Expecting clones from ".$clone->name) or exit;
mojo_request($t, "spinoff", { id_domain => $clone->id });
wait_request(debug => 1, check_error => 1, background => 1, timeout => 120);
# is($clone->id_base,0 );
mojo_check_login($t);
mojo_request($t, "clone", { id_domain => $clone->id, number => $n_clones });
wait_request(debug => 1, check_error => 1, background => 1, timeout => 120);
is($clone->is_base, 1 );
my @n_clones_clone_2= $clone->clones();
is(scalar @n_clones_clone_2, $n_clones_clone+$n_clones*2) or exit;
remove_machines($clone);
}
......@@ -355,6 +514,9 @@ sub _clone_and_base($vm_name, $t, $base0) {
}
sub test_clone($base1) {
mojo_request($t,"prepare_base", {id_domain => $base1->id })
if !$base1->is_base();
$t->get_ok("/machine/clone/".$base1->id.".html")->status_is(200);
my $body = $t->tx->res->body;
my ($id_req) = $body =~ m{subscribe',(\d+)};
......@@ -402,6 +564,55 @@ sub test_admin_can_do_anything($t, $base) {
$user->remove();
}
sub _download_iso($iso_name) {
my $id_iso = search_id_iso($iso_name);
my $sth = connector->dbh->prepare("SELECT device FROM iso_images WHERE id=?");
$sth->execute($id_iso);
my ($device) = $sth->fetchrow;
return if $device;
my $req = Ravada::Request->download(id_iso => $id_iso);
for ( 1 .. 300 ) {
last if $req->status eq 'done';
_wait_request(debug => 1, background => 1, check_error => 1);
}
is($req->status,'done');
is($req->error, '') or exit;
}
sub test_create_base($t, $vm_name, $name) {
my $iso_name = 'Alpine%';
_download_iso($iso_name);
$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
,submit => 1
}
)->status_is(302);
my $user = Ravada::Auth::SQL->new(name => $USERNAME);
my @requests = $user->list_requests();
my ($req_create) = grep { $_->command eq 'create' } @requests;
_wait_request(debug => 1, background => 1, check_error => 1);
my $base;
for ( 1 .. 120 ) {
$base = rvd_front->search_domain($name);
last if $base || $req_create->status eq 'done';
sleep 1;
diag("waiting for $name");
}
is($req_create->status,'done');
is($req_create->error,'');
ok($base, "Expecting domain $name create") or exit;
return $base;
}
########################################################################################
$ENV{MOJO_MODE} = 'development';
......@@ -443,31 +654,16 @@ for my $vm_name ( @{rvd_front->list_vm_types} ) {
_init_mojo_client();
$t->post_ok('/new_machine.html' => form => {
backend => $vm_name
,id_iso => search_id_iso('Alpine%')
,name => $name
,disk => 1
,ram => 1
,swap => 1
,submit => 1
}
)->status_is(302);
_wait_request(debug => 0, background => 1, check_error => 1);
my $base0;
for ( 1 .. 10 ) {
$base0 = rvd_front->search_domain($name);
last if $base0;
sleep 1;
}
ok($base0, "Expecting domain $name create") or exit;
my $base0 = test_create_base($t, $vm_name, $name);
push @bases,($base0->name);