diff --git a/lib/Ravada.pm b/lib/Ravada.pm index 6c7305d6e..3b1287872 100644 --- a/lib/Ravada.pm +++ b/lib/Ravada.pm @@ -2543,6 +2543,59 @@ sub _sql_insert_defaults($self){ ,name => 'auto_view' ,value => $conf->{auto_view} } + ,{ id_parent => $id_frontend + ,name => "widget" + } + ,{ + id_parent => $id_frontend + ,name => 'content_security_policy' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "all" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "default-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "style-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "script-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "object-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "frame-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "font-src" + ,value => '' + } + + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "connect-src" + ,value => '' + } + ,{ + id_parent => "/frontend/content_security_policy" + ,name => "media-src" + ,value => '' + } ,{ id_parent => $id_backend ,name => 'start_limit' diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm index 6ea3f8ffe..945071d84 100644 --- a/lib/Ravada/Front.pm +++ b/lib/Ravada/Front.pm @@ -16,6 +16,7 @@ use Hash::Util qw(lock_hash); use IPC::Run3 qw(run3); use JSON::XS; use Moose; +use Storable qw(dclone); use Ravada; use Ravada::Auth::LDAP; use Ravada::Front::Domain; @@ -1481,6 +1482,19 @@ sub _settings_by_id($self) { return $orig_settings; } +sub _settings_by_parent($self,$parent) { + my $data = $self->_setting_data($parent); + my $sth = $self->_dbh->prepare("SELECT name,value FROM settings " + ." WHERE id_parent = ? "); + $sth->execute($data->{id}); + my $ret; + while (my ($name, $value) = $sth->fetchrow) { + $value = '' if !defined $value; + $ret->{$name} = $value; + } + return $ret; +} + =head2 feature Returns if a feature is available @@ -1521,9 +1535,10 @@ sub update_settings_global($self, $arg, $user, $reload, $orig_settings = $self-> confess Dumper([$field,$arg->{$field}]) if !ref($arg->{$field}); if ( scalar(keys %{$arg->{$field}})>2 ) { confess if !keys %{$arg->{$field}}; - $self->update_settings_global($arg->{$field}, $user, $reload, $orig_settings); + my $field2 = dclone($arg->{$field}); + $self->update_settings_global($field2, $user, $reload, $orig_settings); } - confess "Error: invalid field $field" if $field !~ /^\w+$/; + confess "Error: invalid field $field" if $field !~ /^\w[\w\-]+$/; my ( $value, $id ) = ($arg->{$field}->{value} , $arg->{$field}->{id} diff --git a/public/js/admin.js b/public/js/admin.js index aa43e9136..d7f84318b 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1453,7 +1453,27 @@ ravadaApp.directive("solShowMachine", swMach) function settings_global_ctrl($scope, $http) { $scope.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - $scope.init = function() { + $scope.csp_locked = false; + $scope.set_csp_locked=function() { + var keys = Object.keys($scope.settings.frontend.content_security_policy); + var found = 0; + for ( var n_key=0 ; n_key0; + if ($scope.csp_locked && !$scope.csp_advanced) { + $scope.csp_advanced = true; + } + }; + $scope.init = function(url, csp_advanced) { + $scope.csp_advanced=false; + if (csp_advanced) { + $scope.csp_advanced=true; + } $http.get('/settings_global.json').then(function(response) { $scope.settings = response.data; var now = new Date(); @@ -1472,10 +1492,12 @@ ravadaApp.directive("solShowMachine", swMach) $scope.settings.frontend.maintenance_end.value =new Date($scope.settings.frontend.maintenance_end.value); } + $scope.set_csp_locked(); }); }; $scope.load_settings = function() { $scope.init(); + $scope.set_csp_locked(); $scope.formSettings.$setPristine(); }; $scope.update_settings = function() { @@ -1483,6 +1505,7 @@ ravadaApp.directive("solShowMachine", swMach) $http.post('/settings_global' ,JSON.stringify($scope.settings) ).then(function(response) { + $scope.set_csp_locked(); if (response.data.reload) { window.location.reload(); } diff --git a/script/rvd_front b/script/rvd_front index 4fb3a6982..44f60cce3 100644 --- a/script/rvd_front +++ b/script/rvd_front @@ -149,8 +149,8 @@ sub _time() { sub _security_policy() { - my $config={}; - $config = $CONFIG_FRONT->{security_policy} if exists $CONFIG_FRONT->{security_policy}; + my $config=$RAVADA->_settings_by_parent("/frontend/content_security_policy"); + my $all = ($config->{all} or ''); my %src = ( "default-src" => "'self' sha256- sha384 http: https: data:" ,"style-src" => "'self' cdnjs.cloudflare.com stackpath.bootstrapcdn.com cdn.ckeditor.com cdn.jsdelivr.net use.fontawesome.com 'unsafe-inline'" @@ -165,16 +165,13 @@ sub _security_policy() { for my $field (sort keys %src) { $sec .= " " if $sec; $sec .= $field." ".$src{$field}; - if (ref($config)) { - if ( exists $config->{$field} ) { + $sec .= " $all" if $all; + if ( exists $config->{$field} ) { $sec .= " ".${config}->{$field}; - } - $field =~ s/-/_/g; - if ( exists $config->{$field} ) { + } + $field =~ s/-/_/g; + if ( exists $config->{$field} ) { $sec .= " ".$config->{$field}; - } - } else { - $sec .= " $config"; } $sec .= ";"; @@ -2842,6 +2839,11 @@ sub admin { return access_denied($c) unless $USER->is_admin; my $url = $c->req->url->to_abs->path; my $host = $c->req->url->to_abs->host; + my $csp = $RAVADA->_settings_by_parent("/frontend/content_security_policy"); + my $csp_advanced = 0; + $csp_advanced = grep $csp->{$_}, grep /-/,keys %$csp; + + $c->stash( csp => $csp , csp_advanced => $csp_advanced); $c->stash(url_login => "/login"); } if ($page eq 'storage') { diff --git a/t/mojo/40_security_policy.t b/t/mojo/40_security_policy.t new file mode 100644 index 000000000..4e4af063d --- /dev/null +++ b/t/mojo/40_security_policy.t @@ -0,0 +1,85 @@ +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 Storable qw(dclone); + +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) = (user_admin->name, "$$ $$"); +my $SCRIPT = path(__FILE__)->dirname->sibling('../script/rvd_front'); + +$ENV{MOJO_MODE} = 'devel'; +init('/etc/ravada.conf',0); +my $connector = rvd_back->connector; +like($connector->{driver} , qr/mysql/i) or BAIL_OUT; + +$Test::Ravada::BACKGROUND=1; + +$t = Test::Mojo->new($SCRIPT); +$t->ua->inactivity_timeout(900); +$t->ua->connect_timeout(60); + +mojo_login($t, $USERNAME, $PASSWORD); + +my $sth = rvd_front->_dbh->prepare("UPDATE settings set value='' WHERE id_parent=?"); + +$t->get_ok("/settings_global.json")->status_is(200); +my $body = $t->tx->res->body(); +my $settings = decode_json($body); + +$sth->execute($settings->{frontend}->{content_security_policy}->{id}); + +my $new = dclone($settings); +my $exp_default = "foodefault.example.com"; +my $exp_all = "fooall.example.com"; +$new->{frontend}->{content_security_policy}->{'default-src'}->{value} = $exp_default; +$new->{frontend}->{content_security_policy}->{'all'}->{value} = $exp_all; +delete $new->{backend}; + +my $reload=0; +rvd_front->update_settings_global($new,user_admin,$reload); + +$t->post_ok("/settings_global", json => $new ); + +$t->get_ok("/settings_global.json")->status_is(200); +$body = $t->tx->res->body(); +my $settings2 = decode_json($body); +is($settings2->{frontend}->{content_security_policy}->{'all'}->{value} , $exp_all) or exit; +is($settings2->{frontend}->{content_security_policy}->{'default-src'}->{value} , $exp_default) or exit; + +my $config_csp = rvd_front->_settings_by_parent("/frontend/content_security_policy"); +is($config_csp->{all}, $exp_all); +is($config_csp->{'default-src'}, $exp_default); + +my $header = $t->tx->res->headers->content_security_policy(); +my %csp; +for my $entry (split /;/,$header) { + my ($key,$value) = $entry =~ /\s*(.*?)\s+(.*)/; + $csp{$key}=$value; +} + +like($csp{'default-src'},qr/$exp_all/); +like($csp{'default-src'},qr/$exp_default/); + +$sth->execute($settings->{frontend}->{content_security_policy}->{id}); + +$new->{frontend}->{content_security_policy}->{'default-src'}->{value} = ''; +$new->{frontend}->{content_security_policy}->{'all'}->{value} = ''; +$t->post_ok("/settings_global", json => $new ); +done_testing(); diff --git a/templates/main/admin_settings.html.ep b/templates/main/admin_settings.html.ep index b1e03bcc0..f1e5f257a 100644 --- a/templates/main/admin_settings.html.ep +++ b/templates/main/admin_settings.html.ep @@ -6,7 +6,8 @@ %= include 'bootstrap/navigation'
-
-
+
@@ -71,7 +71,7 @@
-
+
@@ -95,6 +95,46 @@ % } {{settings.frontend.content_security_policy}} +
+
+
<%=l 'Widget' %> + +
+
+ +
+
+ +
+
Content Security Policy
+
+
+
+
+
+
+
+ + +
+
+
+ % for my $item (sort keys %$csp) { +
+
+
<%= $item %>
+
+ +
+
+ % } +
+ +%= include "/main/admin_settings_submit" +
@@ -227,21 +267,7 @@
-
- -
-
- - -
-
- +%= include "/main/admin_settings_submit"
diff --git a/templates/main/admin_settings_submit.html.ep b/templates/main/admin_settings_submit.html.ep new file mode 100644 index 000000000..faa325bc7 --- /dev/null +++ b/templates/main/admin_settings_submit.html.ep @@ -0,0 +1,13 @@ +
+
+
+ + +
+