From 4fe5099e8bce647a62d109087e23043dfa358519 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 19 Apr 2024 11:59:36 +0200 Subject: [PATCH 1/8] refactor(backend): fail when unable to configure host device --- lib/Ravada/Domain/KVM.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Ravada/Domain/KVM.pm b/lib/Ravada/Domain/KVM.pm index a683d0696..0a86f5fc3 100644 --- a/lib/Ravada/Domain/KVM.pm +++ b/lib/Ravada/Domain/KVM.pm @@ -3659,7 +3659,7 @@ sub reload_config($self, $doc) { $new_domain = $self->_vm->vm->define_domain($doc->toString); }; - cluck ''.$@ if $@; + die ''.$@ if $@; $self->domain($new_domain); From 379fff9b62926f1bb7e25efea0c7f4b0ed4f4a54 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 22 Apr 2024 11:07:38 +0200 Subject: [PATCH 2/8] fix: show clones checkbox was not working (#2045) --- public/js/admin.js | 5 ++++- templates/main/admin_machines.html.ep | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index 72727a686..467bd7f67 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -499,7 +499,7 @@ ravadaApp.directive("solShowMachine", swMach) $scope.set_autostart= function(machineId, value) { $http.get("/machine/autostart/"+machineId+"/"+value); }; - $scope.set_public = function(machineId, value) { + $scope.set_public = function(machineId, value, show_clones) { if (value) value=1; else value = 0; $http.get("/machine/public/"+machineId+"/"+value) @@ -509,6 +509,9 @@ ravadaApp.directive("solShowMachine", swMach) } }); + if ( value == 0 ) { + $http.get("/machine/set/"+machineId+"/show_clones/"+show_clones); + } }; $scope.can_remove_base = function(machine) { diff --git a/templates/main/admin_machines.html.ep b/templates/main/admin_machines.html.ep index a344230c2..2ad9eda41 100644 --- a/templates/main/admin_machines.html.ep +++ b/templates/main/admin_machines.html.ep @@ -240,7 +240,7 @@ From 5aac3ee4fd327f70b65220edddd2f47c5080be3e Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 22 Apr 2024 11:32:30 +0200 Subject: [PATCH 3/8] Feature status (#2047) feat: show status for services and nodes closes #2044 --- etc/rvd_front.conf.example | 5 +++ script/rvd_front | 81 +++++++++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/etc/rvd_front.conf.example b/etc/rvd_front.conf.example index 69d9f5d9b..598435264 100644 --- a/etc/rvd_front.conf.example +++ b/etc/rvd_front.conf.example @@ -37,4 +37,9 @@ ,file => '/var/log/ravada/rvd_front.log' ,level => 'debug' } + ,status => { + allowed => [ + '127.0.0.1' + ] + } }; diff --git a/script/rvd_front b/script/rvd_front index 23369e6ec..367b0c02b 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -228,7 +228,7 @@ hook before_routes => sub { || $url =~ m{^/fallback/font} ; - return if $url =~ m{^/(anonymous_logout|login|logout|requirements|robots.txt|favicon.ico)}; + return if $url =~ m{^/(anonymous_logout|login|logout|requirements|robots.txt|favicon.ico|status\.)}; my $bases_anonymous = $RAVADA->list_bases_anonymous(_remote_ip($c)); return access_denied($c) if $url =~ m{^/anonymous} && !@$bases_anonymous; @@ -2792,6 +2792,85 @@ get '/host_devices/templates/list/(#id_vm)' => sub($c) { }; +sub _request_recent() { + my @now = localtime(time); + $now[4]++; + for ( 1 .. 4 ){ + $now[$_] = "0".$now[$_] if length ($now[$_])<2 + } + my $now = "".($now[5]+1900)."-$now[4]-$now[3] $now[2]:$now[1]"; + $now[1]--; + my $now2 = "".($now[5]+1900)."-$now[4]-$now[3] $now[2]:$now[1]"; + my $sth = $RAVADA->_dbh->prepare( + "SELECT date_changed,status,command FROM requests ORDER BY date_changed DESC LIMIT 10" + ); + $sth->execute(); + my $n = 100; + while (my ($date_changed, $status, $command) = $sth->fetchrow ) { + next if $status !~ /working|done/; + return 1 if $date_changed =~ /^($now|$now2)/; + last if $n--<0; + } + return 0; +} + +sub _ping_backend() { + return 1 if _request_recent(); + + my $req = Ravada::Request->ping_backend(); + $RAVADA->wait_request($req, 10); + if ($req->status eq 'done' && !$req->error) { + return 1; + } + return 0; +} + +get '/status.(#type)'=> sub($c) { + my $remote_ip = _remote_ip($c); + + my %allowed = ('127.0.0.1' => 1); + if (exists $CONFIG_FRONT->{status} && $CONFIG_FRONT->{status}->{allowed}) { + if (ref($CONFIG_FRONT->{status}->{allowed}) eq 'ARRAY') { + for my $ip ( @{$CONFIG_FRONT->{status}->{allowed}} ) { + warn $ip; + $allowed{$ip}++; + } + } else { + $allowed{$CONFIG_FRONT->{status}->{allowed}}++; + } + } + return access_denied($c) unless $allowed{$remote_ip}; + my $backend = _ping_backend(); + if ($backend) { + $backend='true'; + } else { + $backend='false'; + } + my $status = { backend => $backend, frontend => 'true' }; + my $sth = $RAVADA->_dbh->prepare("SELECT name,is_active " + ." FROM vms " + ." ORDER BY 'name'" + ); + $sth->execute; + + my $sth_active = $RAVADA->_dbh->prepare("SELECT count(*) " + ." FROM domains " + ." WHERE status='active' AND id_vm=? " + ); + while ( my $row = $sth->fetchrow_hashref) { + $sth_active->execute($row->{id}); + $row->{vms}=$sth_active->fetchrow; + if ($row->{is_active}) { + $row->{status} = 'active'; + } else { + $row->{status} = 'disabled'; + } + delete $row->{is_active}; + push@{$status->{nodes}},($row); + } + return $c->render(json => $status); +}; + ################################################### sub _init_error { From bfebe763981b29e70e137e83ba17790da6464b3b Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 22 Apr 2024 11:48:12 +0200 Subject: [PATCH 4/8] Feat: config ram check (#2046) feat: enable configure startup RAM check --- CHANGELOG.md | 2 + lib/Ravada.pm | 13 ++- lib/Ravada/Domain.pm | 4 + lib/Ravada/VM/Void.pm | 4 +- t/mojo/80_check_resources.t | 157 ++++++++++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 t/mojo/80_check_resources.t diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce31a2be..3c126c762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,6 @@ **Bugfixes** +- Node Option Not Available [\#2032] + **Refactors** diff --git a/lib/Ravada.pm b/lib/Ravada.pm index ff8c64e15..d25357bb8 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -3,7 +3,7 @@ package Ravada; use warnings; use strict; -our $VERSION = '2.2.0'; +our $VERSION = '2.2.1'; use utf8; @@ -2713,6 +2713,17 @@ sub _sql_insert_defaults($self){ ,name => 'time' ,value => '21:00' } + ,{ + id_parent => '/backend' + ,name => 'limits' + ,value => undef + } + ,{ + id_parent => '/backend/limits' + ,name => 'startup_ram' + ,value => 1 + } + ] ); diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 43fdeb229..d3e6e43d9 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -1023,6 +1023,8 @@ sub _check_has_clones { sub _check_free_vm_memory { my $self = shift; + return if !Ravada::Front::setting(undef,"/backend/limits/startup_ram"); + my $vm_free_mem = $self->_vm->free_memory; my $domain_memory = $self->info(Ravada::Utils::user_daemon)->{memory}; @@ -2967,6 +2969,7 @@ sub _copy_clone($self, %args) { $id_owner = $user->id if (! $id_owner); my $alias = delete $args{alias}; my $options = delete $args{options}; + my $start = delete $args{start}; confess "ERROR: Unknown arguments ".join(",",sort keys %args) if keys %args; @@ -2978,6 +2981,7 @@ sub _copy_clone($self, %args) { push @copy_arg, ( memory => $memory ) if $memory; push @copy_arg, ( volatile => $volatile ) if $volatile; push @copy_arg, ( options => $options ) if $options; + push @copy_arg, ( start => $start ) if $start; $request->status("working","Copying domain ".$self->name ." to $name") if $request; diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm index 69316ed66..b68973f82 100644 --- a/lib/Ravada/VM/Void.pm +++ b/lib/Ravada/VM/Void.pm @@ -358,9 +358,9 @@ sub list_routes { sub list_virtual_networks($self) { - my $dir_net = $self->dir_img."/networks/"; + my $dir_net = $self->dir_img."/networks"; if (!$self->file_exists($dir_net)) { - my ($out, $err) = $self->run_command("mkdir", $dir_net); + my ($out, $err) = $self->run_command("mkdir","-p", $dir_net); die $err if $err; } my @files = $self->list_files($dir_net,qr/.yml$/); diff --git a/t/mojo/80_check_resources.t b/t/mojo/80_check_resources.t new file mode 100644 index 000000000..d498cb6f0 --- /dev/null +++ b/t/mojo/80_check_resources.t @@ -0,0 +1,157 @@ +use warnings; +use strict; + +use Carp qw(confess); +use Data::Dumper; +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); + +$ENV{MOJO_MODE} = 'development'; +my $SCRIPT = path(__FILE__)->dirname->sibling('../script/rvd_front'); + +my ($USERNAME, $PASSWORD); + +my $URL_LOGOUT = '/logout'; + +$Test::Ravada::BACKGROUND=1; +my $t; + +my $BASE_NAME="zz-test-base-ubuntu"; +my $BASE; +######################################################### + +sub _import_base($vm_name) { + mojo_login($t,$USERNAME, $PASSWORD); + my $name = new_domain_name()."-".$vm_name."-$$"; + if ($vm_name eq 'KVM') { + my $base0 = rvd_front->search_domain($BASE_NAME); + mojo_request_url_post($t,"/machine/copy",{id_base => $base0->id, new_name => $name, copy_ram => 0.128, copy_number => 1}); + for ( 1 .. 90 ) { + $BASE= rvd_front->search_domain($name); + last if $BASE; + wait_request(); + } + + } else { + $BASE = mojo_create_domain($t, $vm_name); + } + + Ravada::Request->shutdown_domain(uid => user_admin->id + ,id_domain => $BASE->id); + my $req = Ravada::Request->prepare_base(uid => user_admin->id + ,id_domain => $BASE->id + ); + wait_request(); + is($req->error,''); + + $BASE->_data('shutdown_disconnected' => 1); + +} + +sub _remove_clones($time) { + Ravada::Request->remove_clones( + uid => user_admin->id + ,id_domain => $BASE->id + ,at => $time + ); +} + +sub _free_memory() { + open my $mem,"<","/proc/meminfo" or die $!; + my $mem_avail; + while (my $line = <$mem> ) { + ($mem_avail) = $line =~ /^MemAvailable.*?(\d+)/; + return $mem_avail if $mem_avail; + } + die; +} + +sub test_ram($vm_name,$enable_check, $expected=undef) { + + my $free_mem = _free_memory(); + my $limit = int($free_mem/1024/1024)+1 ; + _remove_clones(time+300+$limit*2); + my $count = 0; + for my $n ( 0 .. $limit*3 ) { + my $free = int(_free_memory()/1024/1024); + my $name = new_domain_name(); + my $req=Ravada::Request->clone( + uid => user_admin->id + ,id_domain => $BASE->id + ,name => $name + ,memory => 3 * 1024 * 1024 + ); + my $new; + for ( 1 .. 90 ) { + $new = rvd_front->search_domain($name); + last if $new; + wait_request(); + } + last if !$new; + $req = Ravada::Request->start_domain( uid => user_admin->id + ,id_domain => $new->id + ); + for ( 1 .. 10 ) { + wait_request(); + last if $req->status eq 'done'; + } + if ($req->error) { + diag($req->error); + last; + } + $count++; + last if defined $expected && $count > $expected; + my $free2 = int(_free_memory()/1024/1024); + redo if $vm_name eq 'KVM' && ($free2>=$free); + + } + _remove_clones(0); + wait_request(); + return $count; +} + +######################################################### +$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; + +$t = Test::Mojo->new($SCRIPT); +$t->ua->inactivity_timeout(900); +$t->ua->connect_timeout(60); + +remove_old_domains_req(); + +$USERNAME = user_admin->name; +$PASSWORD = "$$ $$"; +for my $vm_name (reverse @{rvd_front->list_vm_types} ) { + diag("Testing RAM limit in $vm_name"); + + _import_base($vm_name); + + rvd_back->setting("/backend/limits/startup_ram" => 1); + my $started_limit =test_ram($vm_name,1); + rvd_back->setting("/backend/limits/startup_ram" => 0); + my $started_no_limit =test_ram($vm_name,0, $started_limit); + ok($started_no_limit > $started_limit); +} + +remove_old_domains_req(0); # 0=do not wait for them + +end(); +done_testing(); From 2ad3fe4d11109b92d481308bf5b4b2f8cb56063f Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 22 Apr 2024 16:17:42 +0200 Subject: [PATCH 5/8] fix(install): remove custom files --- deb/debianize.pl | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/deb/debianize.pl b/deb/debianize.pl index c3fa75a05..7d52a52d3 100755 --- a/deb/debianize.pl +++ b/deb/debianize.pl @@ -5,6 +5,7 @@ use Carp qw(confess); use Cwd qw(getcwd); +use Data::Dumper; use File::Path qw(remove_tree make_path); use IPC::Run3; use lib './lib'; @@ -93,6 +94,32 @@ sub remove_not_needed { die "Missing $path" if ! -e $path; remove_tree($path); } + remove_custom_files("public/js/custom"); +} + +sub remove_custom_files { + my $dir = shift; + opendir my $ls,$dir or die "$! $dir"; + while ( my $file = readdir $ls) { + next if $file =~ m/^\.+$/; + my $path = "$dir/$file"; + if ( -d $path ) { + die "Error: no dirs should be in $dir"; + } elsif ( -f $path ) { + if ($file !~ /insert_here/) { + my ($dir_dst, $component) = $dir =~ m{(.*)/(.*)}; + die "Unknown dir $dir " if !exists $DIR{$dir_dst}; + my $deb_path = "$DIR_DST/$DIR{$dir_dst}/$component/$file"; + if (! -e $deb_path ) { + ($component) = $dir =~ m{.*/(\w+/\w+)}; + $deb_path = "$DIR_DST/$DIR{$dir_dst}/$component/$file"; + } + unlink $deb_path or die "$! $deb_path"; + } + } else { + warn "Warning: unknown file type $file (neither file nor dir)"; + } + } } sub create_md5sums { From 05403cbc2af223dca137bab94d893d97c10e77bc Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Mon, 22 Apr 2024 17:23:01 +0200 Subject: [PATCH 6/8] refactor: removed debug --- t/device/10_templates.t | 4 ---- 1 file changed, 4 deletions(-) diff --git a/t/device/10_templates.t b/t/device/10_templates.t index d3bfa3d04..f3fd80b60 100644 --- a/t/device/10_templates.t +++ b/t/device/10_templates.t @@ -552,7 +552,6 @@ sub _mangle_dom_hd_void($domain) { $config->{hardware}->{host_devices}->[0]->{vendor_id} = $new_id; $domain->_store(hardware => $config->{hardware}); - warn $device_name; $sth = connector->dbh->prepare("UPDATE host_devices_domain set name=?" ." WHERE id=?"); $sth->execute($device_name, $id_hd); @@ -805,11 +804,8 @@ sub test_templates($vm) { _fix_host_device($host_device) if $vm->type eq 'KVM'; - warn 11; test_hd_in_domain($vm, $host_device); - warn 12; test_hd_dettach($vm, $host_device); - warn 13; my $req = Ravada::Request->list_host_devices( uid => user_admin->id From dea3abb74834473ce5b25d26449a0df372fbd3b7 Mon Sep 17 00:00:00 2001 From: aadrri <166824449+aadrri@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:49:02 +0200 Subject: [PATCH 7/8] Mint 18.02 deleted (#2043) Mint 18.02 deleted --- lib/Ravada.pm | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/lib/Ravada.pm b/lib/Ravada.pm index d25357bb8..f20425e83 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -463,19 +463,7 @@ sub _update_isos { ,arch => 'x86_64' } - ,serena64 => { - name => 'Mint 18.1 Mate 64 bits' - ,description => 'Mint Serena 18.1 with Mate Desktop based on Ubuntu Xenial 64 bits' - ,arch => 'x86_64' - ,xml => 'xenial64-amd64.xml' - ,xml_volume => 'xenial64-volume.xml' - ,url => 'https://mirrors.edge.kernel.org/linuxmint/stable/18.3' - ,file_re => 'linuxmint-18.3-mate-64bit.iso' - ,md5_url => '' - ,md5 => 'c5cf5c5d568e2dfeaf705cfa82996d93' - ,min_disk_size => '10' - - } + ,mint20_64 => { name => 'Mint 20 Mate 64 bits' ,description => 'Mint Ulyana 20 with Mate Desktop 64 bits' @@ -1477,6 +1465,8 @@ sub _remove_old_isos { ,"DELETE FROM iso_images " ." WHERE name like 'Alpine%3.8%'" + ,"DELETE FROM iso_images " + ." WHERE name like 'Mint 18.1 Mate 64 bits'" ) { my $sth = $CONNECTOR->dbh->prepare($sql); $sth->execute(); From df781a02e672efb7aec00431749295e0e75306d6 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 25 Apr 2024 16:03:39 +0200 Subject: [PATCH 8/8] Feat/sql groups (#2041) * feat: check access by group * feat: group management backend * feat: booking local group * feat: now bookings are enabled by default * feat: upload group members --- MANIFEST | 2 +- lib/Ravada.pm | 45 +- lib/Ravada/Auth/Group.pm | 188 +++++++ lib/Ravada/Auth/LDAP.pm | 3 +- lib/Ravada/Auth/SQL.pm | 82 ++- lib/Ravada/Auth/User.pm | 97 +++- lib/Ravada/Booking.pm | 10 +- lib/Ravada/Booking/Entry.pm | 73 ++- lib/Ravada/Domain.pm | 40 +- lib/Ravada/Front.pm | 49 +- public/js/admin.js | 66 ++- public/js/booking/booking.module.js | 5 +- public/js/booking/booking.services.js | 17 +- public/js/booking/calendar.component.js | 3 +- public/js/booking/ldapGroups.component.js | 2 + public/js/booking/localGroups.component.js | 54 ++ public/js/ravada.js | 99 ++-- script/rvd_back | 40 +- script/rvd_front | 474 +++++++++++++++--- t/00_libs.t | 1 + t/17_templates.t | 2 +- t/front/80_access.t | 129 ++++- t/lib/Test/Ravada.pm | 36 ++ t/mojo/10_login.t | 2 +- t/mojo/15_list_bases.t | 103 +++- t/mojo/60_upload.t | 71 +++ t/mojo/70_groups.t | 308 ++++++++++++ t/vm/r30_reserve.t | 123 ++++- templates/booking/formEvent.component.html.ep | 21 +- .../booking/localGroup.component.html.ep | 32 ++ templates/bootstrap/navigation.html.ep | 2 +- templates/bootstrap/new_group.html.ep | 5 +- templates/main/admin_group.html.ep | 39 +- templates/main/admin_groups.html.ep | 40 +- templates/main/admin_settings.html.ep | 2 +- templates/main/list_groups_ldap.html.ep | 22 + templates/main/list_groups_local.html.ep | 20 + templates/main/list_groups_tabs.html.ep | 8 + templates/main/machine_access.html.ep | 4 - templates/main/machine_access_group.html.ep | 66 ++- templates/main/manage_user.html.ep | 14 +- templates/main/upload_group_members.html.ep | 87 ++++ templates/ng-templates/new_group.html.ep | 50 +- 43 files changed, 2235 insertions(+), 301 deletions(-) create mode 100644 lib/Ravada/Auth/Group.pm create mode 100644 public/js/booking/localGroups.component.js create mode 100644 t/mojo/70_groups.t create mode 100644 templates/booking/localGroup.component.html.ep create mode 100644 templates/main/list_groups_ldap.html.ep create mode 100644 templates/main/list_groups_local.html.ep create mode 100644 templates/main/list_groups_tabs.html.ep create mode 100644 templates/main/upload_group_members.html.ep diff --git a/MANIFEST b/MANIFEST index ea4e194d6..4b71696c2 100644 --- a/MANIFEST +++ b/MANIFEST @@ -55,6 +55,6 @@ lib/Ravada/I18N/ca.po lib/Ravada/NetInterface.pm lib/Ravada/Auth.pm lib/Ravada/Domain.pm -lib/Ravada/Routes.pm +lib/Ravada/Route.pm script/rvd_front script/rvd_back diff --git a/lib/Ravada.pm b/lib/Ravada.pm index f20425e83..0d935bde2 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -1557,8 +1557,9 @@ sub _add_indexes_generic($self) { "unique (id_domain)" ] ,group_access => [ - "unique (id_domain,name)" + "unique (id_domain,name,id_group)" ,"index(id_domain)" + ,"index(id_group)" ] ,iso_images => [ "unique (name)" @@ -1609,6 +1610,11 @@ sub _add_indexes_generic($self) { "index(id_booking_entry,ldap_group)" ,"index(id_booking_entry)" ] + ,booking_entry_local_groups => [ + "unique(id_booking_entry,id_group)" + ,"index(id_booking_entry)" + ] + ,booking_entry_users => [ "index(id_booking_entry,id_user)" ,"index(id_booking_entry)" @@ -1619,6 +1625,12 @@ sub _add_indexes_generic($self) { ,"index(id_base)" ,"index(id_booking_entry)" ] + ,groups_local => [ + 'UNIQUE (name)' + ] + ,users_group => [ + 'UNIQUE(id_user, id_group)' + ] ,volumes => [ "index(id_domain)" @@ -2202,11 +2214,21 @@ sub _sql_create_tables($self) { ,xml => 'TEXT' } ] + , + [ groups_local => { + id => 'integer PRIMARY KEY AUTO_INCREMENT', + ,name => 'char(255) NOT NULL' + ,is_external => 'int NOT NULL default(0)' + ,external_auth => 'varchar(64) default NULL' + } + ] ,[ group_access => { id => 'integer NOT NULL PRIMARY KEY AUTO_INCREMENT' ,id_domain => 'integer NOT NULL references `domains` (`id`) ON DELETE CASCADE' - ,name => 'char(80)' + ,id_group => 'integer references `groups_local` (`id`) ON DELETE CASCADE' + ,name => 'char(80) DEFAULT NULL' + ,type => 'char(40)' } ] , @@ -2338,6 +2360,17 @@ sub _sql_create_tables($self) { } ] , + [ + booking_entry_local_groups => { + id => 'INTEGER PRIMARY KEY AUTO_INCREMENT' + ,id_booking_entry + => 'int not null references `booking_entries` (`id`) ON DELETE CASCADE' + ,id_group => 'int not null' + ,date_changed => 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' + } + ] + , + [ booking_entry_users => { id => 'INTEGER PRIMARY KEY AUTO_INCREMENT' @@ -2367,6 +2400,12 @@ sub _sql_create_tables($self) { } ] , + [ users_group => { + id => 'integer PRIMARY KEY AUTO_INCREMENT', + ,id_user => 'integer NOT NULL' + ,id_group =>'integer NOT NULL' + } + ], [ volumes => { id => 'integer PRIMARY KEY AUTO_INCREMENT', @@ -2660,7 +2699,7 @@ sub _sql_insert_defaults($self){ ,{ id_parent => $id_backend ,name => 'bookings' - ,value => 0 + ,value => 1 } ,{ id_parent => $id_backend diff --git a/lib/Ravada/Auth/Group.pm b/lib/Ravada/Auth/Group.pm new file mode 100644 index 000000000..469c934d0 --- /dev/null +++ b/lib/Ravada/Auth/Group.pm @@ -0,0 +1,188 @@ +package Ravada::Auth::Group; + +use warnings; +use strict; + +=head1 NAME + +Ravada::Auth::Group - Group management library for Ravada + +=cut + +use Carp qw(carp); +use Data::Dumper qw(Dumper); +use Hash::Util qw(lock_hash); + +use Moose; + +use feature qw(signatures); +no warnings "experimental::signatures"; + +has 'name' => ( + is => 'rw' + ,isa => 'Str' + ,required => 1 +); + +our $CON; + +sub _init_connector { + my $connector = shift; + + $CON = \$connector if defined $connector; + return if $CON; + + $CON= \$Ravada::CONNECTOR if !$CON || !$$CON; + $CON= \$Ravada::Front::CONNECTOR if !$CON || !$$CON; + + if (!$CON || !$$CON) { + my $connector = Ravada::_connect_dbh(); + $CON = \$connector; + } + + die "Undefined connector" if !$CON || !$$CON; +} + +sub BUILD { + my $self = shift; + _init_connector(); + $self->_load_data(); +} + +sub _load_data($self) { + _init_connector(); + + confess "No group name nor id " if !defined $self->name && !$self->id; + + confess "Undefined \$\$CON" if !defined $$CON; + my $sth = $$CON->dbh->prepare( + "SELECT * FROM groups_local WHERE name=? "); + $sth->execute($self->name); + my ($found) = $sth->fetchrow_hashref; + $sth->finish; + + return if !$found->{name}; + + lock_hash %$found; + $self->{_data} = $found if ref $self && $found; + +} + +sub open($self, $id) { + _init_connector(); + my $sth = $$CON->dbh->prepare( + "SELECT name FROM groups_local WHERE id=?" + ); + $sth->execute($id); + my ($name) = $sth->fetchrow; + confess "Error: unknown group id '$id'" if !$name; + + return $self->new(name => $name); +} + +sub id { + my $self = shift; + my $id; + eval { $id = $self->{_data}->{id} }; + confess $@ if $@; + + return $id; +} + +sub add_group(%args) { + _init_connector(); + my $name = delete $args{name}; + my $external_auth = delete $args{external_auth}; + my $is_external = 0; + $is_external = 1 if $external_auth; + + confess "WARNING: Unknown arguments ".Dumper(\%args) + if keys %args; + + + my $sth; + eval { $sth = $$CON->dbh->prepare( + "INSERT INTO groups_local(name,is_external,external_auth)" + ." VALUES(?,?,?)"); + $sth->execute($name, $is_external, $external_auth); + }; + confess $@ if $@; + return Ravada::Auth::Group->new(name => $name); +} + +sub remove_member($self, $name) { + my $sth = $$CON->dbh->prepare("SELECT id FROM users WHERE name=?"); + $sth->execute($name); + my ($id_user) = $sth->fetchrow; + + $sth = $$CON->dbh->prepare("DELETE FROM users_group " + ." WHERE id_user=?" + ); + $sth->execute($id_user); +} + +sub _remove_all_members($self) { + my $sth = $$CON->dbh->prepare("DELETE FROM users_group " + ." WHERE id_group=?" + ); + $sth->execute($self->id); +} + +sub _remove_access($self) { + my $sth = $$CON->dbh->prepare("DELETE FROM group_access " + ." WHERE type='local'" + ." AND name=?" + ); + $sth->execute($self->name); +} + +sub members($self) { + my $sth = $$CON->dbh->prepare( + "SELECT u.id,u.name FROM users u,users_group ug " + ." WHERE u.id = ug.id_user " + ." AND ug.id_group=?" + ." ORDER BY name" + ); + $sth->execute($self->id); + my @members; + while (my ($uid,$name) = $sth->fetchrow) { + push @members,($name); + } + return @members; +} +sub members_info($self) { + my $sth = $$CON->dbh->prepare( + "SELECT u.id,u.name FROM users u,users_group ug " + ." WHERE u.id = ug.id_user " + ." AND ug.id_group=?" + ." ORDER BY name" + ); + $sth->execute($self->id); + my @members; + while (my ($uid,$name) = $sth->fetchrow) { + push @members,({ id => $uid, name => $name}); + } + return @members; +} + +sub remove($self) { + my $id = $self->id; + + $self->_remove_all_members(); + $self->_remove_access(); + + my $sth = $$CON->dbh->prepare( + "DELETE FROM groups_local WHERE id=?" + ); + $sth->execute($id); +} + +sub exists_id($id) { + _init_connector(); + my $sth = $$CON->dbh->prepare("SELECT id FROM groups_local WHERE id=?"); + $sth->execute($id); + my ($found) = $sth->fetchrow; + return $found; +} + +1; diff --git a/lib/Ravada/Auth/LDAP.pm b/lib/Ravada/Auth/LDAP.pm index df74083e5..ce8a51459 100644 --- a/lib/Ravada/Auth/LDAP.pm +++ b/lib/Ravada/Auth/LDAP.pm @@ -280,7 +280,7 @@ sub search_user { } else { $args{name} = $_[0]; } - die "Error: LDAP not configured" if !exists $$CONFIG->{ldap}; + confess "Error: LDAP not configured" if !exists $$CONFIG->{ldap}; my $username = delete $args{name} or confess "Missing user name"; my $retry = (delete $args{retry} or 0); @@ -483,6 +483,7 @@ sub search_group { =cut sub search_group_members($cn, $retry = 0) { + confess if !exists $$CONFIG->{ldap}; my $base = ($$CONFIG->{ldap}->{groups_base} or "ou=groups,"._dc_base()); my $ldap = _init_ldap_admin(); diff --git a/lib/Ravada/Auth/SQL.pm b/lib/Ravada/Auth/SQL.pm index 50f0ea64f..c158d97d7 100644 --- a/lib/Ravada/Auth/SQL.pm +++ b/lib/Ravada/Auth/SQL.pm @@ -327,7 +327,7 @@ Sets or gets the external auth value of an user. sub external_auth($self, $value=undef) { if (!defined $value) { - return $self->{_data}->{external_auth}; + return ($self->{_data}->{external_auth} or ''); } my $sth = $$CON->dbh->prepare( "UPDATE users set external_auth=? WHERE id=?" @@ -633,6 +633,11 @@ Removes the user =cut sub remove($self) { + return if !$self->id; + + die "Error: user ".$self->name." can not be removed.\n" + if $self->id == Ravada::Utils::user_daemon->id; + my $sth = $$CON->dbh->prepare("DELETE FROM grants_user where id_user=?"); $sth->execute($self->id); @@ -1287,6 +1292,26 @@ sub groups($self) { } +=head2 groups_local + +Returns a list of the local groups this user belogs to + +=cut + +sub groups_local($self) { + my $sth = $$CON->dbh->prepare("SELECT g.name FROM groups_local g,users_group ug " + ." WHERE g.id = ug.id_group " + ." AND ug.id_user = ?" + ." ORDER BY g.name " + ); + $sth->execute($self->id); + my @groups; + while (my ($name) = $sth->fetchrow) { + push @groups,($name); + } + return @groups; +} + =head2 disk_used Returns the amount of disk space used by this user in MB @@ -1309,6 +1334,61 @@ sub disk_used($self) { return $used; } +=head2 add_to_group + +Adds the user to a group or list of groups + +Arguments: list of group names + +=cut + +sub add_to_group($self, @group) { + my $sth = $$CON->dbh->prepare( + "INSERT INTO users_group (id_group, id_user) " + ." VALUES (?,?)" + ); + for my $group (@group) { + if (!ref($group)) { + if ($group =~ /^\d+$/) { + $group = Ravada::Auth::Group->open($group); + } else { + $group = Ravada::Auth::Group->new(name => $group); + } + } + confess "Error: unknown group ".$group->name + if !$group->id; + + $sth->execute($group->id,$self->id); + } +} + +=head2 remove_from_group + +Removes the user from a group or list of groups + +Arguments: list of group names + +=cut + + +sub remove_from_group($self, @group) { + + my $sth = $$CON->dbh->prepare( + "DELETE FROM users_group WHERE id_group=? AND id_user=? " + ); + for my $group (@group) { + if (!ref($group)) { + if ($group =~ /^\d+$/) { + $sth->execute($group,$self->id); + next; + } + $group = Ravada::Auth::Group->new(name => $group); + } + $sth->execute($group->id,$self->id); + } +} + + sub _load_network($network) { confess "Error: undefined network" if !defined $network; diff --git a/lib/Ravada/Auth/User.pm b/lib/Ravada/Auth/User.pm index f807a453c..9e867b3ef 100644 --- a/lib/Ravada/Auth/User.pm +++ b/lib/Ravada/Auth/User.pm @@ -16,6 +16,9 @@ use Encode; use Mojo::JSON qw(decode_json); use Moose::Role; +use Ravada::Auth::LDAP; +use Ravada::Auth::Group; + no warnings "experimental::signatures"; use feature qw(signatures); @@ -340,6 +343,37 @@ sub allowed_access($self,$id_domain) { return 0; } +=head2 allowed_access_group + +Return true if the user belongs to a group that can access the base +Also it returns true when there are no group restrictions for that VM. + +=cut + +sub allowed_access_group($self,$id_domain) { + return 1 if $self->is_admin; + + my $sth = $$CONNECTOR->dbh->prepare( + "SELECT id_group,name from group_access " + ." WHERE id_domain=?" + ." AND type=?" + ); + $sth->execute($id_domain, 'local'); + my @groups; + while ( my ($id_group,$name) = $sth->fetchrow ) { + if (!$id_group && $name) { + $id_group= $name; + } + push @groups,($id_group) if defined $id_group; + } + return 1 if !@groups; + + for my $id_group ( @groups ) { + return 1 if $self->is_member($id_group); + } + return 0; +} + sub _list_domains_access($self) { _init_connector(); @@ -416,24 +450,44 @@ sub _load_allowed { sub _load_allowed_groups($self) { - my $sth = $$CONNECTOR->dbh->prepare("SELECT id_domain,name from group_access"); - my ($id_domain, $name); + my $sth = $$CONNECTOR->dbh->prepare("SELECT id,id_domain,name,id_group,type from group_access"); + my ($id, $id_domain, $name, $id_group, $type); $sth->execute(); - $sth->bind_columns(\($id_domain, $name)); - while ( $sth->fetch ) { + $sth->bind_columns(\($id, $id_domain, $name, $id_group, $type)); + while ( my $row = $sth->fetchrow_hashref ) { + $type = 'ldap' if !defined $type; next if $self->{_allowed}->{$id_domain}; $self->{_allowed}->{$id_domain} = 0; - next unless $self->is_external && $self->external_auth eq 'ldap'; - - if (!Ravada::Auth::LDAP::search_group(name => $name)) { - next; + if ($type eq 'ldap') { + next unless $self->is_external && $self->external_auth eq 'ldap'; + if (!Ravada::Auth::LDAP::search_group(name => $name)) { + next; + } + $self->{_allowed}->{$id_domain} = 1 + if $self->ldap_entry + && Ravada::Auth::LDAP::is_member($self->ldap_entry, $name); + } elsif ($type eq 'local') { + my $group; + if ($id_group) { + $group = Ravada::Auth::Group->open($id_group) + } elsif($name) { + $group = Ravada::Auth::Group->new(name => $name); + } else { + warn "Error: group access withouth id_group or group name id=$id"; + next; + } + if (!$group || !$group->id) { + warn "Error: unknown group '$name' for group access ".Dumper($row); + } else { + $self->{_allowed}->{$id_domain} = 1 + if $self->is_member($group); + } + } else { + warn "Error: unknown type '$type' for group access '$name'"; } - $self->{_allowed}->{$id_domain} = 1 - if $self->ldap_entry - && Ravada::Auth::LDAP::is_member($self->ldap_entry, $name); } } @@ -470,4 +524,25 @@ sub list_requests($self, $date_req=Ravada::Utils::date_now(3600)) { return @req; } +=head2 is_member + +Returns wether an user is member of a group + +Arguments: group name or object + +=cut + +sub is_member($self, $group) { + confess "Error: undefined group" if !defined $group; + if (!ref($group)) { + if ($group =~ /^\d+$/) { + $group = Ravada::Auth::Group->open($group); + } else { + $group = Ravada::Auth::Group->new(name => $group); + } + } + my $is_member = grep { $_ eq $group->name} $self->groups_local; + return ($is_member or 0); +} + 1; diff --git a/lib/Ravada/Booking.pm b/lib/Ravada/Booking.pm index 13c5f6f1e..bb6ea22c2 100644 --- a/lib/Ravada/Booking.pm +++ b/lib/Ravada/Booking.pm @@ -36,11 +36,15 @@ sub BUILD($self, $args) { my $day_of_week = delete $args->{day_of_week}; my %entry; - my @fields_entry = qw ( bases ldap_groups users time_start time_end ); + my @fields_entry = qw ( bases ldap_groups local_groups users time_start time_end ); for (@fields_entry) { $entry{$_} = delete $args->{$_}; } + my %fields = map { $_ => 1 } keys %$args; + delete @fields{'title','id_owner','description','date_created','local_groups'}; + die "Error: unknown arguments ".(join("," , keys %fields)) if keys %fields; + $self->_insert_db(%$args , date_start => $date->ymd , date_end => $date_end->ymd @@ -386,7 +390,7 @@ sub bookings_range(%args) { $date_start = DateTime::Format::DateParse->parse_datetime($date_start) if !ref($date_start); $date_start->set( hour => 0, minute => 0, second => 0); - my $date_end = ( delete $args{date_end} or _today ) ; + my $date_end = ( delete $args{date_end} or $date_start->clone) ; $date_end = DateTime::Format::DateParse->parse_datetime($date_end) if !ref($date_end); $date_end->set( hour => 0, minute => 0, second => 0); @@ -401,7 +405,7 @@ sub bookings_range(%args) { my %day_of_week = map { $_ => 1 } split //,$day_of_week; #todo check date_end > date_start - die "Error end must be after start ".$date_start." ".$date_end + confess "Error end must be after start ".$date_start." ".$date_end if DateTime->compare( $date_start, $date_end) > 0; my $show_user_allowed = delete $args{show_user_allowed}; diff --git a/lib/Ravada/Booking/Entry.pm b/lib/Ravada/Booking/Entry.pm index 5758a79d2..3e3bc1a1c 100644 --- a/lib/Ravada/Booking/Entry.pm +++ b/lib/Ravada/Booking/Entry.pm @@ -18,6 +18,7 @@ sub BUILD($self, $args) { return $self->_open($args->{id}) if $args->{id}; my $ldap_groups = delete $args->{ldap_groups}; + my $local_groups = delete $args->{local_groups}; my $users = delete $args->{users}; my $bases = delete $args->{bases}; @@ -26,6 +27,7 @@ sub BUILD($self, $args) { $self->_insert_db($args); $self->_add_ldap_groups($ldap_groups); + $self->_add_local_groups($local_groups); $self->_add_users($users); $self->_add_bases($bases); @@ -74,6 +76,45 @@ sub _change_ldap_groups($self, $ldap_groups) { $self->_purge_table('booking_entry_ldap_groups','ldap_group',$ldap_groups,[ $self->ldap_groups ]); } +sub _change_local_groups($self, $local_groups) { + my $sth = $self->_dbh->prepare("DELETE FROM booking_entry_local_groups " + ." WHERE id_booking_entry=?" + ); + $sth->execute($self->_data('id')); + $self->_add_local_groups($local_groups); +} + + +sub _add_local_groups($self, $local_groups) { + return if !$local_groups; + my $id = $self->_data('id'); + my %already_added = map { $_ => 1 } $self->local_groups(); + $local_groups = [ $local_groups ] if !ref($local_groups); + + my $sth = $self->_dbh->prepare("INSERT INTO booking_entry_local_groups " + ."( id_booking_entry, id_group )" + ."values( ?,? ) " + ); + confess "Error: local_groups not an array ref".Dumper($local_groups) + if !ref($local_groups) || ref($local_groups) ne 'ARRAY'; + + my $sth_gr = $self->_dbh->prepare("SELECT id FROM groups_local WHERE name=?"); + my @local_groups2 = @$local_groups; + for my $current_group (@local_groups2) { + if ( $current_group !~ /^\d+$/) { + $sth_gr->execute($current_group); + my ($id_group) = $sth_gr->fetchrow; + die "Error: group '$current_group' not found" if !$id_group; + $current_group=$id_group; + } + + next if $already_added{$current_group}++; + $sth->execute($id, $current_group); + } + +} + + sub _add_ldap_groups($self, $ldap_groups) { return if !$ldap_groups; my $id = $self->_data('id'); @@ -84,6 +125,9 @@ sub _add_ldap_groups($self, $ldap_groups) { ."( id_booking_entry, ldap_group )" ."values( ?,? ) " ); + die "Error: ldap_groups not an array ref".Dumper($ldap_groups) + if !ref($ldap_groups) || ref($ldap_groups) ne 'ARRAY'; + for my $current_group (@$ldap_groups) { next if $already_added{$current_group}++; $sth->execute($id, $current_group); @@ -224,6 +268,9 @@ sub change($self, %fields) { if ($field eq 'ldap_groups') { $self->_change_ldap_groups($fields{$field}); next; + } elsif ($field eq 'local_groups') { + $self->_change_local_groups($fields{$field}); + next; } elsif ($field eq 'users') { $self->_change_users($fields{$field}); next; @@ -305,6 +352,21 @@ sub ldap_groups($self) { return @groups; } +sub local_groups($self) { + my $sth = $self->_dbh->prepare("SELECT g.name " + ." FROM booking_entry_local_groups be,groups_local g" + ." WHERE be.id_booking_entry=?" + ." AND be.id_group=g.id " + ); + $sth->execute($self->id); + my @groups; + while ( my ($group) = $sth->fetchrow ) { + push @groups,($group); + } + return @groups; +} + + sub users ($self) { my $sth = $self->_dbh->prepare("SELECT id_user,u.name " ." FROM booking_entry_users b,users u" @@ -363,8 +425,15 @@ sub user_allowed($entry, $user_name) { for my $allowed_user_name ( $entry->users ) { return 1 if $user_name eq $allowed_user_name; } - for my $group_name ($entry->ldap_groups) { - return 1 if Ravada::Auth::LDAP::is_member($user_name, $group_name); + my $user = Ravada::Auth::SQL->new(name => $user_name); + return 0 if !$user->id; + if ($user->external_auth() eq 'ldap') { + for my $group_name ($entry->ldap_groups) { + return 1 if Ravada::Auth::LDAP::is_member($user_name, $group_name); + } + } + for my $group_name ($entry->local_groups) { + return 1 if $user->is_member($group_name); } return 0; } diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index d3e6e43d9..670b6f57b 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -2075,6 +2075,7 @@ sub info($self, $user) { ,is_public => $self->is_public ,show_clones => $self->show_clones ,id_base => $self->id_base + ,is_public => $self->is_public ,is_active => $is_active ,is_hibernated => $self->is_hibernated ,spice_password => $self->spice_password @@ -4963,10 +4964,9 @@ sub type { if (!exists $self->{_data} || !exists $self->{_data}->{vm}) { my ($type) = ref($self) =~ /.*::([a-zA-Z][a-zA-Z0-9]*)/; confess "Unknown type from ".ref($self) if !$type; - return $type; + return $type if $type ne 'Domain'; } - confess "Unknown vm ".Dumper($self->{_data}) - if !$self->_data('vm'); + return 'Unknown' if !exists $self->{_data}->{vm}; return $self->_data('vm'); } @@ -6285,7 +6285,7 @@ Arguments is a named list sub grant_access($self, %args) { my $type = delete $args{type} or confess "Error: Missing type"; - return $self->_allow_group_access(%args) if $type eq 'group'; + return $self->_allow_group_access(%args, type=> $type) if $type =~ /^group/; my $attribute = delete $args{attribute} or confess "Error: Missing attribute"; my $value = delete $args{value} or confess "Error: Missing value"; @@ -6323,14 +6323,22 @@ sub grant_access($self, %args) { } sub _allow_group_access($self, %args) { - my $group = delete $args{group} or confess "Error: group required"; + my $group = delete $args{group}; + my $id_group = delete $args{id_group}; + confess "Error: group name or id_group required" unless $group || $id_group; + confess "Error: wrong group name '$group'" if $group && $group =~ /^\d+$/; + + my $type = delete $args{type}; + $type =~ s/.*\.(.*)/$1/; + $type = 'ldap' if !$type || $type eq 'group'; + confess "Error: unknown args ".Dumper(\%args) if keys %args; my $sth = $$CONNECTOR->dbh->prepare( "INSERT INTO group_access " - ."( id_domain,name)" - ." VALUES(?,? )" + ."( id_domain, id_group, name, type)" + ." VALUES(?,?,?,? )" ); - $sth->execute($self->id, $group); + $sth->execute($self->id,$id_group, $group, $type); } =head2 list_access_groups @@ -6339,14 +6347,20 @@ Returns the list of groups who can access this virtual machine =cut -sub list_access_groups($self) { - my $sth = $$CONNECTOR->dbh->prepare("SELECT name from group_access " +sub list_access_groups($self, $type) { + my $sth = $$CONNECTOR->dbh->prepare("SELECT id,id_group,name from group_access " ." WHERE id_domain=?" + ." AND type=?" ); - $sth->execute($self->id); + $sth->execute($self->id, $type); my @groups; - while ( my ($name) = $sth->fetchrow ) { - push @groups,($name); + my $sth_gname = $$CONNECTOR->dbh->prepare("SELECT name FROM groups_local WHERE id=?"); + while ( my $row = $sth->fetchrow_hashref ) { + if (!$row->{name} && $row->{id_group}) { + $sth_gname->execute($row->{id_group}); + ($row->{name}) = $sth_gname->fetchrow; + } + push @groups,($row->{name}); } return @groups; } diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 1aacabcde..4386afa56 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -156,7 +156,7 @@ sub list_machines_user($self, $user, $access_data={}) { my ($clone) = ($clones[0] or undef); next unless - $clone && $show_clones + $clone && $show_clones && $user->allowed_access_group($id) || $user->is_admin || ($is_public && $user->allowed_access($id)) || ($id_owner == $user->id); @@ -1931,6 +1931,53 @@ sub add_to_bundle ($self, $id_bundle, $id_domain){ } +=head2 upload_group_members + +Upload a list of users to be added to a group + +=head3 Arguments + +=over + +=item * string with users + +=item * exclusive: remove all other users not uploaded here + +=back + +=cut + +sub upload_group_members($self, $group_name, $users, $exclusive=0) { + my $group = Ravada::Auth::Group->new(name => $group_name); + $group = Ravada::Auth::Group::add_group(name => $group_name) if !$group->id; + my ($found,$count) = (0,0); + my @error; + my @external = ( is_external => 1, external_auth => 'sso'); + my %members; + for my $line (split /\n/,$users) { + my ($name) = split(/:/,$line); + $found++; + my $user = Ravada::Auth::SQL->new(name => $name); + if (!$user || !$user->id) { + $user = Ravada::Auth::SQL::add_user(name => $name, + ,@external); + } + $members{$name}++; + if (!$user->is_member($group_name)) { + $user->add_to_group($group_name); + $count++; + } else { + push @error,("User $name already a member"); + } + } + if ($exclusive) { + for my $name ($group->members) { + $group->remove_member($name) unless $members{$name}; + } + } + return ($found, $count, \@error); +} + =head2 version Returns the version of the main module diff --git a/public/js/admin.js b/public/js/admin.js index 467bd7f67..f17605397 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -697,7 +697,7 @@ ravadaApp.directive("solShowMachine", swMach) $scope.list_groups= function() { $scope.loading_groups = true; $scope.error = ''; - $http.get('/list_ldap_groups') + $http.get('/group/ldap/list') .then(function(response) { $scope.loading_groups = false; $scope.groups = response.data; @@ -1462,18 +1462,47 @@ ravadaApp.directive("solShowMachine", swMach) }; function admin_groups_ctrl($scope, $http) { - var group; $scope.group_filter = ''; - $scope.username_filter = 'a'; + $scope.username_filter = ''; + var type; + var group_name; + var group_id; + $scope.init = function(type0, group_name0, group_id0) { + type = type0; + group_name = group_name0; + group_id = group_id0; + $scope.list_group_members(); + }; $scope.list_ldap_groups = function() { - $http.get('/list_ldap_groups/'+$scope.group_filter) + $http.get('/group/ldap/list/'+$scope.group_filter) .then(function(response) { $scope.ldap_groups=response.data; }); }; - $scope.list_group_members = function(group_name) { + list_local_groups=function() { + $http.get('/group/local/list_data') + .then(function(response) { + $scope.local_groups=response.data; + $scope.local_groups_all=response.data; + }); + } + $scope.filter_local_groups=function() { + $scope.local_groups = []; + var re = new RegExp($scope.group_filter); + for (var i=0; i<$scope.local_groups_all.length; i++) { + if (re.test($scope.local_groups_all[i])) { + $scope.local_groups.push($scope.local_groups_all[i]); + } + } + }; + $scope.list_groups=function() { + $scope.list_ldap_groups(); + list_local_groups(); + }; + + $scope.list_group_members = function() { group = group_name; - $http.get('/list_ldap_group_members/'+group_name) + $http.get('/group/'+type+'/list_members/'+group_name) .then(function(response) { $scope.group_members=response.data; }); @@ -1481,29 +1510,32 @@ ravadaApp.directive("solShowMachine", swMach) $scope.list_users = function() { $scope.loading_users = true; $scope.error = ''; - $http.get('/list_ldap_users/'+$scope.username_filter) + $http.get('/user/'+type+'/list/'+$scope.username_filter) .then(function(response) { $scope.loading_users = false; $scope.error = response.data.error; $scope.users = response.data.entries; }); }; - $scope.add_member = function(cn) { - $http.post("/ldap/group/add_member/" + $scope.add_member = function(user_id, user_name) { + $http.post("/group/"+type+"/add_member/" ,JSON.stringify( - { 'group': group - ,'cn': cn + { 'group': group_name + ,'id_user': user_id + ,'id_group': group_id + ,'name': user_name }) ).then(function(response) { - $scope.list_group_members(group); + $scope.list_group_members(); $scope.error = response.data.error; }); }; - $scope.remove_member = function(dn) { - $http.post("/ldap/group/remove_member/" + $scope.remove_member = function(user) { + $http.post("/group/"+type+"/remove_member/" ,JSON.stringify( { 'group': group - ,'dn': dn + ,'id_user': user.id + ,'name': user.name }) ).then(function(response) { $scope.list_group_members(group); @@ -1512,13 +1544,13 @@ ravadaApp.directive("solShowMachine", swMach) }; $scope.remove_group = function() { $scope.confirm_remove=false; - $http.get("/ldap/group/remove/"+group).then(function(response) { + $http.get("/group/"+type+"/remove/"+group).then(function(response) { $scope.error=response.data.error; $scope.removed = true; }); }; - }; + } function settings_global_ctrl($scope, $http) { $scope.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; diff --git a/public/js/booking/booking.module.js b/public/js/booking/booking.module.js index 3b71e3052..d03b853b7 100644 --- a/public/js/booking/booking.module.js +++ b/public/js/booking/booking.module.js @@ -5,8 +5,9 @@ import confirmActions from './confirmActions.component.js' import entryModalComponent from "./entryModal.component.js" import formEventComponent from "./formEvent.component.js" import ldapGroupsComponent from "./ldapGroups.component.js" +import localGroupsComponent from "./localGroups.component.js" import timeComponent from "./time.component.js" -import { svcBookings, svcEntry, svcLDAP } from "./booking.services.js" +import { svcBookings, svcEntry, svcLDAP, svcLocal } from "./booking.services.js" angular.module("ravada.booking", ['ui.bootstrap','angularMoment','angularjsToast','ngMessages']) .component("rvdCalendar", calendarComponent) @@ -15,8 +16,10 @@ angular.module("ravada.booking", ['ui.bootstrap','angularMoment','angularjsToast .component("rvdFormEvent",formEventComponent) .component("rvdTimePicker",timeComponent) .component("ldapGroups", ldapGroupsComponent) + .component("localGroups", localGroupsComponent) .service("apiBookings",svcBookings) .service("apiEntry",svcEntry) .service("apiLDAP",svcLDAP) + .service("apiLocal",svcLocal) .run( amMoment => amMoment.changeLocale('en') ); diff --git a/public/js/booking/booking.services.js b/public/js/booking/booking.services.js index 7599747d1..6806fcae1 100644 --- a/public/js/booking/booking.services.js +++ b/public/js/booking/booking.services.js @@ -3,7 +3,8 @@ export { svcBookings, svcEntry, - svcLDAP + svcLDAP, + svcLocal } svcBookings.$inject = ["$resource"]; @@ -25,7 +26,19 @@ function svcLDAP($resource) { list_groups: { method: 'GET', isArray: true, - params: { action: 'list_ldap_groups'} + params: { action: 'group_ldap_list'} + } + }); +} + +svcLocal.$inject = ["$resource"]; + +function svcLocal($resource) { + return $resource('/:action/:qry',{ qry: '@qry' }, { + list_groups: { + method: 'GET', + isArray: true, + params: { action: 'group_local_list'} } }); } diff --git a/public/js/booking/calendar.component.js b/public/js/booking/calendar.component.js index f72c805f1..91148614f 100644 --- a/public/js/booking/calendar.component.js +++ b/public/js/booking/calendar.component.js @@ -114,7 +114,8 @@ function calendarCtrl($element, $window, apiBookings,$uibModal,moment,apiEntry) dow : [0,0,0,0,0,0,0], editable: true, background_color: "#7ab2fa", - ldap_groups: [] + ldap_groups: [], + local_groups: [] }; const today_dow = moment(selectionInfo.startStr).weekday(); booking_entry.dow[today_dow]=today_dow+1; diff --git a/public/js/booking/ldapGroups.component.js b/public/js/booking/ldapGroups.component.js index eeea28dfc..b93a67a9e 100644 --- a/public/js/booking/ldapGroups.component.js +++ b/public/js/booking/ldapGroups.component.js @@ -43,6 +43,8 @@ function grpCtrl(apiLDAP, $scope, $timeout) { } self.onAdd({ group: self.group_selected}) self.group_selected = null; + // issue a bogus remove so the form is not pristine + self.remove_ldap_group('*UNDEF*'); }; self.remove_ldap_group = group => { self.selected_groups = remove_array_element(self.selected_groups,group) diff --git a/public/js/booking/localGroups.component.js b/public/js/booking/localGroups.component.js new file mode 100644 index 000000000..75b832a0f --- /dev/null +++ b/public/js/booking/localGroups.component.js @@ -0,0 +1,54 @@ +'use strict'; + +export default { + require: { + ngModel: '^ngModel', + }, + bindings: { + editable: '<', + onAdd: '&', + onDelete: '&' + }, + templateUrl: '/booking/localGroup.component.html', + controller: grpCtrl +} +grpCtrl.$inject = ["apiLocal","$scope","$timeout"]; + +function grpCtrl(apiLocal, $scope, $timeout) { + const self = this; + const remove_array_element = (arr,el) => arr.filter(e => e !== el); + const msgError = msg => { self.err = msg; $timeout(() => self.err=null,2000)}; + self.available_groups = []; + self.group_selected = null; + + self.$onInit = () => { + self.ngModel.$render = () => { + self.selected_groups = self.ngModel.$viewValue; + }; + $scope.$watchCollection( () => self.selected_groups, value => { + self.ngModel.$setViewValue(value); + self.required = Object.prototype.hasOwnProperty.call(self.ngModel.$validators,"required"); + if (self.required) self.ngModel.$setValidity("required",!!value.length); + }); + self.getGroups() + }; + self.getGroups = async qry => await apiLocal.list_groups({ qry }).$promise + + self.add_local_group = () => { + if (!self.group_selected) return; + if (self.selected_groups.indexOf(self.group_selected) >= 0) { + msgError(self.group_selected + ' already there'); + } else { + self.selected_groups.push(self.group_selected); + } + self.onAdd({ group: self.group_selected}) + self.group_selected = null; + // issue a bogus remove so the form is not pristine + self.remove_local_group('*UNDEF*'); + }; + self.remove_local_group = group => { + self.selected_groups = remove_array_element(self.selected_groups,group) + self.onDelete({ group }) + } + +} diff --git a/public/js/ravada.js b/public/js/ravada.js index 917cfc4ab..a7c1f497e 100644 --- a/public/js/ravada.js +++ b/public/js/ravada.js @@ -20,6 +20,7 @@ .service("listMess", gtListMess) .controller("SupportForm", suppFormCtrl) .controller("AddUserForm",addUserFormCrtl) + .controller("AddGroupForm",addGroupFormCrtl) .controller("ChangePasswordForm",changePasswordFormCrtl) // .controller("machines", machinesCrtl) // .controller("messages", messagesCrtl) @@ -68,6 +69,31 @@ }; + function addGroupFormCrtl($scope, $http, request){ + $scope.type = 'local'; + $scope.group_name = ''; + $scope.object_class = { + 'posixGroup': true + ,'nsMemberOf': false + ,'groupOfUniqueNames': false + }; + $scope.list_object_class=Object.keys($scope.object_class); + + $scope.add_group = function() { + $scope.new_group_done = false; + $http.post('/group/new' + , JSON.stringify({ 'type': $scope.type + ,'group_name': $scope.group_name + ,'object_class': $scope.object_class + } + ) + ).then(function(response) { + $scope.error = response.data.error; + $scope.new_group_done = $scope.error.length == 0; + }); + }; + }; + function addUserFormCrtl($scope, $http, request){ @@ -323,6 +349,7 @@ $scope.shared_user_found=false; $scope.storage_pools=['default']; $scope.shared_user_count = -1 + $scope.access_groups=[]; $scope.getUnixTimeFromDate = function(date) { date = (date instanceof Date) ? date : date ? new Date(date) : new Date(); @@ -610,13 +637,14 @@ $scope.list_ldap_attributes(); list_users(); list_host_devices(); - list_access_groups(); + list_access_groups('ldap'); + list_access_groups('local'); } $scope.copy_ram = $scope.showmachine.max_mem / 1024 / 1024; }); if (is_admin ) { $scope.list_ldap_attributes(); - list_ldap_groups(); + list_groups(); } $scope.list_cpu_models(); }; @@ -775,7 +803,6 @@ ,'storage': $scope.sp_move.storage_pool }) ).then(function(response) { - console.log(response.data); }); } @@ -875,6 +902,7 @@ $scope.searching_ldap_attributes = false; $scope.user_name = response.data.name; $scope.check_access(); + list_access_groups('ldap'); }); } }; @@ -1076,11 +1104,15 @@ } }); } - var list_ldap_groups = function() { - $http.get('/list_ldap_groups') + var list_groups = function() { + $http.get('/group/ldap/list') .then(function(response) { $scope.ldap_groups=response.data; }); + $http.get('/group/local/list') + .then(function(response) { + $scope.local_groups=response.data; + }); }; /* Host Devices */ @@ -1171,22 +1203,24 @@ }); }; - var list_access_groups = function() { - $http.get("/machine/list_access_groups/"+$scope.showmachine.id).then(function(response) { - $scope.access_groups=response.data; + var list_access_groups = function(type) { + $http.get("/machine/list_access_groups/"+type+"/"+$scope.showmachine.id).then(function(response) { + $scope.access_groups[type]=response.data; }); }; - $scope.add_group_access = function(group) { - $http.get("/machine/add_access_group/"+$scope.showmachine.id+"/"+group) - .then(function(response) { - list_access_groups(); + $scope.add_group_access = function(type,group) { + $http.post("/machine/add_access_group/"+type+"/"+$scope.showmachine.id + ,JSON.stringify( { 'group': group }) + ).then(function(response) { + list_access_groups(type); }); }; - $scope.remove_group_access = function(group) { - $http.get("/machine/remove_access_group/"+$scope.showmachine.id+"/"+group) - .then(function(response) { - list_access_groups(); + $scope.remove_group_access = function(type,group) { + $http.post( "/machine/remove_access_group/"+type+"/"+$scope.showmachine.id + ,JSON.stringify( { 'group':group }) + ).then(function(response) { + list_access_groups(type); }); }; @@ -1561,39 +1595,6 @@ }; }; -/* - function requestsCrtlSingle($scope, $interval, $http, request){ - $scope.getReqs= function() { - $http.get('/requests.json').then(function(response) { - $scope.requests=response.data; - }); - ).then function(response) { - $scope.conflicts = response.data - }) - }; - - $http.get('/list_ldap_groups/') - .then(function(response) { - $scope.ldap_groups=response.data; - }); - $http.get('/list_bases.json') - .then(function(response) { - $scope.bases=response.data; - }); - - }; -/* - function requestsCrtlSingle($scope, $interval, $http, request){ - $scope.getReqs= function() { - $http.get('/requests.json').then(function(response) { - $scope.requests=response.data; - }); - }; -// $interval($scope.getReqs,5000); - $scope.getReqs(); - }; -*/ - function nameAvail($timeout, $q) { return { restrict: 'AE', diff --git a/script/rvd_back b/script/rvd_back index 829db9bf3..92ffd4052 100755 --- a/script/rvd_back +++ b/script/rvd_back @@ -37,6 +37,8 @@ my $FILE_CONFIG; my $ADD_USER_LDAP; my $ADD_USER_FILE; +my $UPLOAD_GROUP_MEMBERS; +my $GROUP; my $ADD_GROUP_LDAP; my $RM_GROUP_LDAP; my $ADD_USER_GROUP; @@ -97,6 +99,7 @@ my $USAGE = "$0 " ." --add-user-ldap : adds a new LDAP user\n" ." --add-user-file: adds new users from a file\n" ." --remove-user : removes a db user\n" + ." --upload-group-members: adds new members to a group\n" ." --add-group-ldap : creates a new LDAP group\n" ." --remove-group-ldap : removes a LDAP group\n" ." --add-user-group : adds user to a LDAP group\n" @@ -135,6 +138,7 @@ my $USAGE = "$0 " ."Create users modifiers\n" ." --type : plain, ldap or sso. Defaults to plain\n" ." --create : only valid for LDAP users, creates the entries.\n" + ." --group: add user as member of this group.\n" ."\n" ; @@ -167,6 +171,8 @@ GetOptions ( help => \$help ,'add-group-ldap=s'=> \$ADD_GROUP_LDAP ,'remove-group-ldap=s'=> \$RM_GROUP_LDAP ,'add-user-group=s'=> \$ADD_USER_GROUP + ,'upload-group-members=s'=> \$UPLOAD_GROUP_MEMBERS + ,'group=s' => \$GROUP ,'import-domain=s' => \$IMPORT_DOMAIN ,'import-vbox=s' => \$IMPORT_VBOX @@ -352,9 +358,10 @@ sub add_user { $is_admin = 1 if $is_admin_q =~ /y/i; - Ravada::Auth::SQL::add_user( name => $login + my $user = Ravada::Auth::SQL::add_user( name => $login , password => $password , is_admin => $is_admin); + $user->add_to_group($GROUP) if $GROUP; } sub add_user_file($ADD_USER_FILE) { @@ -1160,6 +1167,36 @@ sub show_volume($rvd_back) { } } +sub upload_group_members($file) { + if (!$GROUP) { + ($GROUP) = $file =~ m{.*/(.*)\.}; + } + my $ravada = Ravada::Front->new(); + + if (my ($user,$path)= $file =~ /^~(.*)\/(.*)/) { + $user = 'root' if !$user; + my @data = getpwnam($user); + $file = $data[7]."/".$path; + } + die "Error: File $file does not exist\n" + if !-e $file; + + open my $in,"<",$file or die "$! $file\n"; + my $users = join("",<$in>); + close $in; + + my ($found, $count, $error)=$ravada->upload_group_members($GROUP,$users); + + print "Group $GROUP: $found found, $count added.\n"; + if (scalar (@$error)) { + print "Errors:\n"; + for (sort @$error) { + print " - $_\n"; + } + } + +} + sub DESTROY { } @@ -1178,6 +1215,7 @@ if $HIBERNATE_DOMAIN || $SHUTDOWN_DOMAIN; add_user($ADD_USER) if $ADD_USER; add_user_ldap($ADD_USER_LDAP) if $ADD_USER_LDAP; add_user_file($ADD_USER_FILE) if $ADD_USER_FILE; +upload_group_members($UPLOAD_GROUP_MEMBERS) if $UPLOAD_GROUP_MEMBERS; add_group_ldap($ADD_GROUP_LDAP) if $ADD_GROUP_LDAP; remove_group_ldap($RM_GROUP_LDAP) if $RM_GROUP_LDAP; add_user_group($rvd_back, $ADD_USER_GROUP) if $ADD_USER_GROUP; diff --git a/script/rvd_front b/script/rvd_front index 367b0c02b..e582ab96c 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -354,19 +354,74 @@ post '/settings_global' => sub($c) { return $c->render(json => { ok => 1, reload => $reload }); }; -get '/admin/group/#name' => sub($c) { +any '/admin/group/#type/#name' => sub($c) { + my $name = $c->stash('name'); + my $type= $c->stash('type'); + return _admin_group($c, $name, $type); +}; + +sub _upload_members($c, $name) { + my $csv = $c->req->upload('members'); + my $strict = $c->req->param('strict'); + + if($csv->headers->content_type !~ m{text/(csv|plain)}) { + if ($csv->headers->content_type =~ m{application/octet-stream}) { + $USER->send_message("Error processing batch file, probably empty file"); + return; + } + $USER->send_message("Wrong content type ".$csv->headers->content_type + ." , it should be text/csv or plain" + ); + return; + } + + my ($found, $count, $error) = $RAVADA->upload_group_members( + $name, $csv->slurp, $strict + ); + + $USER->send_message("$found entries found in file, $count users added to group $name"); + my $n_msg=0; + for my $msg (@$error) { + $USER->send_message($msg); + last if $n_msg++>10; + } +} + +sub _admin_group($c,$name, $type) { return access_denied($c) unless $USER->can_manage_groups || $USER->can_view_groups; _add_admin_libs($c); - my $group = Ravada::Auth::LDAP::search_group(name => $c->stash('name')); - return $c->render( text => "Error: group ".$c->stash('name')." not found." - ."Groups" - , status => 302) if !$group; - $c->stash(object_class => [ grep !/^top$/,$group->get_value('objectClass')]); + if ( $c->req->method eq 'POST') { + _upload_members($c, $name); + } + + + my ($group, $id_group); + if ($type eq 'ldap') { + $group = Ravada::Auth::LDAP::search_group(name => $name); + return not_found($c) if !$group; + $c->stash(object_class => [ grep !/^top$/,$group->get_value('objectClass')]); + $c->stash(group_name => $name); + } elsif ($type eq 'local') { + if ($name =~ /^\d+$/) { + $group = Ravada::Auth::Group->open($name) if Ravada::Auth::Group::exists_id($name); + } else { + $group = Ravada::Auth::Group->new(name => $name); + } + return not_found($c) if !$group || !$group->id; + $c->stash(object_class => [ ]); + $c->stash(group_name => $group->name); + $id_group = $group->id + } else { + die "Error group type '$type' unknown for group $name"; + } + return $c->render( template => "/main/admin_group" ,group => $group + ,type => $type + ,group_id => $id_group ); }; @@ -1080,7 +1135,9 @@ get '/machine/view/(:id).(:type)' => sub { if ( $domain->id_owner == $USER->id || $USER->can_start_machine($domain) ) { if ( $domain->id_base) { my $base = Ravada::Front::Domain->open($domain->id_base); - if ($base->is_public || $base->show_clones()) { + if (($base->show_clones && $USER->allowed_access_group($base->id)) + || ($base->is_public && $USER->allowed_access($base->id)) + ) { return view_machine($c); } } else { @@ -1353,34 +1410,68 @@ get '/machine/move_access/(#id_domain)/(#id_access)/(#position)' => sub { return $c->render(json => { ok => 1 }); }; -get '/machine/list_access_groups/(#id_domain)' => sub { + +get '/machine/list_access_groups/#type/#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); - return $c->render( json => [ $domain->list_access_groups ] ); + return $c->render( json => [ $domain->list_access_groups($c->stash('type')) ] ); }; -get '/machine/add_access_group/(#id_domain)/(#group)' => sub($c) { +post '/machine/add_access_group/#type/#id_domain' => sub($c) { + my $type = $c->stash('type'); my $id_domain = $c->stash('id_domain'); - my $group = $c->stash('group'); + my $arg = decode_json($c->req->body); + my $group_name = delete $arg->{group}; + + my $id_group; my $ok = 0; eval { my $domain = Ravada::Front::Domain->open($id_domain); - $domain->grant_access(type => 'group' , group => $group); + if ($group_name =~ /^\d+$/) { + $id_group = $group_name; + $group_name =undef; + } + if ($type eq 'local' && !$id_group) { + my $group = Ravada::Auth::Group->new(name => $group_name); + $id_group = $group->id; + $group_name=undef; + } + + $domain->grant_access(type => "group.$type" , group => $group_name, id_group => $id_group); $ok =1; }; return $c->render( json => { ok => $ok, error => $@ }); }; -get '/machine/remove_access_group/(#id_domain)/(#group)' => sub($c) { +post '/machine/remove_access_group/#type/#id_domain' => sub($c) { - my $sth = $RAVADA->_dbh->prepare("DELETE FROM group_access WHERE id_domain=? " - ." AND name=?" - ); - $sth->execute($c->stash('id_domain'), $c->stash('group')); + my $arg = decode_json($c->req->body); + + my $group_name = delete $arg->{group}; + if ($c->stash('type') eq 'ldap') { + + my $sth = $RAVADA->_dbh->prepare("DELETE FROM group_access WHERE id_domain=? " + ." AND name=? " + ." AND type=?" + ); + + $sth->execute($c->stash('id_domain'), $group_name, $c->stash('type')); + } else { + my $group = Ravada::Auth::Group->new(name => $group_name); + if ($group && $group->id) { + my $sth = $RAVADA->_dbh->prepare("DELETE FROM group_access WHERE id_domain=? " + ." AND id_group=? " + ." AND type=?" + ); + + $sth->execute($c->stash('id_domain'), $group->id, $c->stash('type')); + } + + } return $c->render(json => { ok => 1 } ); }; @@ -1544,13 +1635,93 @@ any '/users/register' => sub { return register($c); }; -any '/group/new' => sub { +get '/group/new' => sub($c) { - my $c = shift; - return access_denied($c) if !$USER->is_admin(); - return new_group($c); + return access_denied($c) if !$USER->is_admin(); + + my $type = ( $c->req->param('type') or 'local'); + $c->render(template => 'bootstrap/new_group', type => $type); +}; + +post '/group/new' => sub($c) { + return new_group($c); +}; + +any '/group/upload_members.#req' => sub($c) { + + return access_denied_json($c) unless $USER->is_admin; + _add_admin_libs($c); + + my $csv = $c->req->upload('users'); + if($csv->headers->content_type !~ m{text/(csv|plain)}) { + return $c->render(status => 400 + ,text => "Wrong content type ".$csv->headers->content_type + ." , it should be text/csv or plain" + ); + } + + my $strict = $c->req->param('strict'); + my $group_name = $c->req->param('group'); + + my ($found, $count, $error) = $RAVADA->upload_group_members( + $group_name, $csv->slurp, $strict + ); + + return $c->render(json => + { output => "$count users added" + ,error => $error + }) if $c->stash('req') eq 'json'; + +}; + +get '/group/#type/list_data' => sub($c) { + return _group_list($c,1); +}; + +get '/group/#type/list' => sub($c) { + return _group_list($c); +}; + +get '/group/#type/list/#filter' => sub($c) { + return _group_list($c); +}; + +get '/group_local_list'=> sub($c) { + $c->stash(type => 'local'); + return _group_list($c); +}; + +get '/group_ldap_list'=> sub($c) { + $c->stash(type => 'ldap'); + return _group_list($c); }; +get '/group_local_list/#filter'=> sub($c) { + $c->stash(type => 'local'); + return _group_list($c); +}; + +get '/group_ldap_list/#filter'=> sub($c) { + $c->stash(type => 'ldap'); + return _group_list($c); +}; + +sub _group_list($c, $data=0) { + return access_denied($c) unless $USER->can_view_groups || $USER->can_manage_groups; + + my $type = $c->stash('type'); + my $filter = $c->stash('filter'); + + if ($type eq 'ldap') { + $filter = '*' if !defined $filter; + return _list_ldap_groups($c,$filter, $data); + } elsif($type eq 'local') { + return _list_sql_groups($c, $filter, $data); + } else { + return not_found($c); + } +} + any '/admin/users/upload.#req' => sub($c) { return access_denied_json($c) unless $USER->is_admin; @@ -1777,6 +1948,7 @@ get '/user/list_groups/(#id_user)' => sub($c) { my $user = Ravada::Auth::SQL->search_by_id($id_user); return $c->render(json => [] ) if !$user; + return $c->render(json => [] ) if $user->external_auth && $user->external_auth eq 'ldap' && !$RAVADA->feature('ldap'); return $c->render(json => [$user->groups()]); }; @@ -2290,6 +2462,31 @@ get '/list_users.json' => sub($c) { return $c->render(json => $RAVADA->list_users ); }; +get '/user/#type/list' => sub($c) { + return _list_users($c); +}; + +get '/user/#type/list/#name' => sub($c) { + return _list_users($c); +}; + + +sub _list_users($c) { + return access_denied($c) unless $USER->can_manage_users || $USER->can_manage_groups; + + my $name = ($c->stash('name') or ''); + my $type = $c->stash('type'); + if ($type eq 'ldap') { + $name .= '*' unless $name =~ m{\*$}; + return _list_ldap_users($c,$name); + } elsif ($type eq 'local') { + my $users = $RAVADA->list_users($name); + return $c->render(json => { entries => $users}); + } else { + return not_found($c); + } +}; + get '/search_user/#name'=> sub($c) { my $name = $c->stash('name'); return _search_user($c, $name); @@ -2314,10 +2511,14 @@ sub _search_user($c,$name='') { 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; +get '/group/#type/list_members/#name' => sub($c) { + return access_denied($c) if !$USER->can_view_groups; + + my $name = $c->stash('name'); + my $type = $c->stash('type'); + + return _list_group_members($c,$type,$name); - return _list_ldap_groups($c,'*'); }; get '/list_ldap_users' => sub($c) { @@ -2349,70 +2550,154 @@ get '/list_ldap_group_members/#name' => sub($c) { return _list_ldap_group_members($c,$name); }; -post '/ldap/group/add_member' => sub($c) { +post '/group/#type/add_member' => sub($c) { return access_denied($c) if !$USER->can_manage_groups; my $arg = decode_json($c->req->body); - my $login = delete $arg->{cn}; + my $login = delete $arg->{name}; + my $id_user = delete $arg->{id_user}; my $group = delete $arg->{group}; + my $id_group = delete $arg->{id_group}; return $c->render(json => { error => "Error: unknown args ".Dumper($arg)}) if keys %$arg; - eval { - Ravada::Auth::LDAP::add_to_group($login, $group); - }; - my $error = $@; + my $type = $c->stash('type'); + my $error = ''; + + my $user; + if ($id_user) { + $user = Ravada::Auth::SQL->search_by_id($id_user); + $login = $user->name if $user; + } + + if ($type eq 'ldap') { + die "Error: missing login name" if !$login; + die "Error: missing group name" if !$group; + eval { + Ravada::Auth::LDAP::add_to_group($login, $group); + }; + $error = $@; + } elsif ($type eq 'local') { + my $group = Ravada::Auth::Group->open($id_group); + if ($user->is_member($group->id)) { + $error = "Error: user $login already added to $id_group"; + } else { + $user->add_to_group($group->id); + } + } else { $error = "Error: unkonwn group type '$type" } $error =~ s/(.*) at lib.*/$1/ if $error; return $c->render(json => { error => $error } ); }; -post '/ldap/group/remove_member' => sub($c) { + +post '/group/#type/remove_member' => sub($c) { return access_denied($c) if !$USER->can_manage_groups; my $arg = decode_json($c->req->body); - my $dn = delete $arg->{dn}; + my $type = $c->stash('type'); my $group = delete $arg->{group}; + my $id_group = delete $arg->{id_group}; + my $user_name = delete $arg->{name}; + my $id_user = delete $arg->{id_user}; return $c->render(json => { error => "Error: unknown args ".Dumper($arg)}) if keys %$arg; + if ($id_user && $id_user =~ /^\d+$/) { + my $user = Ravada::Auth::SQL->search_by_id($id_user); + $user_name = $user->name; + } - eval { - Ravada::Auth::LDAP::remove_from_group($dn, $group); - }; - return $c->render(json => { error => ($@ or '') } ); + my $error = ''; + $error = "Error: unkonwn args ".Dumper($arg) if keys %$arg; + if ($type =~ /ldap/i) { + if (!defined $user_name) { + $error = "Error: missing user name"; + } else { + eval { + Ravada::Auth::LDAP::remove_from_group($user_name, $group); + }; + $error = ( $@ or ''); + } + } elsif ($type eq 'local') { + if (!defined $id_user) { + $error = "Error: missing user id"; + } else { + my $user = Ravada::Auth::SQL->search_by_id($id_user); + $id_group = $group if !defined $id_group && $group =~ /^\d+$/; + + $user->remove_from_group($id_group); + } + } + return $c->render(json => { error => $error } ); }; + get '/ldap/group/remove/(#group)' => sub($c) { return access_denied($c) if !$USER->can_manage_groups; eval { Ravada::Auth::LDAP::remove_group($c->stash('group')) }; return $c->render( json => { error => ($@ or '') }); }; +get '/group/#type/remove/#group' => sub($c) { + return access_denied($c) if !$USER->can_manage_groups; + my $error = ''; + my $type = $c->stash('type'); + + if ($type =~ /ldap/i) { + eval { + Ravada::Auth::LDAP::remove_group($c->stash('group')); + my $sth = $RAVADA->_dbh->prepare( + "DELETE FROM group_access WHERE type = 'ldap' " + ." AND name=?" + ); + $sth->execute($c->stash('group')); + }; + + $error = $@; + } elsif ($type eq 'local') { + my $group = Ravada::Auth::Group->open($c->stash('group')); + if ($group && $group->id) { + $group->remove; + } else { + $error = "Error: unknown group $group"; + } + } + warn $error if $error; + return $c->render( json => { error => $error }); +}; + get '/booking' => sub($c) { - return $c->render(template => '/ng-templates/error' - ,message => "LDAP is required to set up bookings. " - ."" - ."See how to configure LDAP authentication." - ) - if !$RAVADA->feature('ldap'); - my @groups = Ravada::Auth::LDAP::search_group( name => '*' ); - return $c->render(template => '/ng-templates/error' - ,message => "LDAP groups are required to set up bookings. No groups found. Add new entries here." - ) if !@groups; + my $sth = $RAVADA->_dbh->prepare("SELECT count(*) FROM groups_local"); + $sth->execute(); + my ($groups_local) = $sth->fetchrow; - my @members_found; + my @groups_ldap; + $sth = $RAVADA->_dbh->prepare("SELECT count(*) FROM users_group"); + $sth->execute(); + my ($members_found) = $sth->fetchrow; + + if (!$groups_local && $RAVADA->feature('ldap')) { + @groups_ldap = Ravada::Auth::LDAP::search_group( name => '*' ); + if (!$members_found) { + for my $group (@groups_ldap) { + $members_found = $group->get_value('memberUid'); + last if $members_found; + } + } + } - for my $group (@groups) { - @members_found = $group->get_value('memberUid'); - last if @members_found; + if (!$groups_local && !@groups_ldap) { + return $c->render(template => '/ng-templates/error' + ,message => "Groups are required to set up bookings. No groups found. Add new entries here." + ); } - if (!@members_found) { + if (!$members_found) { return $c->render(template => '/ng-templates/error' - ,message => "LDAP groups are required to set up bookings. Some groups where found but no members belong to them. Add new entries here." + ,message => "Groups are required to set up bookings. Some groups where found but no members belong to them. Add new entries here." ) } @@ -2436,6 +2721,8 @@ post '/v1/bookings/' => sub($c) { my $arg = decode_json($c->req->body); delete $arg->{dow}; delete $arg->{repeat}; + delete $arg->{editable}; + delete $arg->{background_color}; $arg->{id_owner} = $USER->id; my $dow = ''; for my $curr ( split//,$arg->{day_of_week} ) { @@ -2455,6 +2742,7 @@ post '/v1/bookings/' => sub($c) { get '/v1/booking_entry/:id' => sub($c) { my $booking_entry = Ravada::Booking::Entry->new( id => $c->stash('id')); $booking_entry->{_data}->{ldap_groups} = [ $booking_entry->ldap_groups ]; + $booking_entry->{_data}->{local_groups} = [ $booking_entry->local_groups ]; $booking_entry->{_data}->{users} = [ $booking_entry->users ]; return $c->render( json => $booking_entry->{_data} ); }; @@ -3580,33 +3868,37 @@ sub _search_requested_machine { sub new_group($c) { my @error = (); + my $arg = decode_json($c->req->body); - my $groupname = ($c->param('groupname') or ''); - my $object_class = $c->every_param('object_class'); - - my $object_class_checkbox; - for (@$object_class) { - $object_class_checkbox->{$_} = 'checked'; + my $type = delete $arg->{'type'}; + my $groupname = delete $arg->{'group_name'}; + my $h_object_class = delete $arg->{'object_class'}; + if(!keys %$h_object_class) { + $h_object_class->{'posixGroup'}=1; } - $c->stash(object_class => $object_class_checkbox); + warn "Warning: extra arguments ignored ".Dumper($arg) + if keys %$arg; - $object_class = ['top',@$object_class]; + my $object_class = ['top',keys %$h_object_class]; if ($groupname) { - if ($groupname =~ /^[0-9a-zA-Z._-]+$/) { - eval { + if ($groupname !~ /\s/) { + if ($type eq 'ldap') { + eval { Ravada::Auth::LDAP::add_group($groupname,undef, $object_class); - }; - push @error,($@) if $@; + }; + push @error,($@) if $@; + } else { + Ravada::Auth::Group::add_group(name => $groupname); + } } else { push @error,("Error: group name '$groupname' invalid." - ,"Group name can only contain words, numbers, dashes, dots and underscores" ); + ,"Group name can not contain spaces"); } } else { - $c->stash(object_class => { posixGroup => 'checked' }); + push @error,("Empty group name"); } - $c->render(template => 'bootstrap/new_group', error => \@error - ,groupname => $groupname); + return $c->render(json => { error => \@error }); } sub register { @@ -4360,12 +4652,37 @@ sub maintenance($c) { ); } -sub _list_ldap_groups($c, $name='*') { +sub _list_sql_groups($c, $filter=undef, $data=0) { + my $sql = "SELECT id,name FROM groups_local "; + $sql .= " WHERE name like ? " if defined $filter; + $sql .= " ORDER BY name"; + my $sth = $RAVADA->_dbh->prepare($sql); + if (defined $filter) { + $sth->execute('%'.$filter.'%'); + } else { + $sth->execute(); + } + my @groups; + while (my $row = $sth->fetchrow_hashref) { + if ($data) { + push @groups,($row); + } else { + push @groups,($row->{name}); + } + } + return $c->render(json => \@groups); +} + +sub _list_ldap_groups($c, $name='*', $data=0) { return $c->render(json => []) if !$RAVADA->feature('ldap'); $name = '*'.$name.'*' if $name !~ /\*/; my @groups = Ravada::Auth::LDAP::search_group( name => $name ); - return $c->render(json => [ sort { $a cmp $b } map { $_->get_value('cn') } @groups ] ); + if ($data) { + return $c->render(json => [ sort { $a cmp $b } map { {name => $_->get_value('cn')} } @groups ] ); + } else { + return $c->render(json => [ sort { $a cmp $b } map { $_->get_value('cn') } @groups ] ); + } }; sub _list_ldap_users($c, $filter='*' ) { @@ -4401,9 +4718,24 @@ sub _list_ldap_users($c, $filter='*' ) { } sub _list_ldap_group_members($c, $name) { - return $c->render(json => [ Ravada::Auth::LDAP::_group_members($name) ] ); + return $c->render(json => [ map { { id => $_, name => $_ } } Ravada::Auth::LDAP::_group_members($name) ] ); } +sub _list_group_members($c, $type, $name) { + if ($type eq 'ldap') { + return _list_ldap_group_members($c, $name); + } elsif ($type eq 'local') { + my $group; + if ($name =~ /^\d+$/) { + $group = Ravada::Auth::Group->open($name); + } else { + $group = Ravada::Auth::Group->new(name => $name); + } + return $c->render(json => [ $group->members_info ]); + } else { + confess "Error: unkonwn group type $type"; + } +} ################################################################################# my $routes = app->routes->children; diff --git a/t/00_libs.t b/t/00_libs.t index a2befc589..fd622bec1 100644 --- a/t/00_libs.t +++ b/t/00_libs.t @@ -6,6 +6,7 @@ use Test::More; use_ok('Ravada::Auth'); use_ok('Ravada::Auth::LDAP'); use_ok('Ravada::Auth::SQL'); +use_ok('Ravada::Auth::Group'); use_ok('Ravada::VM'); use_ok('Ravada::Domain'); use_ok('Ravada::Front::Domain'); diff --git a/t/17_templates.t b/t/17_templates.t index bb6b42aa5..abdb7e277 100644 --- a/t/17_templates.t +++ b/t/17_templates.t @@ -67,7 +67,7 @@ sub _check_html_lint($url, $content, $option = {}) { for my $error ( $lint->errors() ) { next if $error->errtext =~ /Entity .*is unknown/; next if $option->{internal} && $error->errtext =~ /(body|head|html|title).*required/; - if ( $error->errtext =~ /Unknown element <(canvas|footer|header|nav|ldap-groups)/ + if ( $error->errtext =~ /Unknown element <(canvas|footer|header|nav|ldap-groups|local-groups)/ || $error->errtext =~ /Entity && is unknown/ || $error->errtext =~ /should be written as/ || $error->errtext =~ /Unknown attribute.*%/ diff --git a/t/front/80_access.t b/t/front/80_access.t index 4a8b8ff51..752b704e9 100644 --- a/t/front/80_access.t +++ b/t/front/80_access.t @@ -11,6 +11,8 @@ use feature qw(signatures); use lib 't/lib'; use Test::Ravada; +use_ok('Ravada::Auth::Group'); + init('t/etc/ravada_ldap_basic.conf'); clean(); @@ -38,19 +40,24 @@ sub _remove_bases(@bases) { } } -sub test_access_by_group($vm) { - my $base = create_domain($vm->type); - $base->prepare_base(user_admin); - $base->is_public(1); - +sub _create_group_ldap() { my $g_name = new_domain_name(); my $group = Ravada::Auth::LDAP::search_group(name => $g_name); if (!$group) { Ravada::Auth::LDAP::add_group($g_name); } + return $g_name; +} + +sub test_access_by_group_ldap($vm, $type='group') { + my $base = create_domain($vm->type); + $base->prepare_base(user_admin); + $base->is_public(1); + + my $g_name = _create_group_ldap(); $base->grant_access( - type => 'group' + type => $type ,group => $g_name ); my $user = create_user(new_domain_name(),$$); @@ -76,6 +83,109 @@ sub test_access_by_group($vm) { remove_domain($base); } +sub _check_grant_access($id_base, $gm_name) { + my $sth = connector->dbh->prepare("SELECT * FROM group_access" + ." WHERE id_domain=?" + ); + $sth->execute($id_base); + my $n_found = 0; + while ( my $row = $sth->fetchrow_hashref ) { + $n_found++; + } + ok($n_found) or exit; +} + +sub test_access_by_group_sql($vm, $by_name=0) { + my $base = create_domain($vm->type); + $base->prepare_base(user_admin); + $base->is_public(1); + + my $g_name = new_domain_name(); + my $group = Ravada::Auth::Group::add_group(name => $g_name); + + my @group_data = ( id_group => $group->id ); + @group_data = ( group => $g_name ) if $by_name; + $base->grant_access( + type => 'group.local' + ,@group_data + ); + _check_grant_access($base->id, $g_name); + + my $g_name_ldap = _create_group_ldap(); + $base->grant_access( + type => 'group.ldap' + ,group => $g_name_ldap + ); + + my $user = create_user(new_domain_name(),$$); + is($user->is_admin, 0 ); + + my $list_bases = rvd_front->list_machines_user($user); + is(scalar(@$list_bases),0) or exit; + + my $user_sql = create_user(new_domain_name(), $$); + $list_bases = rvd_front->list_machines_user($user_sql); + is(scalar(@$list_bases),0) or exit; + + is($user_sql->is_member($g_name),0); + is($user_sql->is_member($group->id),0); + $user_sql->add_to_group($group->id); + + is($user_sql->is_member($g_name),1); + is($user_sql->is_member($group->id),1); + + $user_sql->_load_allowed(1); + is($user_sql->allowed_access($base->id),1); + + $list_bases = rvd_front->list_machines_user($user_sql); + is(scalar(@$list_bases),1); + Ravada::Request->create_domain( + id_owner => $user_sql->id + ,id_base => $list_bases->[0]->{id} + ,name => new_domain_name() + ); + wait_request(debug => 0); + + $list_bases = rvd_front->list_machines_user(user_admin); + is(scalar(@$list_bases),1); + + my $user_ldap0 = create_ldap_user(new_domain_name(), $$); + my $user_ldap = Ravada::Auth::SQL->new(name => $user_ldap0->get_value('cn')); + $list_bases = rvd_front->list_machines_user($user_ldap); + is(scalar(@$list_bases),0) or exit; + + $base->is_public(0); + $base->show_clones(0); + + $list_bases = rvd_front->list_machines_user($user_sql); + is(scalar(@$list_bases),0) or exit; + + $base->show_clones(1); + + $list_bases = rvd_front->list_machines_user($user_sql); + is(scalar(@$list_bases),1) or exit; + + + is($user_sql->is_member($group->id),1); + is($user_sql->allowed_access_group($base->id),1); + + $user_sql->remove_from_group($group->id); + $user_sql->_load_allowed(1); + + is($user_sql->is_member($g_name),0); + is($user_sql->is_member($group->id),0); + + is($user_sql->allowed_access_group($base->id),0) or die $base->id; + + remove_domain($base); + + $group->remove() if $group; + + my $group2 = Ravada::Auth::Group->new(name => $g_name); + is($group2->id,undef); +} + + sub test_access_by_agent($vm, $do_clones=0) { @@ -378,7 +488,12 @@ for my $vm_name (reverse vm_names()) { skip($msg,10) if !$vm; diag("Testing access restrictions in domain for $vm_name"); - test_access_by_group($vm); + test_access_by_group_ldap($vm); + test_access_by_group_ldap($vm,'group.ldap'); + test_access_by_group_sql($vm); + test_access_by_group_sql($vm,1); + + # test_access_by_group_sql_or_ldap($vm); test_access_by_agent($vm); diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm index 3b40207a8..60b12f38f 100644 --- a/t/lib/Test/Ravada.pm +++ b/t/lib/Test/Ravada.pm @@ -23,6 +23,7 @@ use feature qw(signatures); use Ravada; use Ravada::Auth::SQL; +use Ravada::Auth::Group; use Ravada::Domain::Void; use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); @@ -41,6 +42,7 @@ create_domain flush_rules_node flush_rules vm_names + user remote_config remote_config_nodes @@ -84,6 +86,7 @@ create_domain mojo_request_url mojo_request_url_post + create_group remove_old_user remove_old_users remove_old_users_ldap @@ -123,6 +126,7 @@ our $CONT = 0; our $CONT_POOL= 0; our $CONT_VOL= 0; our $USER_ADMIN; +our $USER; our @USERS_LDAP; our $CHAIN = 'RAVADA'; @@ -192,6 +196,30 @@ sub config_host_devices($type, $die=1) { return $config->{$type}; } +sub user { + + return $USER if $USER; + + my $login; + my $name = new_domain_name()."-$$"; + my $pass = "$$ $$"; + eval { + $login = Ravada::Auth::SQL->new(name => $name, password => $pass ); + }; + if ($@ && $@ =~ /Login failed/ ) { + $login = Ravada::Auth::SQL->new(name => $name); + $login->remove() if $login->id; + $login = undef; + } elsif ($@) { + die $@; + } + $USER = $login if $login && $login->id; + $USER = create_user($name, $pass, 0) + if !$USER; + + return $USER; +} + sub user_admin { return $USER_ADMIN if $USER_ADMIN; @@ -1203,6 +1231,14 @@ sub create_user($name=new_domain_name(), $pass=$$, $is_admin=0) { return $user; } +sub create_group($name = new_domain_name()) { + my $group = Ravada::Auth::Group->new(name => $name); + return $group if $group && $group->id; + + $group = Ravada::Auth::Group::add_group(name => $name); + return $group; +} + sub create_ldap_user($name, $password, $keep=0) { my $ldap = Ravada::Auth::LDAP::_init_ldap_admin(); diff --git a/t/mojo/10_login.t b/t/mojo/10_login.t index a7d555b74..d81a7d4e8 100644 --- a/t/mojo/10_login.t +++ b/t/mojo/10_login.t @@ -496,7 +496,7 @@ sub _check_html_lint($url, $content, $option = {}) { for my $error ( $lint->errors() ) { next if $error->errtext =~ /Entity .*is unknown/; next if $option->{internal} && $error->errtext =~ /(body|head|html|title).*required/; - if ( $error->errtext =~ /Unknown element <(footer|header|nav|ldap-groups)/ + if ( $error->errtext =~ /Unknown element <(footer|header|nav|ldap-groups|local-groups)/ || $error->errtext =~ /Entity && is unknown/ || $error->errtext =~ /should be written as/ || $error->errtext =~ /Unknown attribute.*%/ diff --git a/t/mojo/15_list_bases.t b/t/mojo/15_list_bases.t index 7bfca48e7..da1ed3539 100644 --- a/t/mojo/15_list_bases.t +++ b/t/mojo/15_list_bases.t @@ -1,7 +1,7 @@ use warnings; use strict; -use Carp qw(confess); +use Carp qw( croak confess); use Data::Dumper; use HTML::Lint; use Test::More; @@ -55,11 +55,109 @@ sub _create_base($vm_name) { return $base; } -sub test_list_machines_user($vm_name) { +sub test_list_fail($base) { + $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 croak "Expecting no ".$base->name." in listing"; + + $t->get_ok("/machine/clone/".$base->id.".html") + ->status_is(403); + +} + +sub _list_machines_user($base) { + $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 croak "Expecting ".$base->name." in listing"; + + return $base_f; +} + + +sub test_list_match($base, $do_clone=1) { + + my $base_f = _list_machines_user($base); + return if !$do_clone; + + $t->get_ok("/machine/clone/".$base->id.".html") + ->status_is(200); + + wait_request(); + + $base_f = _list_machines_user($base); + + my $id_clone = $base_f->{list_clones}->[0]->{id}; + ok($id_clone) or return; + + $t->get_ok("/machine/view/".$id_clone.".html") + ->status_is(200); + + return $id_clone; +} + +sub test_list_machines_group($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(); + my $group = create_group(); + $base->grant_access( group => $group->name, type => 'group.local'); + + mojo_login($t, $user->name,$$); + + test_list_fail($base); + + $user->add_to_group($group->name); + + my $id_clone = test_list_match($base); + + $user->remove_from_group($group->name); + $base->_data('show_clones' => 0); + + test_list_fail($base); + + $t->get_ok("/machine/clone/".$base->id.".html") + ->status_is(403); + + $t->get_ok("/machine/view/".$id_clone.".html") + ->status_is(403); + + diag("Check list machines match with show clones=1"); + + $base->_data('show_clones' => 1); + is ($user->allowed_access($base->id),0) or exit; + is ($user->allowed_access_group($base->id),0) or exit; + + test_list_fail($base); + + # access previous clone + $t->get_ok("/machine/view/".$id_clone.".html") + ->status_is(403); + + # but will not be able to clone + $t->get_ok("/machine/clone/".$base->id.".html") + ->status_is(403); + + +} + +sub test_list_machines_user($vm_name) { + mojo_login($t, $USERNAME, $PASSWORD); + my $base = _create_base($vm_name); + $base->is_public(1); + $t->ua->get($URL_LOGOUT); my $user = create_user(); @@ -150,6 +248,7 @@ for my $vm_name (reverse @{rvd_front->list_vm_types} ) { diag("Testing new machine in $vm_name"); + test_list_machines_group($vm_name); test_list_machines_user($vm_name); } remove_old_domains_req(0); # 0=do not wait for them diff --git a/t/mojo/60_upload.t b/t/mojo/60_upload.t index 22ec204fc..1877a7bdd 100644 --- a/t/mojo/60_upload.t +++ b/t/mojo/60_upload.t @@ -197,6 +197,72 @@ sub test_upload_no_admin($t) { } +sub _upload_group_members($group_name, $users, $mojo, $strict=0) { + if ($mojo==1) { + $t->post_ok("/group/upload_members.json" => + form => { + users => { content => $users, filename => 'users.txt' + ,'Content-Type' => 'text/csv' + } + ,group => $group_name + ,strict=> $strict + }, + )->status_is(200); + die $t->tx->res->body if $t->tx->res->code != 200; + } elsif($mojo==2) { + $t->post_ok("/admin/group/local/".$group_name => + form => { + members => { content => $users, filename => 'users.txt' + ,'Content-Type' => 'text/csv' + } + ,group => $group_name + ,strict=> $strict + }, + )->status_is(200); + die $t->tx->res->body if $t->tx->res->code != 200; + + } else { + rvd_front->upload_group_members($group_name, $users, $strict); + } +} + + +sub test_upload_group($mojo=0) { + my ($user1) = ( new_domain_name(), $$.1); + my ($user2) = ( new_domain_name(), $$.2); + _clean($user1, $user2); + + my $users = $user1."\n".$user2."\n" ; + + my $group_name = new_domain_name(); + + for ( 1 .. 2 ) { + _upload_group_members($group_name, $users, $mojo); + my $group = Ravada::Auth::Group->new( name => $group_name ); + + my %members = map { $_ => 1 } $group->members; + is(scalar(keys %members),2); + ok($members{$user1}); + ok($members{$user2}); + } + + _upload_group_members($group_name, $user1, $mojo); + + my $group = Ravada::Auth::Group->new( name => $group_name ); + my %members = map { $_ => 1 } $group->members; + is(scalar(keys %members),2); + ok($members{$user1}); + ok($members{$user2}); + + _upload_group_members($group_name, $user2, $mojo, 1); + %members = map { $_ => 1 } $group->members; + is(scalar(keys %members),1,"strict update mojo=$mojo failed"); + ok(!$members{$user1}); + ok($members{$user2}); + +} + + ################################################################################ $ENV{MOJO_MODE} = 'development'; @@ -212,6 +278,11 @@ test_upload_no_admin($t); _login($t); +test_upload_group(); +test_upload_group(1); # mojo +test_upload_group(2); # mojo post + + for my $type ('ldap','sso') { test_upload_users_nopassword( $type ); test_upload_users_nopassword( $type, 1 ); diff --git a/t/mojo/70_groups.t b/t/mojo/70_groups.t new file mode 100644 index 000000000..1af47498c --- /dev/null +++ b/t/mojo/70_groups.t @@ -0,0 +1,308 @@ +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 ($USERNAME2, $PASSWORD2); +my $SCRIPT = path(__FILE__)->dirname->sibling('../script/rvd_front'); + +$ENV{MOJO_MODE} = 'devel'; + +sub _clean_group($name) { + my $ldap = Ravada::Auth::LDAP::_init_ldap_admin(); + for my $group (Ravada::Auth::LDAP::search_group(name => $name)) { + diag("removing old LDAP group ".$group->dn); + my $mesg=$ldap->delete($group); + warn "ERROR: removing ".$group->dn." ".$mesg->code." : ".$mesg->error + if $mesg->code; + } + my $group = Ravada::Auth::Group->new(name => $name); + if ($group && $group->id) { + diag("removing old group ".$name); + $group->remove; + } + my $sth = connector->dbh->prepare("DELETE FROM group_access WHERE name=?"); + $sth->execute($name); +} + +sub test_group_created($type, $name) { + if ($type eq 'ldap') { + my $group = Ravada::Auth::LDAP::search_group(name => $name); + ok($group,"Expecting group $name in LDAP") or exit; + } elsif ($type eq 'local') { + my $group = Ravada::Auth::Group->new(name => $name); + ok($group->id,"Expecting group $name in SQL") or exit; + } else { + die "Unknown type '$type'"; + } +} + +sub test_list_users($type, $user_name) { + + $t->get_ok("/user/$type/list")->status_is(200); + my $result = decode_json($t->tx->res->body); + my ($found) = grep { $_->{name} eq $user_name} @{$result->{entries}}; + + my ($filter) = $user_name =~ /(.)/; + + $t->get_ok("/user/$type/list")->status_is(200); + $result = decode_json($t->tx->res->body); + ($found) = grep { $_->{name} eq $user_name} @{$result->{entries}}; +} + +################################################################### +sub test_group($type) { + my $group_name = new_domain_name(); + _clean_group($group_name); + $t->post_ok("/group/new",json => { type => $type , group_name => $group_name }) + ->status_is(200); + + test_group_created($type, $group_name); + + my $user_name = new_domain_name(); + my $id_group; + my @args; + my $url_list_members = "/group/$type/list_members/$group_name"; + my $url_admin_group = "/admin/group/$type/$group_name"; + if ($type eq 'ldap') { + create_ldap_user($user_name,$$); + push @args,(name => $user_name); + push @args,(group => $group_name); + + } else { + my $group = Ravada::Auth::Group->new(name => $group_name); + my $id_group = $group->id; + $url_list_members = "/group/$type/list_members/$id_group"; + $url_admin_group = "/admin/group/$type/$id_group"; + my $login; + eval { $login = Ravada::Auth::SQL->new(name => $user_name ) }; + $login->remove if $login && $login->id; + my $user = create_user($user_name); + push @args,( id_user => $user->id ); + push @args,( id_group => $group->id); + } + + test_list_users($type,$user_name); + + $t->post_ok("/group/$type/add_member", json => {@args})->status_is(200); + my $result = decode_json($t->tx->res->body); + is($result->{error},''); + + $t->get_ok($url_list_members)->status_is(200); + my $members = decode_json($t->tx->res->body); + my ($found) = grep {$_->{name} eq $user_name } @$members; + ok($found) or warn "Expecting $user_name in group $group_name ".Dumper($members); + die if !$found; + + my $id_domain = test_access($type, $group_name, $user_name); + + if ($type eq 'ldap') { + my $entry = Ravada::Auth::LDAP::search_user(name => $user_name); + my $dn = $entry->dn; + $url_list_members = "/group/$type/list_members/$group_name"; + $url_admin_group = "/admin/group/$type/$group_name"; + } + $t->post_ok("/group/$type/remove_member", json => {@args})->status_is(200); + die $t->tx->res->body if $t->tx->res->code != 200; + my $result2 = decode_json($t->tx->res->body); + is($result2->{error},''); + + $t->get_ok($url_list_members)->status_is(200); + my $members2 = decode_json($t->tx->res->body); + is_deeply($members2,[]) or exit; + + $t->get_ok($url_admin_group)->status_is(200); + die $t->tx->res->body if $t->tx->res->code != 200; + + test_list_groups($type, $group_name); + + test_group_removed($type, $group_name, $user_name, $id_domain); + + mojo_login($t, $USERNAME2, $PASSWORD2); + $t->get_ok($url_admin_group)->status_is(403); + + mojo_login($t, $USERNAME, $PASSWORD); + +} + +sub test_list_groups($type, $group_name) { + $t->get_ok("/group/$type/list")->status_is(200); + return if $t->tx->res->code != 200; + + my $list = decode_json($t->tx->res->body); + return if ref($list) ne 'ARRAY'; + + ok(grep({$_ eq $group_name } @$list), "Missing $type $group_name in ".Dumper($list)); + + $t->get_ok("/group_${type}_list")->status_is(200); + return if $t->tx->res->code != 200; + + my $list_book = decode_json($t->tx->res->body); + is_deeply($list_book, $list); + + my ($first) = $group_name =~ /^(.)/; + $t->get_ok("/group/$type/list/$first")->status_is(200); + $list = decode_json($t->tx->res->body); + return if ref($list) ne 'ARRAY'; + + ok(grep({$_ eq $group_name } @$list), "Missing $type $group_name in ".Dumper($list)); + +} + +sub test_add_access($type,$group_name, $user_name, $id_domain) { + my $url_add_access = "/machine/add_access_group/$type/$id_domain"; + my $id_group = ''; + $t->post_ok($url_add_access, json => { group => $ group_name }) + ->status_is(200); + my $result = decode_json($t->tx->res->body); + is($result->{error},''); + + my $sth; + if ($type eq 'ldap') { + $sth = connector->dbh->prepare( "SELECT * FROM group_access WHERE name=?"); + $sth->execute($group_name); + } else { + my $group = Ravada::Auth::Group->new(name => $group_name); + $sth = connector->dbh->prepare( "SELECT * FROM group_access WHERE id_group=?"); + $id_group = $group->id; + $sth->execute($id_group); + } + my ($found) = $sth->fetchrow_hashref(); + ok($found,"Expecting group access by name=$group_name") or exit; + is($found->{type}, $type); + + my $user = Ravada::Auth::SQL->new(name => $user_name); + is($user->allowed_access($id_domain),1); + my $list2 = rvd_front->list_machines_user($user); + + my ($found_machine) = grep { $_->{id} eq $id_domain } @$list2; + ok($found_machine,"Expecting $id_domain") or die Dumper([ map {$_->{id} } @$list2]); + + $t->get_ok("/machine/list_access_groups/$type/$id_domain")->status_is(200); + + my $list_groups = decode_json($t->tx->res->body); + is($result->{error},''); + + my ($found_groups) = grep ( { $_ eq $group_name } @$list_groups); + is($found_groups,$group_name) or die Dumper($list_groups) ; + +} + +sub test_remove_access($type, $group_name, $user_name, $id_domain) { + my $url = "/machine/remove_access_group/$type/$id_domain"; + $t->post_ok($url, json => { group => $group_name }) + ->status_is(200); + $t->get_ok("/machine/list_access_groups/$type/$id_domain")->status_is(200); + + my $list_groups = decode_json($t->tx->res->body); + + my ($found_groups) = grep ( { $_ eq $group_name } @$list_groups); + is($found_groups, undef) or die Dumper($list_groups); + +} + +sub _search_id_domain { + my $list = rvd_front->list_machines_user(user_admin); + my ($domain) = grep { $_->{is_public} } @$list; + if (!$domain) { + ($domain) = $list->[0]; + } + return $domain->{id}; +} + +sub test_access($type, $group_name, $user_name) { + my $id_domain = _search_id_domain(); + test_add_access($type, $group_name, $user_name, $id_domain); + test_remove_access($type, $group_name, $user_name, $id_domain); + return $id_domain; +} + +sub test_group_removed($type, $group_name, $user_name, $id_domain) { + + my $group0 = Ravada::Auth::Group->new(name => $group_name); + my $id_group = $group0->id; + + my $user = Ravada::Auth::SQL->new( name => $user_name); + + $t->post_ok("/group/$type/add_member", json => {id_user => $user->id, id_group => $id_group,group => $group_name })->status_is(200); + + $t->post_ok("/machine/add_access_group/$type/$id_domain",json => { group => $group_name}) + ->status_is(200); + + if ($type eq 'ldap') { + $t->get_ok("/group/$type/remove/$group_name")->status_is(200); + } else { + $t->get_ok("/group/$type/remove/$id_group")->status_is(200); + } + + die $t->tx->res->body if $t->tx->res->code != 200; + + if ($type eq 'ldap') { + my $group = Ravada::Auth::LDAP::search_group(name => $group_name); + ok(!$group,"Expecting no group $group_name in LDAP") or exit; + } elsif ($type eq 'local') { + my $group = Ravada::Auth::Group->new(name => $group_name); + ok(!$group->id,"Expecting no group $group_name in SQL") or exit; + + my $sth = connector->dbh->prepare("SELECT count(*) FROM users_group " + ." WHERE id_group=?" + ); + $sth->execute($id_group); + my ($found) = $sth->fetchrow; + is($found,0); + + } else { + die "Unknown type '$type'"; + } + + my $sth2 = connector->dbh->prepare("SELECT * FROM group_access " + ." WHERE name = ?" + ); + $sth2->execute($group_name); + my ($found) = $sth2->fetchrow_hashref; + is($found,undef); + + if ($type eq'ldap') { + $t->get_ok("/admin/group/$type/$group_name")->status_is(404); + } else { + $t->get_ok("/admin/group/$type/$id_group")->status_is(404); + } + +} + +################################################################### + +init('/etc/ravada.conf',0); +$Test::Ravada::BACKGROUND=1; +($USERNAME, $PASSWORD) = ( user_admin->name, "$$ $$"); +($USERNAME2, $PASSWORD2) = ( user->name, "$$ $$"); + +$t = Test::Mojo->new($SCRIPT); +$t->ua->inactivity_timeout(900); +$t->ua->connect_timeout(60); + + +mojo_login($t, $USERNAME, $PASSWORD); + +test_group('ldap'); +test_group('local'); + +done_testing(); diff --git a/t/vm/r30_reserve.t b/t/vm/r30_reserve.t index 647d69e2c..aeaf4c28f 100644 --- a/t/vm/r30_reserve.t +++ b/t/vm/r30_reserve.t @@ -4,6 +4,7 @@ use strict; use Carp qw(confess); use Data::Dumper; use DateTime; +use Hash::Util qw(lock_hash); use Test::More; use YAML qw(DumpFile); @@ -16,7 +17,8 @@ use feature qw(signatures); use lib 't/lib'; use Test::Ravada; -my $GROUP = 'test_bookings'; +my $GROUP = 'test_bookings_ldap'; +my $GROUP_LOCAL; my ($USER_YES_NAME_1, $USER_YES_NAME_2, $USER_NO_NAME) = ( 'mcnulty','bunk','stringer'); my ($USER_2_NAME,$USER_3_NAME)=('bubbles','walon'); @@ -26,6 +28,18 @@ use_ok('Ravada::Booking'); ################################################################### my ($USER_YES_1, $USER_YES_2, $USER_NO, $USER_2, $USER_3); +my ($USER_LOCAL_YES_1, $USER_LOCAL_YES_2, $USER_LOCAL_NO, $USER_LOCAL_2, $USER_LOCAL_3); + +sub _init_local() { + $GROUP_LOCAL = create_group() if !$GROUP_LOCAL; + + for my $ref ( \($USER_LOCAL_YES_1, $USER_LOCAL_YES_2, $USER_LOCAL_NO, $USER_LOCAL_2, $USER_LOCAL_3)) { + my $user = create_user(); + $$ref=$user; + } + $USER_LOCAL_YES_1->add_to_group($GROUP_LOCAL); + $USER_LOCAL_YES_2->add_to_group($GROUP_LOCAL); +} sub _init_ldap(){ @@ -134,39 +148,46 @@ sub _wait_end_of_hour($seconds=0) { my $now = DateTime->from_epoch( epoch => time() , time_zone => $TZ ); last if $now->minute <59 && ( $now->minute>0 || $now->second>$seconds); - diag("Waiting for end of hour to run booking tests " + diag("Waiting for hour:01 to run booking tests " .$now->hour.":".$now->minute.".".$now->second); sleep 1; } } -sub test_booking_oneday_dow($vm) { - return test_booking_oneday($vm,1); +sub test_booking_oneday_dow($vm, $mode) { + return test_booking_oneday($vm, $mode, 1); } -sub test_booking_oneday_date_end($vm) { - return test_booking_oneday($vm,0,1); +sub test_booking_oneday_date_end($vm, $mode) { + return test_booking_oneday($vm, $mode, 0,1); } -sub test_booking_oneday_date_end_dow($vm) { - return test_booking_oneday($vm,1,1); +sub test_booking_oneday_date_end_dow($vm, $mode) { + return test_booking_oneday($vm,$mode, 1,1); } -sub test_booking_oneday($vm, $dow=0, $date_end=0) { +sub test_booking_oneday($vm, $mode, $dow=0, $date_end=0) { my $base = create_domain($vm); $base->prepare_base(user_admin); $base->is_public(1); + confess if !ref($mode) || ref($mode) ne 'HASH'; + + for my $key (keys %$mode) { + next if $key =~ /^(local|ldap)$/; + die "Mode incorrect. It should be ldap,local or both ".Dumper($mode); + } my $today = DateTime->from_epoch( epoch => time(), time_zone => $TZ); my @args; push @args, ( day_of_week => $today->day_of_week) if $dow; push @args, ( date_end => $today->ymd) if $date_end; + push @args , ( ldap_groups => $GROUP ) if $mode->{'ldap'}; + push @args , ( local_groups => $GROUP_LOCAL->id ) if $mode->{'local'}; my $booking = Ravada::Booking->new( bases => $base->id - , ldap_groups => $GROUP , users => [$USER_2_NAME , $USER_3->id] , date_start => $today->ymd , time_start => "08:00" @@ -260,10 +281,15 @@ sub test_booking($vm, $clone0_no1, $clone0_no2, $clone0_as, $base0) { $USER_2->remove(); + my @users_yes; + @users_yes = ($USER_2_NAME , $USER_3->id); + push @users_yes,( $USER_LOCAL_2->name, $USER_LOCAL_3->name); + my $booking = Ravada::Booking->new( bases => $base->id , ldap_groups => $GROUP - , users => [$USER_2_NAME , $USER_3->id] + , local_groups => $GROUP_LOCAL->id + , users => \@users_yes , date_start => $date_start , date_end => $date_end , time_start => $time_start @@ -287,17 +313,11 @@ sub test_booking($vm, $clone0_no1, $clone0_no2, $clone0_as, $base0) { my @groups = $entry->ldap_groups; is($groups[0], $GROUP); my @users = $entry->users(); - is(scalar(@users),2,Dumper(\@users)); + is(scalar(@users),scalar(@users_yes),Dumper(\@users)); }; - is(Ravada::Booking::user_allowed($USER_YES_1, $base->id),1); - is(Ravada::Booking::user_allowed($USER_YES_1->id, $base->id),1); - is(Ravada::Booking::user_allowed($USER_YES_1->name, $base->id),1); - is(Ravada::Booking::user_allowed($USER_2_NAME, $base->id),1, $USER_2_NAME) or exit; - is(Ravada::Booking::user_allowed($USER_3, $base->id),1); - is(Ravada::Booking::user_allowed($USER_NO->name, $base->id ),0); - is(Ravada::Booking::user_allowed($USER_NO->id, $base->id ),0); - is(Ravada::Booking::user_allowed($USER_NO, $base->id ),0) - or die Dumper(''.localtime(time),\@entries); + + _test_user_allowed_ldap($base); + _test_user_allowed_local($base); eval { $clone_no->start(user => $USER_NO) }; like($@,qr/Resource .*booked/i ); @@ -321,6 +341,27 @@ sub test_booking($vm, $clone0_no1, $clone0_no2, $clone0_as, $base0) { } +sub _test_user_allowed_ldap($base) { + is(Ravada::Booking::user_allowed($USER_YES_1, $base->id),1); + is(Ravada::Booking::user_allowed($USER_YES_1->id, $base->id),1); + is(Ravada::Booking::user_allowed($USER_YES_1->name, $base->id),1); + is(Ravada::Booking::user_allowed($USER_2_NAME, $base->id),1, $USER_2_NAME) or exit; + is(Ravada::Booking::user_allowed($USER_3, $base->id),1); + is(Ravada::Booking::user_allowed($USER_NO->name, $base->id ),0); + is(Ravada::Booking::user_allowed($USER_NO->id, $base->id ),0); + is(Ravada::Booking::user_allowed($USER_NO, $base->id ),0) +} + +sub _test_user_allowed_local($base) { + is(Ravada::Booking::user_allowed($USER_LOCAL_YES_1, $base->id),1); + is(Ravada::Booking::user_allowed($USER_LOCAL_YES_1->id, $base->id),1); + is(Ravada::Booking::user_allowed($USER_LOCAL_YES_1->name, $base->id),1); + is(Ravada::Booking::user_allowed($USER_LOCAL_2->name, $base->id),1, $USER_LOCAL_2->name) or exit; + is(Ravada::Booking::user_allowed($USER_LOCAL_3, $base->id),1); + is(Ravada::Booking::user_allowed($USER_LOCAL_NO->name, $base->id ),0); + is(Ravada::Booking::user_allowed($USER_LOCAL_NO->id, $base->id ),0); + is(Ravada::Booking::user_allowed($USER_LOCAL_NO, $base->id ),0) +} sub test_bookings_week_2days($vm) { my $base = create_domain($vm); @@ -623,6 +664,7 @@ sub test_change_entry($vm, $booking) { is($new_entry->_data('time_start'), $new_time); test_change_groups($entry); + test_change_local_groups($entry); test_change_users($entry); test_change_bases($vm,$entry); } @@ -642,6 +684,26 @@ sub test_change_groups($entry) { is_deeply( \@new_groups ,\@groups2) or die Dumper(\@new_groups,\@groups2); } +sub test_change_local_groups($entry) { + my @groups = $entry->local_groups(); + my $new_group_1 = create_group(); + my @groups2 = sort (@groups, $new_group_1->name); + + $entry->change( local_groups => \@groups2 ); + my @new_groups = sort $entry->local_groups; + is_deeply( \@new_groups ,\@groups2) or die Dumper(\@new_groups,\@groups2); + + #clear groups + my $new_group_2 = create_group(); + @groups2 = $new_group_2->id; + $entry->change( local_groups => \@groups2 ); + @new_groups = sort $entry->local_groups; + + my @groups3 = ($new_group_2->name); + is_deeply( \@new_groups ,\@groups3) or die Dumper(\@new_groups,\@groups3); +} + + sub test_change_users($entry) { test_change_users_with_name($entry); test_change_users_with_id($entry); @@ -1234,7 +1296,7 @@ sub test_config { }; like($@, qr/LDAP required/i); - is(rvd_back->setting('/backend/bookings'),0); + is(rvd_back->setting('/backend/bookings'),1); } ################################################################### @@ -1247,6 +1309,7 @@ $TZ = DateTime::TimeZone->new(name => rvd_front->setting('/backend/time_zone')); delete $Ravada::CONFIG->{ldap}->{ravada_posix_group}; _init_ldap(); +_init_local(); rvd_back->setting('/backend/bookings', 1); @@ -1269,10 +1332,18 @@ for my $vm_name ( vm_names()) { test_conflict($vm); test_booking_datetime($vm); - test_booking_oneday($vm); - test_booking_oneday_dow($vm); - test_booking_oneday_date_end($vm); - test_booking_oneday_date_end_dow($vm); + for my $ldap (0, 1) { + for my $local ( 0, 1) { + my $mode = {}; + $mode->{'ldap'} = $ldap; + $mode->{'local'} = $ldap; + lock_hash(%$mode); + test_booking_oneday($vm , $mode); + test_booking_oneday_dow($vm, $mode); + test_booking_oneday_date_end($vm, $mode); + test_booking_oneday_date_end_dow($vm, $mode); + } + } _check_no_bookings(); diff --git a/templates/booking/formEvent.component.html.ep b/templates/booking/formEvent.component.html.ep index c35bcd53f..a7a951b1d 100644 --- a/templates/booking/formEvent.component.html.ep +++ b/templates/booking/formEvent.component.html.ep @@ -89,14 +89,25 @@ -
- +
+ + >
+
+ + +
+
+
+
+
@@ -126,5 +138,4 @@
- {{ form_booking.ldap_groups }} diff --git a/templates/booking/localGroup.component.html.ep b/templates/booking/localGroup.component.html.ep new file mode 100644 index 000000000..a88ec0b82 --- /dev/null +++ b/templates/booking/localGroup.component.html.ep @@ -0,0 +1,32 @@ +
+ + + +
+ <%=l 'No Results Found' %> +
+ +
+
+
+ + {{group}} + + + + +
+ +
+
+ +
diff --git a/templates/bootstrap/navigation.html.ep b/templates/bootstrap/navigation.html.ep index 233e7a5a5..5c8fab373 100644 --- a/templates/bootstrap/navigation.html.ep +++ b/templates/bootstrap/navigation.html.ep @@ -24,7 +24,7 @@ navbar-dark bg-dark fixed-top navbar-expand-lg navbar-inverse"> <%=l 'Available Machines' %> % } -% if ($bookings && defined $_user && $FEATURE->{ldap} ) { +% if ($bookings && defined $_user) { % } -% if ( $_user->is_admin && $user->is_external && $user->external_auth eq 'ldap' && $user->ldap_entry ) { +% if ( $_user->is_admin && $user->is_external && $user->external_auth eq 'ldap' && $FEATURE->{ldap} && $user->ldap_entry ) { @@ -63,7 +69,7 @@ % } -% if ($user->id && $_user->is_admin && $user->is_external && $user->external_auth eq 'ldap' && !$user->ldap_entry ) { +% if ($user->id && $_user->is_admin && $user->is_external && $user->external_auth eq 'ldap' && $FEATURE->{ldap} && !$user->ldap_entry ) {
<%=l 'Error: the LDAP entry for this user has been removed.' %>
@@ -84,7 +90,7 @@ %= include '/main/manage_user_password'
% } -% if ( $_user->is_admin && $user->id && $user->is_external && $user->external_auth eq 'ldap' && $user->ldap_entry ) { +% if ( $_user->is_admin && $user->id && $user->is_external && $user->external_auth eq 'ldap' && $FEATURE->{ldap} && $user->ldap_entry ) {
%= include '/main/manage_user_groups'
diff --git a/templates/main/upload_group_members.html.ep b/templates/main/upload_group_members.html.ep new file mode 100644 index 000000000..22cf2d77c --- /dev/null +++ b/templates/main/upload_group_members.html.ep @@ -0,0 +1,87 @@ + + +%= include '/bootstrap/header' + +
+ %= include '/bootstrap/navigation' +
+ + +
+
+%= include $footer +%= include '/bootstrap/scripts' + + + diff --git a/templates/ng-templates/new_group.html.ep b/templates/ng-templates/new_group.html.ep index de13e6b6c..875e951d0 100644 --- a/templates/ng-templates/new_group.html.ep +++ b/templates/ng-templates/new_group.html.ep @@ -1,27 +1,49 @@
-
+ {{item}} +
+
- <%= $groupname %> -
+ +
+
+
+ +
-
-% for (sort ('nsMemberOf','posixGroup','groupOfUniqueNames') ) { - {$_} or '') %> name="object_class" value="<%= $_ %>"> <%= $_ %>
-% } + +
+
+ + +
-
-
+
+
<%=l 'Oops!' %> <%=l 'Group name is required' %>.
-
+
<%=l 'Oops!' %> <%=l 'Group name can not exceed 80 characters' %>.
- + % if (scalar @$error) { % for my $i (@$error) {
@@ -30,7 +52,7 @@ % } % } -
- <%=l 'Group name' %> <%= $groupname %> <%=l 'added' %>. +
+ <%=l 'Group' %> {{group_name}} <%=l 'added' %>.