Skip to content

Commit

Permalink
Merge branch 'fix/1920_CSP' of github.com:UPC/ravada into fix/1920_CSP
Browse files Browse the repository at this point in the history
  • Loading branch information
frankiejol committed Oct 26, 2023
2 parents f0958c1 + 14a1e15 commit e216f47
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 32 deletions.
53 changes: 53 additions & 0 deletions lib/Ravada.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
19 changes: 17 additions & 2 deletions lib/Ravada/Front.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
25 changes: 24 additions & 1 deletion public/js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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_key<keys.length ; n_key++) {
var field=keys[n_key];
if ( field != 'all' && field != 'id' && field != 'value'
&& $scope.settings.frontend.content_security_policy[field].value) {
found++;
}
}
$scope.csp_locked = found>0;
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();
Expand All @@ -1472,17 +1492,20 @@ 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() {
$scope.formSettings.$setPristine();
$http.post('/settings_global'
,JSON.stringify($scope.settings)
).then(function(response) {
$scope.set_csp_locked();
if (response.data.reload) {
window.location.reload();
}
Expand Down
22 changes: 12 additions & 10 deletions script/rvd_front
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
Expand All @@ -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 .= ";";
Expand Down Expand Up @@ -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') {
Expand Down
85 changes: 85 additions & 0 deletions t/mojo/40_security_policy.t
Original file line number Diff line number Diff line change
@@ -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();
64 changes: 45 additions & 19 deletions templates/main/admin_settings.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
%= include 'bootstrap/navigation'
<div id="page-wrapper"
ng-controller="settings_global"
ng-init="init('<%= url_for('ws_subscribe')->to_abs %>')"
ng-init="init('<%= url_for('ws_subscribe')->to_abs %>'
,<%= ($csp_advanced or 0 ) %>)"
>
<div class="page-header">
<div class="card">
Expand Down Expand Up @@ -45,8 +46,7 @@
</div>
</div>
<div class="row">
<div class="col-md-1"></div>
<div class="col-md-2">
<div class="col-md-3" align="right">
<label for="maintenance"><%=l 'Maintenance' %></label>
</div>
<div class="col-md-6">
Expand All @@ -71,7 +71,7 @@

<div class="row" ng-show="settings.frontend.maintenance.value == 1">
<div class="col-md-1"></div>
<div class="col-md-2">
<div class="col-md-2" align="right">
<label for="maintenance_end"><%=l 'Maintenance End' %></label>
</div>
<div class="col-md-4">
Expand All @@ -95,6 +95,46 @@
% }
{{settings.frontend.content_security_policy}}

<div class="row">
<div class="col-md-1"></div>
<div class="col-md-2" align="right"><%=l 'Widget' %>
<a href="https://ravada.readthedocs.io/en/latest/docs/chatwoot.html"><i class="fa fa-info"></i></a>
</div>
<div class="col-md-6">
<input placeholder="/js/custom/widget.js"
type="text" size="40"
name="widget" ng-model="settings.frontend.widget.value"/>
</div>
</div>

<div class="row">
<div class="col-md-3" align="right">Content Security Policy</div>
<div class="col-md-4" ng-show="!csp_advanced" >
<input name="csp_all" type="text" ng-model="settings.frontend.content_security_policy.all.value"/><br/>
</div>
</div>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-5" ng-hide="csp_locked">
<input type="checkbox" ng-model="csp_advanced"
name="csp_advanced"/>
<label for="csp_advanced"><%=l 'Advanced CSP' %></label>
</div>
</div>
<div ng-show="csp_advanced">
% for my $item (sort keys %$csp) {
<div class="row">
<div class="col-md-1"></div>
<div class="col-md-2" align="right"><%= $item %></div>
<div class="col-md-4">
<input name="csp_<%= $item %>" type="text" ng-model="settings.frontend.content_security_policy['<%= $item %>'].value"/>
</div>
</div>
% }
</div>

%= include "/main/admin_settings_submit"

<div class="row">
<div class="col-md-1"></div>
<div class="col-md-6">
Expand Down Expand Up @@ -227,21 +267,7 @@
</div>
</div>

<hr>

<div class="row">
<div class="col-md-6">
<button ng-click="update_settings()"
ng-disabled="!formSettings.$valid || formSettings.$pristine">
<%=l 'Save' %>
</button>
<button ng-click="load_settings()"
ng-disabled="formSettings.$pristine">
<%=l 'Cancel' %>
</button>
</div>
</div>

%= include "/main/admin_settings_submit"

</form>
</div>
Expand Down
13 changes: 13 additions & 0 deletions templates/main/admin_settings_submit.html.ep
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="row">
<div class="col-md-1"></div>
<div class="col-md-6">
<button ng-click="update_settings()"
ng-disabled="!formSettings.$valid || formSettings.$pristine">
<%=l 'Save' %>
</button>
<button ng-click="load_settings()"
ng-disabled="formSettings.$pristine">
<%=l 'Cancel' %>
</button>
</div>
</div>

0 comments on commit e216f47

Please sign in to comment.