Unverified Commit 44e41a06 authored by Francesc Guasch's avatar Francesc Guasch Committed by GitHub
Browse files

Feature restrict access by client (#1233)

feture(frontend):  allow machines if access by client granted

* feature(frontend): setup access by client
* test(frontend): test ws with access grant
* feature(access): allow by accept-whatever with many options
* test(frontend): check access by encoding

issue #1213
parent 7a7425a8
......@@ -1491,6 +1491,7 @@ sub _after_remove_domain {
$self->_finish_requests_db();
$self->_remove_base_db();
$self->_remove_access_attributes_db();
$self->_remove_access_grants_db();
$self->_remove_ports_db();
$self->_remove_volumes_db();
$self->_remove_bases_vm_db();
......@@ -1540,6 +1541,16 @@ sub _remove_access_attributes_db($self) {
$sth->finish;
}
sub _remove_access_grants_db($self) {
return if !$self->{_data}->{id};
my $sth = $$CONNECTOR->dbh->prepare("DELETE FROM domain_access"
." WHERE id_domain=?");
$sth->execute($self->id);
$sth->finish;
}
sub _remove_volumes_db($self) {
return if !$self->{_data}->{id};
my $sth = $$CONNECTOR->dbh->prepare("DELETE FROM volumes"
......@@ -4088,6 +4099,181 @@ sub allow_ldap_access($self, $attribute, $value, $allowed=1, $last=0 ) {
$sth->execute($self->id, $attribute, $value, $allowed, $n_order+1, $last);
}
sub default_access($self, $type, $allowed) {
my @list = $self->list_access($type);
my ($default) = grep { $_->{value} eq '*' } @list;
if ($default) {
my $sth = $$CONNECTOR->dbh->prepare("UPDATE domain_access "
." SET allowed = ? "
." WHERE type=? AND id_domain=? AND value='*'"
);
$sth->execute($allowed, $type, $self->id);
} else {
$self->grant_access(attribute => '_DEFAULT_'
,value => '*'
,allowed => 0
,type => $type
);
}
}
sub grant_access($self, %args) {
my $attribute = delete $args{attribute} or confess "Error: Missing attribute";
my $value = delete $args{value} or confess "Error: Missing value";
my $type = delete $args{type} or confess "Error: Missing type";
my $allowed = delete $args{allowed};
$allowed = 1 if !defined $allowed;
my $last = ( delete $args{last} or 0 );
confess "Error: unknown args ".Dumper(\%args) if keys %args;
return $self->allow_ldap_access($attribute, $value, $allowed,$last)
if $type eq 'ldap';
my $sth ;
if ($value eq '*') {
$sth=$$CONNECTOR->dbh->prepare("DELETE FROM domain_access "
." WHERE id_domain=? AND type=? AND value='*' ");
$sth->execute($self->id,$type);
}
$sth = $$CONNECTOR->dbh->prepare(
"SELECT max(n_order) FROM domain_access"
." WHERE id_domain=?"
);
$sth->execute($self->id);
my ($n_order) = ($sth->fetchrow or 0);
$sth->finish;
$sth = $$CONNECTOR->dbh->prepare(
"INSERT INTO domain_access"
."(id_domain, type, attribute, value, allowed, n_order, last) "
."VALUES(?,?,?,?,?,?,?)");
$sth->execute($self->id, $type, $attribute, $value, $allowed, $n_order+1, $last);
$self->_fix_default_access($type) unless $value eq '*';
}
sub _fix_default_access($self, $type) {
my @list = $self->list_access($type);
my $id_default;
my $max=0;
for ( @list ) {
$max = $_->{n_order} if $_->{n_order} > $max;
if ( $_->{value} eq '*' ) {
$id_default = $_->{id};
}
}
if ( $id_default ) {
my $sth = $$CONNECTOR->dbh->prepare("UPDATE domain_access "
."SET n_order = ? WHERE id=? "
);
$sth->execute($max+2, $id_default);
return;
}
$self->default_access($type,0);
}
sub _mangle_client_attributes($attribute) {
for my $name (keys %$attribute) {
next if ref($attribute->{$name});
if ($name =~ /Accept-\w+/) {
my @values = map {my $item = $_ ; $item =~ s/^(.*?)[;].*/$1/; $item}
split /,/,$attribute->{$name};
$attribute->{$name} = \@values;
} else {
$attribute->{$name} = [$attribute->{$name}]
if !ref($attribute->{$name});
}
}
}
sub _mangle_access_attributes($args) {
for my $type (sort keys %$args) {
_mangle_client_attributes($args->{$type}) if $type eq 'client';
}
}
sub access_allowed($self, %args) {
_mangle_access_attributes(\%args);
lock_hash(%args);
my $sth = $$CONNECTOR->dbh->prepare(
"SELECT type, attribute, value, allowed, last FROM domain_access "
." WHERE id_domain=? "
." ORDER BY type,n_order"
);
$sth->execute($self->id);
my $default_allowed = undef;
while ( my ($type, $attribute, $value, $allowed, $last) = $sth->fetchrow) {
if ($value eq '*') {
$default_allowed = $allowed if !defined $default_allowed;
next;
}
next unless exists $args{$type} && exists $args{$type}->{$attribute};
my $req_value = $args{$type}->{$attribute};
my $found;
for (@$req_value) {
$found =1 if $value eq $_;
}
if ($found) {
return $allowed if $last || !$allowed;
$default_allowed = $allowed;
}
}
return $default_allowed;
}
sub list_access($self, $type=undef) {
return $self->list_ldap_access()
if defined $type && $type eq 'ldap';
my $sql =
"SELECT * from domain_access"
." WHERE id_domain = ? ";
$sql .= " AND type= ".$$CONNECTOR->dbh->quote($type)
if defined $type;
my $sth = $$CONNECTOR->dbh->prepare(
"$sql ORDER BY n_order"
);
$sth->execute($self->id);
my @list;
while (my $row = $sth->fetchrow_hashref) {
push @list,($row) if keys %$row;
}
return @list;
}
sub delete_access($self, @id_access) {
for my $id_access (@id_access) {
$id_access = $id_access->{id} if ref($id_access);
my $sth = $$CONNECTOR->dbh->prepare(
"SELECT * FROM domain_access"
." WHERE id=? ");
$sth->execute($id_access);
my $row = $sth->fetchrow_hashref();
confess "Error: domain access id $id_access not found"
if !keys %$row;
confess "Error: domain access id $id_access not from domain "
.$self->id
." it belongs to domain ".$row->{id_domain}
if $row->{id_domain} != $self->id;
$sth = $$CONNECTOR->dbh->prepare(
"DELETE FROM domain_access"
." WHERE id_domain=? AND id=? ");
$sth->execute($self->id, $id_access);
}
}
#TODO: check something has been deleted
sub delete_ldap_access($self, $id_access) {
my $sth = $$CONNECTOR->dbh->prepare(
......@@ -4130,7 +4316,13 @@ sub deny_ldap_access($self, $attribute, $value) {
}
sub _set_access_order($self, $id_access, $n_order) {
my $sth = $$CONNECTOR->dbh->prepare("UPDATE access_ldap_attribute "
my $sth = $$CONNECTOR->dbh->prepare("UPDATE domain_access"
." SET n_order=? WHERE id=? AND id_domain=?");
$sth->execute($n_order, $id_access, $self->id);
}
sub _set_ldap_access_order($self, $id_access, $n_order) {
my $sth = $$CONNECTOR->dbh->prepare("UPDATE access_ldap_attribute"
." SET n_order=? WHERE id=? AND id_domain=?");
$sth->execute($n_order, $id_access, $self->id);
}
......@@ -4164,6 +4356,53 @@ sub move_ldap_access($self, $id_access, $position) {
my ($id_access2, $n_order2) = ($list[$index2]->{id}, $list[$index2]->{n_order});
die "Error: position ".$index2." not found for domain ".$self->id
."\n".Dumper(\@list)
if !defined $id_access2;
die "Error: n_orders are the same for index $index and ".($index+$position)
."in \n".Dumper(\@list)
if $n_order == $n_order2;
$self->_set_ldap_access_order($id_access, $n_order2);
$self->_set_ldap_access_order($id_access2, $n_order);
}
sub move_access($self, $id_access, $position) {
confess "Error: You can only move position +1 or -1"
if ($position != -1 && $position != 1);
my $sth = $$CONNECTOR->dbh->prepare("SELECT type FROM domain_access "
." WHERE id=?");
$sth->execute($id_access);
my ($type) = $sth->fetchrow();
confess "Error: I can't find accedd id=$id_access" if !defined $type;
my @list = $self->list_access($type);
my $index;
for my $n (0 .. $#list) {
if (defined $list[$n] && $list[$n]->{id} == $id_access ) {
$index = $n;
last;
}
}
confess "Error: access id: $id_access not found for domain ".$self->id
."\n".Dumper(\@list)
if !defined $index;
my ($n_order) = $list[$index]->{n_order};
die "Error: position $index has no n_order for domain ".$self->id
."\n".Dumper(\@list)
if !defined $n_order;
my $index2 = $index + $position;
die "Error: position $index2 has no id for domain ".$self->id
."\n".Dumper(\@list)
if !defined $list[$index2] || !defined$list[$index2]->{id};
my ($id_access2, $n_order2) = ($list[$index2]->{id}, $list[$index2]->{n_order});
die "Error: position ".$index2." not found for domain ".$self->id
."\n".Dumper(\@list)
if !defined $id_access2;
......@@ -4183,6 +4422,14 @@ sub set_ldap_access($self, $id_access, $allowed, $last) {
$sth->execute($allowed, $last, $id_access);
}
sub set_access($self, $id_access, $allowed, $last) {
my $sth = $$CONNECTOR->dbh->prepare("UPDATE domain_access"
." SET allowed=?, last=?"
." WHERE id=?");
$sth->execute($allowed, $last, $id_access);
}
sub rebase($self, $user, $new_base) {
croak "Error: ".$self->name." is not a base\n" if !$self->is_base;
......
......@@ -127,9 +127,7 @@ Returns: listref of machines
=cut
sub list_machines_user {
my $self = shift;
my $user = shift;
sub list_machines_user($self, $user, $access_data={}) {
my $sth = $CONNECTOR->dbh->prepare(
"SELECT id,name,is_public, screenshot"
......@@ -177,6 +175,7 @@ sub list_machines_user {
$base{can_remove} = 1 if $user->can_remove && $clone->id_owner == $user->id;
$base{can_hibernate} = 1 if $clone->is_active && !$clone->is_volatile;
}
next if !$self->_access_allowed($id, $base{id_clone}, $access_data);
$base{screenshot} =~ s{^/var/www}{};
lock_hash(%base);
push @list,(\%base);
......@@ -185,6 +184,19 @@ sub list_machines_user {
return \@list;
}
sub _access_allowed($self, $id_base, $id_clone, $access_data) {
if ($id_clone) {
my $clone = Ravada::Front::Domain->open($id_clone);
my $allowed = $clone->access_allowed(%$access_data);
return $allowed if $allowed;
}
my $base = Ravada::Front::Domain->open($id_base);
my $allowed = $base->access_allowed(%$access_data);
return 1 if !defined $allowed;
return $allowed;
}
sub list_machines($self, $user) {
return $self->list_domains() if $user->can_list_machines();
......
......@@ -98,7 +98,9 @@ sub _list_machines_user($rvd, $args) {
my $user = Ravada::Auth::SQL->new(name => $login)
or die "Error: uknown user $login";
return $rvd->list_machines_user($user)
my $client = $args->{client};
my $ret = $rvd->list_machines_user($user, {client => $client});
return $ret;
}
sub _list_bases_anonymous($rvd, $args) {
......
......@@ -130,8 +130,11 @@
$scope.$apply(function () {
$scope.public_bases = 0;
$scope.private_bases = 0;
if ($scope.machines && $scope.machines.length != data.length) {
$scope.machines = [];
}
for (var i = 0; i < data.length; i++) {
if ( !$scope.machines[i] ) {
if ( !$scope.machines[i] || $scope.machines[i].id != data[i].id ) {
$scope.machines[i] = data[i];
} else {
$scope.machines[i].can_hibernate = data[i].can_hibernate;
......@@ -220,6 +223,12 @@
$scope.init = function(id, url) {
url_ws = url;
$scope.showmachineId=id;
$scope.tab_access=['client']
$scope.client_attributes = [ 'User-Agent'
, 'Accept', 'Connection', 'Accept-Language', 'DNT', 'Host'
, 'Accept-Encoding', 'Cache-Control', 'X-Forwarded-For'
];
subscribe_ws(url_ws);
$http.get('/machine/info/'+$scope.showmachineId+'.json')
.then(function(response) {
......@@ -228,6 +237,7 @@
$scope.new_name=$scope.showmachine.name+"-2";
$scope.validate_new_name($scope.showmachine.name);
}
$scope.init_domain_access();
$scope.init_ldap_access();
$scope.list_ldap_attributes();
list_interfaces();
......@@ -477,14 +487,21 @@
};
$scope.add_ldap_access = function() {
$http.get('/add_ldap_access/'+$scope.showmachine.id+'/'+$scope.ldap_attribute+'/'
+$scope.ldap_attribute_value+"/"+$scope.ldap_attribute_allowed
+'/'+$scope.ldap_attribute_last)
.then(function(response) {
$scope.init_ldap_access();
$scope.add_access = function(type) {
$http.post('/machine/add_access/'+$scope.showmachine.id
,JSON.stringify({
'type': type
,'attribute': $scope.access_attribute[type]
,'value': $scope.access_value[type]
,'allowed': $scope.access_allowed[type]
,'last': $scope.access_last[type]
})
).then(function(response) {
if (type == 'ldap') { $scope.init_ldap_access() }
else { $scope.init_domain_access() }
});
};
$scope.delete_ldap_access= function(id_access) {
$http.get('/delete_ldap_access/'+$scope.showmachine.id+'/'+id_access)
.then(function(response) {
......@@ -504,6 +521,22 @@
$scope.init_ldap_access();
});
};
$scope.move_access= function(type, id_access, count) {
$http.get('/machine/move_access/'+$scope.showmachine.id+'/'
+id_access+'/'+count)
.then(function(response) {
$scope.init_domain_access();
});
};
$scope.set_access = function(id_access, allowed, last) {
$http.get('/machine/set_access/'+$scope.showmachine.id+'/'+id_access+'/'+allowed
+'/'+last)
.then(function(response) {
$scope.init_domain_access();
});
};
$scope.init_ldap_access = function() {
$scope.ldap_entries = 0;
$scope.ldap_verified = 0;
......@@ -516,6 +549,24 @@
$scope.ldap_attributes_default = response.data.default;
});
};
$scope.init_domain_access = function() {
$http.get('/machine/list_access/'+$scope.showmachine.id).then(function(response) {
$scope.domain_access = response.data.list;
$scope.domain_access_default = response.data.default;
});
$http.get('/machine/check_access/'+$scope.showmachine.id)
.then(function(response) {
$scope.check_client_access = response.data.ok;
});
};
$scope.delete_access= function(id_access) {
$http.get('/machine/delete_access/'+$scope.showmachine.id+'/'+id_access)
.then(function(response) {
$scope.init_domain_access();
});
};
$scope.init_new_port = function() {
$scope.new_port = null;
$scope.new_port_name = null;
......@@ -581,6 +632,10 @@
// $scope.getSingleMachine();
// $scope.updatePromise = $interval($scope.getSingleMachine,3000);
list_nodes();
$scope.access_attribute = [ ];
$scope.access_value = [ ];
$scope.access_allowed = [ ];
$scope.access_last = [ ];
$scope.list_ldap_attributes();
};
......
......@@ -634,6 +634,76 @@ post '/machine/hardware/change' => sub {
});
};
get '/machine/list_access/(#id_domain)' => sub {
my $c = shift;
return _access_denied($c) if !$USER->is_admin;
my $domain_id = $c->stash('id_domain');
my $domain = Ravada::Front::Domain->open($domain_id);
my @access0 = $domain->list_access();
my $default = {};
my @access;
for my $access (@access0) {
if ($access->{value} eq '*') {
$default = $access;
next;
}
push @access,($access);
}
return $c->render(json => {list => \@access, default => $default} );
};
get '/machine/check_access/(#id_domain)' => sub {
my $c = shift;
return _access_denied($c) if !$USER->is_admin;
my $domain_id = $c->stash('id_domain');
my $domain = Ravada::Front::Domain->open($domain_id);
my %client;
for my $name (@{$c->req->headers->names}) {
$client{$name}= $c->req->headers->header($name);
}
return $c->render( json => { ok => $domain->access_allowed(client => \%client)});
};
get '/machine/delete_access/(#id_domain)/(#id_access)' => sub {
my $c = shift;
return _access_denied($c) if !$USER->is_admin;
my $domain_id = $c->stash('id_domain');
my $domain = Ravada::Front::Domain->open($domain_id);
$domain->delete_access($c->stash('id_access'));
# delete default if it is the only one left
my @access = $domain->list_access();
if (scalar @access == 1 && $access[0]->{value} eq '*') {
$domain->delete_access($access[0]->{id});
}
return $c->render(json => { ok => 1 });
};
get '/machine/move_access/(#id_domain)/(#id_access)/(#position)' => sub {
my $c = shift;
return _access_denied($c) if !$USER->is_admin;
my $domain_id = $c->stash('id_domain');
my $domain = Ravada::Front::Domain->open($domain_id);
$domain->move_access($c->stash('id_access'),$c->stash('position'));
return $c->render(json => { ok => 1 });
};
get '/node/exists/#name' => sub {
my $c = shift;
my $name = $c->stash('name');
......@@ -822,7 +892,7 @@ get '/count_ldap_entries/(#attribute)/(#value)' => sub {
return $c->render(json => { entries => scalar @entries });
};
get '/add_ldap_access/(#id_domain)/(#attribute)/(#value)/(#allowed)/(#last)' => sub {
post '/machine/add_access/(#id_domain)' => sub {
my $c = shift;
return _access_denied($c) if !$USER->is_admin;
......@@ -830,26 +900,40 @@ get '/add_ldap_access/(#id_domain)/(#attribute)/(#value)/(#allowed)/(#last)' =>
my $domain_id = $c->stash('id_domain');
my $domain = Ravada::Front::Domain->open($domain_id);
my $attribute = $c->stash('attribute');
my $value = $c->stash('value');
my $allowed = 1;
if ($c->stash('allowed') eq 'false') {
my $args = decode_json($c->req->body);
my $attribute = delete $args->{attribute};
my $value = delete $args->{value};
my $type = delete $args->{type};
my $allowed = delete $args->{allowed};
if (!defined $allowed || !$allowed || $allowed =~ /false|undefined/) {
$allowed = 0;
} else {
$allowed= 1;
}
my $last = 1;
if ($c->stash('last') eq 'false') {
my $last = delete $args->{last};
if (!defined $last || !$last || $last =~ /false|undefined/) {
$last = 0;
} else {
$last = 1;
}
$last = 1 if !$allowed;
confess "Error: unknown args ".Dumper($args) if keys %$args;
$domain->grant_access(type => $type
, attribute => $attribute
, allowed => $allowed
, value => $value
, last => $last
);
_fix_default_ldap_access($c, $domain, $allowed)
if $type eq 'ldap';
eval { $domain->allow_ldap_access($attribute => $value, $allowed, $last ) };
_fix_default_ldap_access($c, $domain, $allowed) if !$@;
return $c->render(json => { error => $@ }) if $@;
return $c->render(json => { ok => 1 });
};
sub _fix_default_ldap_access($c, $domain, $allowed) {
sub _fix_default_ldap_access($c, $type, $domain, $allowed) {
my @list = $domain->list_ldap_access();
my $default_found;
for ( @list ) {
......@@ -863,8 +947,12 @@ sub _fix_default_ldap_access($c, $domain, $allowed) {
}
my $allowed_default = 0;
$allowed_default = 1 if !$allowed;