From cd7753505d027ea249a810ac3ea1bceaaf26e8cf Mon Sep 17 00:00:00 2001 From: Timothy Legge Date: Thu, 24 Oct 2024 04:03:04 +0100 Subject: [PATCH 01/50] Show the authentication providers in a table --- lib/GADS.pm | 34 +++++++- lib/GADS/API.pm | 96 +++++++++++++++++++++ lib/GADS/Authentication.pm | 63 ++++++++++++++ lib/GADS/SAML.pm | 68 +++++++++++++-- lib/GADS/Schema/Result/Authentication.pm | 47 ++++++++++ lib/GADS/Schema/ResultSet/Authentication.pm | 13 ++- views/admin/admin_settings.tt | 16 ++++ views/authentication/providers.tt | 73 ++++++++++++++++ 8 files changed, 399 insertions(+), 11 deletions(-) create mode 100644 lib/GADS/Authentication.pm create mode 100644 views/authentication/providers.tt diff --git a/lib/GADS.pm b/lib/GADS.pm index 3ca41be8a..c54c8a989 100644 --- a/lib/GADS.pm +++ b/lib/GADS.pm @@ -26,6 +26,7 @@ use File::Temp qw/ tempfile /; use GADS::Alert; use GADS::Approval; use GADS::Audit; +use GADS::Authentication; use GADS::Layout; use GADS::Column; use GADS::Column::Autocur; @@ -477,18 +478,23 @@ get '/saml' => sub { post '/saml' => sub { + my $authentication = schema->resultset('Authentication')->find(session 'authentication_id') + or error "Error finding authentication provider"; + my $saml = GADS::SAML->new( + authentication => $authentication, request_id => session('request_id'), base_url => request->base, ); + my $callback = $saml->callback( saml_response => body_parameters->get('SAMLResponse'), + cacert => $authentication->cacert, ); - my $authentication = schema->resultset('Authentication')->find(session 'authentication_id') - or error "Error finding authentication provider"; + my $username = $callback->{nameid} + or error __"Missing nameid in SAML response"; - my $username = $callback->{nameid}; my $user = schema->resultset('User')->active->search({ username => $username })->next; if (!$user) @@ -497,6 +503,8 @@ post '/saml' => sub { return forwardHome({ danger => __x($msg, username => $username) }, 'login?password=1' ); } + # TIM FIXME add in the group attributes here + $user->update_attributes($callback->{attributes}); $user->update({ lastlogin => DateTime->now }); @@ -1504,6 +1512,26 @@ any ['get', 'post'] => '/user_export/?' => require_any_role [qw/useradmin supera }; }; +any ['get', 'post'] => '/authentication_providers/' => require_any_role [qw/useradmin superadmin/] => sub { + + my $auth = GADS::Authentication->new(schema => schema); + template 'authentication/providers' => { + values => { + site_id => "site_id", #$auth->site_id, + type => "type", #$auth->type, + name => "name", #$auth->name, + xml => "xm;", #$auth->xml, + saml2_firstname => "first", #$auth->saml2_firstname, + saml2_surname => "surname", #$auth->saml2_surname, + enabled => "enabled", #$auth->enabled, + error_messages => "error_message", #$auth->error_messages, + + }, + permissions => "permisission", #$auth->permissions, + page => 'system_settings', + }; +}; + any ['get', 'post'] => '/user_overview/' => require_any_role [qw/useradmin superadmin/] => sub { my $userso = GADS::Users->new(schema => schema); diff --git a/lib/GADS/API.pm b/lib/GADS/API.pm index cf089333c..db474d3b9 100644 --- a/lib/GADS/API.pm +++ b/lib/GADS/API.pm @@ -1328,6 +1328,102 @@ sub _error error __x $msg; } +any ['get', 'post'] => '/api/providers' => require_any_role [qw/useradmin superadmin/] => sub { + + # Allow parameters to be passed by URL query or in the body. Flatten into + # one parameters object + my $params = Hash::MultiValue->new(query_parameters->flatten, body_parameters->flatten); + + my $site = var 'site'; + if ($params->get('cols')) + { + # Get columns to be shown in the users table summary + my @cols = qw/site_id type name xml saml2_firstname saml2_surname enabled error_messages/; + #push @cols, 'title' if $site->register_show_title; + #push @cols, 'email'; + #push @cols, 'organisation' if $site->register_show_organisation; + #push @cols, 'department' if $site->register_show_department; + #push @cols, 'team' if $site->register_show_team; + #push @cols, 'freetext1' if $site->register_freetext1_name; + #push @cols, qw/created lastlogin/; + my @return = map { { name => $_, data => $_ } } @cols; + content_type 'application/json; charset=UTF-8'; + return encode_json \@return; + } + + my $start = $params->get('start') || 0; + my $length = $params->get('length') || 10; + + my $auth = GADS::Authentication->new(schema => schema)->authentication_summary_rs; + + my $total = $auth->count; + my $col_order = $params->get('order[0][column]'); + use Data::Dumper; + my $sort_by = defined $col_order && $params->get("columns[${col_order}][name]"); + my $dir = $params->get('order[0][dir]'); + my $search = $params->get('search[value]'); + + if (my $sort_field = $site->user_field_by_description($sort_by)) + { + $sort_by = $sort_field->{name} eq 'site_id' + ? 'me.site_id' + : $sort_field->{name} eq 'type' + ? 'me.type' + : $sort_field->{name} eq 'name' + ? 'me.name' + : $sort_field->{name} eq 'enabled' + ? 'me.enabled' + : $sort_field->{name} eq 'error_messages' + ? 'me.error_messages' + : 'me.id'; + } + elsif ($sort_by && $sort_by eq 'ID') + { + $sort_by = 'me.id'; + } + else { + $sort_by = 'me.name'; + } + + my @sr; + foreach my $s (split /\s+/, $search) + { + $s or next; + $s =~ s/\_/\\\_/g; # Escape special like char + push @sr, [ + 'me.id' => $s =~ /^[0-9]+$/ ? $s : undef, + # surname and firstname are case sensitive in database + 'me.site_id' => { -like => "%$s%" }, + 'me.type' => { -like => "%$s%" }, + 'me.name' => { -like => "%$s%" }, + 'me.saml2_firstname' => { -like => "%$s%" }, + 'me.saml2_surname' => { -like => "%$s%" }, + 'me.enabled' => { -like => "%$s%" }, + ]; + } + + $auth = $auth->search({ + -and => \@sr, + },{ + order_by => { $dir && $dir eq 'asc' ? -asc : -desc => $sort_by }, + }); + my $filtered_count = $auth->count; + my $auth_render = $auth->search({},{ + offset => $start, + rows => $length, + }); + + my $return = { + draw => $params->get('draw'), + recordsTotal => $total, + recordsFiltered => $filtered_count, + data => [map $_->for_data_table(site => $site), $auth_render->all], + }; + + content_type 'application/json; charset=UTF-8'; + return encode_json $return; +}; + any ['get', 'post'] => '/api/users' => require_any_role [qw/useradmin superadmin/] => sub { # Allow parameters to be passed by URL query or in the body. Flatten into # one parameters object diff --git a/lib/GADS/Authentication.pm b/lib/GADS/Authentication.pm new file mode 100644 index 000000000..422c20487 --- /dev/null +++ b/lib/GADS/Authentication.pm @@ -0,0 +1,63 @@ +=pod +GADS - Globally Accessible Data Store +Copyright (C) 2014 Ctrl O Ltd + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +=cut + +package GADS::Authentication; + +use GADS::Email; +use GADS::Util; +use Log::Report 'linkspace'; +use POSIX (); +use Scope::Guard qw(guard); + +use Moo; +use MooX::Types::MooseLike::Base qw(:all); + +has schema => ( + is => 'ro', + required => 1, +); + +has all => ( + is => 'lazy', + isa => ArrayRef, +); + +sub authentication_rs +{ my $self = shift; + $self->schema->resultset('Authentication')->providers; +} + +sub authentication_summary_rs +{ my $self = shift; + my $summary = $self->authentication_rs->search_rs({},{ + columns => [ + 'me.id', 'me.site_id', 'me.type', 'me.name', 'me.enabled', 'me.error_messages', + ], + order_by => 'name', + collapse => 1, + }); + return $summary; +} + +sub _build_all +{ my $self = shift; + my @authentication = $self->authentication_summary_rs->all; + \@authentication; +} + +1; diff --git a/lib/GADS/SAML.pm b/lib/GADS/SAML.pm index cff8efb39..d5d61d36e 100644 --- a/lib/GADS/SAML.pm +++ b/lib/GADS/SAML.pm @@ -4,6 +4,7 @@ use Log::Report 'linkspace'; use Moo; +use Net::SAML2 0.67; use Net::SAML2::Binding::POST; use Net::SAML2::Binding::Redirect; use Net::SAML2::IdP; @@ -15,6 +16,7 @@ use IO::Compress::RawDeflate qw/rawdeflate/; use URI; use URI::QueryParam; use URL::Encode qw/url_encode/; +use File::Temp qw/ tempfile /; has request_id => ( is => 'rw', @@ -46,22 +48,49 @@ sub _build_sso_xml sub callback { my ($self, %params) = @_; - my $post = Net::SAML2::Binding::POST->new; + my $cacert_fh; + + # Save CA cert locally if configured + if (my $cacert = $params{cacert}) + { + $cacert_fh = File::Temp->new; + print $cacert_fh $cacert; + $cacert_fh->close + } + + my $post = Net::SAML2::Binding::POST->new( + $cacert_fh ? (cacert => $cacert_fh->filename) : (), + ); + my $saml_response = $params{saml_response}; if (my $return = $post->handle_response($saml_response)) { + my $key_fh; + if (defined $params{sp_key}) { + $key_fh = File::Temp->new; + print $key_fh $params{sp_key}; + $key_fh->close; + } + my $assertion = Net::SAML2::Protocol::Assertion->new_from_xml( - xml => decode_base64($saml_response) + xml => decode_base64($saml_response), + $key_fh ? (key_file => $key_fh->filename) : (), + $cacert_fh ? (cacert => $cacert_fh->filename) : (), ); - error __x"Invalid SSO assertion received. Expected request ID {request_id}", - request_id => $self->request_id + + error __x"Invalid SSO assertion received. Expected request ID {request_id} Status: {status} SubStatus: {substatus}", + request_id => $self->request_id, + status => $assertion->response_status, + substatus => $assertion->response_substatus if !$assertion->valid($self->sso_xml, $self->request_id); return { nameid => $assertion->nameid, attributes => $assertion->attributes, } } + unlink $cacert_fh->filename; + }; has redirect => ( @@ -75,8 +104,20 @@ has authentication => ( sub initiate { my ($self, %params) = @_; + my $cacert_fh; + if (my $cacert = $self->authentication->cacert) + { + $cacert_fh = File::Temp->new; + print $cacert_fh $cacert; + $cacert_fh->close; + } + + error __"Missing Provider Metadata. Please upload to Authentication Settings" + if !$self->authentication->xml; + my $idp = Net::SAML2::IdP->new_from_xml( xml => $self->authentication->xml, + $cacert_fh ? (cacert => $cacert_fh->filename) : (), ); my $sso_url = $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'); @@ -84,15 +125,28 @@ sub initiate my $authnreq = Net::SAML2::Protocol::AuthnRequest->new( issuer => $self->sso_xml, destination => $sso_url, + nameid_format => $idp->format('emailAddress') || undef, + # assertion_url => "https://$www/app/saml", ); $self->request_id($authnreq->id); my $x = $authnreq->as_xml; + my $key_fh; + if (defined $self->authentication->sp_key and $self->authentication->sp_key ne ''){ + my $sp_key = $self->authentication->sp_key; + $key_fh = File::Temp->new; + print $key_fh $sp_key; + $key_fh->close; + } + + # FIXME: Requires a key in the database my $redirect = Net::SAML2::Binding::Redirect->new( - url => $sso_url, - param => 'SAMLRequest', - insecure => 1, + $key_fh ? (key => $key_fh->filename) : (), + url => $sso_url, + param => 'SAMLRequest', + $key_fh ? (insecure => 0) : (insecure => 1), + sig_hash => 'sha256', # Hard coded - may want allow as an option ); my $url = $redirect->get_redirect_uri($x); diff --git a/lib/GADS/Schema/Result/Authentication.pm b/lib/GADS/Schema/Result/Authentication.pm index eb16aa871..150acc666 100644 --- a/lib/GADS/Schema/Result/Authentication.pm +++ b/lib/GADS/Schema/Result/Authentication.pm @@ -24,10 +24,20 @@ __PACKAGE__->add_columns( { data_type => "text", is_nullable => 1 }, "xml", { data_type => "text", is_nullable => 1 }, + "cacert", + { data_type => "text", is_nullable => 1 }, + "sp_cert", + { data_type => "text", is_nullable => 1 }, + "sp_key", + { data_type => "text", is_nullable => 1 }, "saml2_firstname", { data_type => "text", is_nullable => 1 }, "saml2_surname", { data_type => "text", is_nullable => 1 }, + "saml2_groupname", + { data_type => "text", is_nullable => 1 }, + "saml2_relaystate", + { data_type => "varchar", is_nullable => 1, size => 80 }, "enabled", { data_type => "smallint", default_value => 0, is_nullable => 0 }, "error_messages", @@ -36,6 +46,8 @@ __PACKAGE__->add_columns( __PACKAGE__->set_primary_key("id"); +__PACKAGE__->add_unique_constraint("authentication_ux_saml2_relaystate", ["saml2_relaystate"]); + __PACKAGE__->belongs_to( "site", "GADS::Schema::Result::Site", @@ -48,6 +60,41 @@ __PACKAGE__->belongs_to( }, ); +sub for_data_table +{ my ($self, %params) = @_; + my $site = $params{site}; + my $return = { + _id => $self->id, + ID => { + type => 'id', + name => 'ID', + values => [$self->id] + }, + "Site ID" => { + type => 'string', + name => 'Site ID', + values => [$self->site_id], + }, + Type => { + type => 'string', + name => 'Type', + values => [$self->type], + }, + Name => { + type => 'string', + name => 'Name', + values => [$self->name], + }, + enabled => { + type => 'string', + name => 'enabled', + values => [$self->enabled ? "Enabled" : "Disabled"], + }, + }; + + $return; +} + sub error_messages_decoded { my $self = shift; my $json = $self->error_messages diff --git a/lib/GADS/Schema/ResultSet/Authentication.pm b/lib/GADS/Schema/ResultSet/Authentication.pm index 2ac99a19b..8cd82b2b2 100644 --- a/lib/GADS/Schema/ResultSet/Authentication.pm +++ b/lib/GADS/Schema/ResultSet/Authentication.pm @@ -3,10 +3,21 @@ package GADS::Schema::ResultSet::Authentication; use strict; use warnings; +use GADS::SAML; +use GADS::Util; +use Session::Token; + use parent 'DBIx::Class::ResultSet'; use Log::Report 'linkspace'; +__PACKAGE__->load_components(qw(Helper::ResultSet::CorrelateRelationship)); + +sub providers +{ shift->search({ + }); +} + sub enabled { shift->search({ 'me.enabled' => 1, @@ -16,7 +27,7 @@ sub enabled sub saml2_provider { my $self = shift; $self->enabled->search({ - type => 'saml2', + 'me.type' => 'saml2', })->next; } diff --git a/views/admin/admin_settings.tt b/views/admin/admin_settings.tt index 0b193191b..476e5e35d 100644 --- a/views/admin/admin_settings.tt +++ b/views/admin/admin_settings.tt @@ -132,7 +132,23 @@ + + + [% END %] [% IF user.permission.audit %] diff --git a/views/authentication/providers.tt b/views/authentication/providers.tt new file mode 100644 index 000000000..12c5d8fa1 --- /dev/null +++ b/views/authentication/providers.tt @@ -0,0 +1,73 @@ +[% + # prepare table config + table_class = 'table-striped table-hover'; + table_dom = '<"row row--header"<"col"f><"col-lg-auto dataTables_length_wrapper"l>><"row row--main"<"col-sm-12"tr>><"row row--footer"<"col-sm-12 col-lg-6"p><"col-sm-12 col-lg-6 dataTables_info_wrapper"i>>'; + table_language = { + emptyTable => "There is no data available in this table", + lengthMenu => "Rows per page _MENU_", + paginate => { + next => "Next page", + previous => "Previous page" + }, + search => "Search in authentication providers:", + searchPlaceholder => "Search through your users" + } + table_ajax = url.page _ "/api/providers?csrf-token=" _ csrf_token; + table_ajax_target = url.page _ "/authentication_providers"; # will be appended with /{id} by JS + table_save_state = 1; + table_unresponsive = 0; + table_caption = "Table to show list of authentication providers"; + + table_columns = [{ + name = "ID" + orderable = 1 + },{ + name = "Site ID" + orderable = 1 + },{ + name = "Type" + orderable = 1 + },{ + name = "Name" + orderable = 1 + },{ + name = "enabled" + orderable = 1 + },{ + name = "error_messages" + orderable = 1 + }]; + + # add standardized page header + INCLUDE layouts/page_header.tt + title = "Authentication Providers" + description = "This is an overview of all the authentication providers within your Linkspace system." + aside_buttons = ! user.permission.useradmin ? [] : [{ + type = "modal_button" + modalId = "userModal" + class = "btn btn-add" + label = "Add an authentication provider" + }]; +%] + +
+
+
+
+ +
+
+
+ + [% INCLUDE tables/basic_table.tt; %] +
+ +[%- + INCLUDE wizard/user_add.tt endpoint="/api/user_account" modalId="userModal"; + INCLUDE wizard/user_email.tt modalId="emailUsersModal"; +-%] From 302000f534e309af5b8ddb505f9e999165c58547 Mon Sep 17 00:00:00 2001 From: Timothy Legge Date: Mon, 25 Nov 2024 04:07:02 +0000 Subject: [PATCH 02/50] Working create, update delete authentication providers --- lib/GADS.pm | 87 +++++- lib/GADS/API.pm | 47 ++- lib/GADS/Audit.pm | 16 +- lib/GADS/Authentication.pm | 3 +- lib/GADS/Schema/Result/Authentication.pm | 98 ++++++ lib/GADS/Schema/Result/Site.pm | 94 ++++++ lib/GADS/Schema/ResultSet/Authentication.pm | 48 ++- views/authentication/provider_edit.tt | 120 ++++++++ views/authentication/providers.tt | 4 +- views/wizard/provider_add.tt | 321 ++++++++++++++++++++ 10 files changed, 820 insertions(+), 18 deletions(-) create mode 100644 views/authentication/provider_edit.tt create mode 100644 views/wizard/provider_add.tt diff --git a/lib/GADS.pm b/lib/GADS.pm index c54c8a989..1b7cc21eb 100644 --- a/lib/GADS.pm +++ b/lib/GADS.pm @@ -1516,17 +1516,7 @@ any ['get', 'post'] => '/authentication_providers/' => require_any_role [qw/user my $auth = GADS::Authentication->new(schema => schema); template 'authentication/providers' => { - values => { - site_id => "site_id", #$auth->site_id, - type => "type", #$auth->type, - name => "name", #$auth->name, - xml => "xm;", #$auth->xml, - saml2_firstname => "first", #$auth->saml2_firstname, - saml2_surname => "surname", #$auth->saml2_surname, - enabled => "enabled", #$auth->enabled, - error_messages => "error_message", #$auth->error_messages, - - }, + providers => $auth, permissions => "permisission", #$auth->permissions, page => 'system_settings', }; @@ -1607,6 +1597,81 @@ any ['get', 'post'] => '/user_requests/' => require_any_role [qw/useradmin super }; }; +any ['get', 'post'] => '/authentication_providers/:id' => require_any_role [qw/useradmin superadmin/] => sub { + my $user = logged_in_user; + my $userso = GADS::Users->new(schema => schema); + my $auth = GADS::Authentication->new(schema => schema); + my $id = route_parameters->get('id'); + my $audit = GADS::Audit->new(schema => schema, user => $user); + + if (!$id) { + error __x"User id not available"; + } + + my $editProvider = rset('Authentication')->providers->search({id => $id})->next + or error __x"Authentication provider id {id} not found", id => $id; + + # The submit button will still be triggered on a new org/title creation, + # if the user has pressed enter, in which case ignore it + if (param('submit')) + { + my %values = ( + name => param('name'), + type => param('type'), + saml2_firstname => param('saml2_firstname'), + saml2_surname => param('saml2_surname'), + xml => param('xml'), + cacert => param('cacert'), + sp_cert => param('sp_cert'), + sp_key => param('sp_key'), + saml2_relaystate => param('saml2_relaystate'), + saml2_groupname => param('saml2_groupname'), + ); + $values{permissions} = [body_parameters->get_all('permission')] + if logged_in_user->permission->{superadmin}; + + if (process sub { + # Don't use DBIC update directly, so that permissions etc are updated properly + $editProvider->update_provider(current_user => logged_in_user, %values); + }) + { + return forwardHome( + { success => "Authentication Provider has been updated successfully" }, 'authentication_providers/' ); + } + } + elsif (my $delete_id = param('delete')) + { + return forwardHome( + { danger => "You do not have permission to delete an authentication provider" } ) + if !logged_in_user->permission->{superadmin}; + my $usero = rset('Authentication')->find($delete_id); + return forwardHome( + { danger => "Cannot delete currently active authentication provider" } ) + if $usero->enabled; + if (process( sub { $usero->retire(current_user => logged_in_user) })) + { + #FIXME: fix audit + $audit->login_change("Authentication Provider ID $delete_id deleted"); + return forwardHome( + { success => "Authentication Provider has been updated successfully" }, 'authentication_providers/' ); + } + } + + my $output = template 'authentication/provider_edit' => { + editprovider => $editProvider, + groups => GADS::Groups->new(schema => schema)->all, + values => { + title => $userso->titles, + organisation => $userso->organisations, + department_id => $userso->departments, + team_id => $userso->teams, + }, + permissions => $userso->permissions, + page => 'admin', + }; + $output; +}; + any ['get', 'post'] => '/user/:id' => require_any_role [qw/useradmin superadmin/] => sub { my $user = logged_in_user; my $userso = GADS::Users->new(schema => schema); diff --git a/lib/GADS/API.pm b/lib/GADS/API.pm index db474d3b9..1ce002a7e 100644 --- a/lib/GADS/API.pm +++ b/lib/GADS/API.pm @@ -541,6 +541,10 @@ post '/api/table_request' => require_login sub { _post_table_request(); }; +post '/api/authentication_providers/?:id?' => require_login sub { + _post_add_authentication_providers(); +}; + # AJAX record browse any ['get', 'post'] => '/api/:sheet/records' => require_login sub { _get_records(); @@ -755,6 +759,46 @@ sub _post_add_user_account return _success("$msg"); } +sub _post_add_authentication_providers +{ my $body = _decode_json_body(); + + my $logged_in_user = logged_in_user; + + my $id = route_parameters->get('id'); + my $update_provider; + if ($id) + { + $update_provider = schema->resultset('Authentication')->find($id) + or error __x"Authentication id {id} not found", id => $id; + } + + error __"Unauthorised access" + unless $logged_in_user->permission->{superadmin} || $logged_in_user->permission->{useradmin}; + + my %values = ( + name => $body->{name}, + type => $body->{type}, + saml2_firstname => $body->{saml2_firstname}, + saml2_surname => $body->{saml2_surname}, + xml => $body->{xml}, + cacert => $body->{cacert}, + sp_cert => $body->{sp_cert}, + sp_key => $body->{sp_key}, + saml2_relaystate => $body->{saml2_relaystate}, + saml2_groupname => $body->{saml2_groupname}, + ); + + $values{permissions} = $body->{permissions} + if $logged_in_user->permission->{superadmin}; + + # Any exceptions/errors generated here will be automatically sent back as JSON error + $id ? $update_provider->update_provider(%values, current_user => $logged_in_user) + : schema->resultset('Authentication')->create_provider(%values, current_user => $logged_in_user, request_base => request->base); + + my $msg = __x"Authentication Provider {type} successfully", type => $id ? 'updated' : 'created'; + return _success("$msg"); +} + sub _create_table { my $params = shift; @@ -1338,7 +1382,7 @@ any ['get', 'post'] => '/api/providers' => require_any_role [qw/useradmin supera if ($params->get('cols')) { # Get columns to be shown in the users table summary - my @cols = qw/site_id type name xml saml2_firstname saml2_surname enabled error_messages/; + my @cols = qw/site_id type name xml saml2_firstname saml2_surname cacert sp_cert sp_key enabled error_messages/; #push @cols, 'title' if $site->register_show_title; #push @cols, 'email'; #push @cols, 'organisation' if $site->register_show_organisation; @@ -1358,7 +1402,6 @@ any ['get', 'post'] => '/api/providers' => require_any_role [qw/useradmin supera my $total = $auth->count; my $col_order = $params->get('order[0][column]'); - use Data::Dumper; my $sort_by = defined $col_order && $params->get("columns[${col_order}][name]"); my $dir = $params->get('order[0][dir]'); my $search = $params->get('search[value]'); diff --git a/lib/GADS/Audit.pm b/lib/GADS/Audit.pm index f0413e3d2..44acaaad1 100644 --- a/lib/GADS/Audit.pm +++ b/lib/GADS/Audit.pm @@ -70,8 +70,22 @@ has filtering => ( builder => sub { +{} }, ); -sub audit_types{ [qw/user_action login_change login_success logout login_failure/] }; +sub audit_types{ [qw/user_action login_change login_success logout login_failure auth_provider_change/] }; +sub auth_provider_change +{ my ($self, %options) = @_; + + my $layout = $options{layout}; + $self->schema->resultset('Audit')->create({ + user_id => $self->user_id, + description => $options{description}, + type => 'auth_provider_change', + method => $options{method}, + url => $options{url}, + datetime => DateTime->now, + instance_id => $layout && $layout->instance_id, + }); +} sub user_action { my ($self, %options) = @_; diff --git a/lib/GADS/Authentication.pm b/lib/GADS/Authentication.pm index 422c20487..856893465 100644 --- a/lib/GADS/Authentication.pm +++ b/lib/GADS/Authentication.pm @@ -46,7 +46,8 @@ sub authentication_summary_rs { my $self = shift; my $summary = $self->authentication_rs->search_rs({},{ columns => [ - 'me.id', 'me.site_id', 'me.type', 'me.name', 'me.enabled', 'me.error_messages', + 'me.id', 'me.site_id', 'me.type', 'me.name', 'me.enabled', 'me.error_messages', + 'me.xml', 'me.sp_key', ], order_by => 'name', collapse => 1, diff --git a/lib/GADS/Schema/Result/Authentication.pm b/lib/GADS/Schema/Result/Authentication.pm index 150acc666..fd7d705ad 100644 --- a/lib/GADS/Schema/Result/Authentication.pm +++ b/lib/GADS/Schema/Result/Authentication.pm @@ -108,4 +108,102 @@ sub user_not_found_error || "Username {username} not found"; } +sub update_provider +{ my ($self, %params) = @_; + my $request = 0; + + my $guard = $self->result_source->schema->txn_scope_guard; + + # This was originally a delete call, does this now create an issue as it's required for the internal "update" call within `create user` + my $current_user = $params{current_user}; + + my $site = $self->result_source->schema->resultset('Site')->next; + + # Set null values where required for database insertions + #delete $params{title} if !$params{title} && !$site->user_field_is_editable('title'); + + my $values; + + if(defined $params{account_request}) { + #FIXME: Remove + $values->{account_request} = $params{account_request}; + $request = 1 if $self->account_request && !$params{account_request}; + } + if($request) { + #FIXME: Remove + $self->result_source->schema->resultset('Authentication')->create_provider(%params); + #$self->result_source->schema->resultset('Authentication')->find($self->id)->delete; + + $guard->commit; + + } else { + + my $original_name = $self->name; + + foreach my $field ($site->provider_fields) + { + next if !exists $params{$field->{name}}; + my $fname = $field->{name}; + $self->$fname($params{$fname}); + } + + my $audit = GADS::Audit->new(schema => $self->result_source->schema, user => $current_user); + + $audit->auth_provider_change("Provider $original_name (id ".$self->id.") being changed to ".$self->name) + if $original_name && $self->is_column_changed('name'); + + $self->update($values); + + error __"You do not have permission to update an authentication provider" + if !$current_user->permission->{superadmin}; + + my $required = 0; + + error __x"Please select a {name} for the user", name => $site->team_name + if !$params{team_id} && $required; + + error __x"Please select a {name} for the user", name => $site->department_name + if !$params{department_id} && $required; + + length $params{name} <= 128 + or error __"Name must be less than 128 characters"; + length $params{saml2_surname} <= 128 + or error __"Surname attribute must be less than 128 characters" + if defined $params{saml2_surname}; + length $params{saml2_firstname} <= 128 + or error __"Firstname attribute must be less than 128 characters" + if defined $params{saml2_firstname}; + length $params{saml2_groupname} <= 128 + or error __"Groupname attribute must be less than 128 characters" + if defined $params{saml2_groupname}; + !defined $params{organisation} || $params{organisation} =~ /^[0-9]+$/ + or error __x"Invalid organisation {id}", id => $params{organisation}; + + my $msg = __x"Authentication Provider updated: ID {id}, name: {name}", + id => $self->id, name => $params{name}; + + $audit->auth_provider_change(description => $msg); + + $guard->commit; + } +} + +sub retire +{ my ($self, %options) = @_; + + my $schema = $self->result_source->schema; + my $current_user = $options{current_user}; + + error __"You do not have permission to update an authentication provider" + if !$current_user->permission->{superadmin}; + + my $audit = GADS::Audit->new(schema => $self->result_source->schema, user => $current_user); + + my $msg = __x"Authentication Provider: ID {id}, name: {name} was deleted", + id => $self->id, name => $self->name; + $audit->auth_provider_change(description => $msg); + + $self->delete(); +} + 1; diff --git a/lib/GADS/Schema/Result/Site.pm b/lib/GADS/Schema/Result/Site.pm index d5ac50eb5..d75e2e0d8 100644 --- a/lib/GADS/Schema/Result/Site.pm +++ b/lib/GADS/Schema/Result/Site.pm @@ -261,6 +261,100 @@ sub user_field_by_description $field; } +sub provider_fields +{ my $self = shift; + + my @fields = ( + { + name => 'name', + description => 'Provider Name', + type => 'freetext', + placeholder => 'Provider Name', + is_required => 1, + }, + { + name => 'type', + description => 'Type', + type => 'dropdown', + placeholder => 'SAML2', + is_required => 1, + }, + { + name => 'enabled', + description => 'Enabled', + type => 'freetext', + placeholder => '', + }, + { + name => 'entity_id', + description => 'Entity ID', + type => 'freetext', + placeholder => 'Entity ID', + }, + { + name => 'sso_url', + description => 'Reply URL / SSO URL / ACS URL', + type => 'freetext', + placeholder => '', + }, + { + name => 'saml2_firstname', + description => 'User attribute for first name (optional)', + type => 'freetext', + placeholder => 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname', + }, + { + name => 'saml2_surname', + description => 'User attribute for surname (optional)', + type => 'freetext', + placeholder => 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname', + }, + { + name => 'saml2_groupname', + description => 'Attribute for groupname (optional)', + type => 'freetext', + placeholder => 'http://schemas.xmlsoap.org/claims/Group', + }, + { + name => 'xml', + description => 'Identity Provider Metadata XML', + type => 'textarea', + placeholder => '', + }, + { + name => 'cacert', + description => 'Identity provider Signing/Encryption Certificate', + type => 'textarea', + placeholder => '', + }, + { + name => 'sp_key', + description => 'GADS Authentication Provider Key', + type => 'textarea', + placeholder => '', + }, + { + name => 'sp_cert', + description => 'GADS Authentication Provider Certificate', + type => 'textarea', + placeholder => '', + }, + { + name => 'saml2_relaystate', + description => 'RelayState', + type => 'freetext', + placeholder => '', + }, + ); + + my $user_editable = decode_json($self->user_editable_fields || '{}'); + + $_->{editable} = $user_editable->{$_->{name}} // 1 # Default to editable + foreach @fields; + + return @fields; +} + sub user_fields { my $self = shift; diff --git a/lib/GADS/Schema/ResultSet/Authentication.pm b/lib/GADS/Schema/ResultSet/Authentication.pm index 8cd82b2b2..3328faf84 100644 --- a/lib/GADS/Schema/ResultSet/Authentication.pm +++ b/lib/GADS/Schema/ResultSet/Authentication.pm @@ -14,7 +14,10 @@ use Log::Report 'linkspace'; __PACKAGE__->load_components(qw(Helper::ResultSet::CorrelateRelationship)); sub providers -{ shift->search({ +{ my ($self, %search) = @_; + + $self->search({ + %search, }); } @@ -31,4 +34,47 @@ sub saml2_provider })->next; } +sub create_provider +{ my ($self, %params) = @_; + + my $guard = $self->result_source->schema->txn_scope_guard; + + my $site = $self->result_source->schema->resultset('Site')->next; + + error __"A name must be specified for the provider" + if !$params{name}; + + error __x"Provider {name} already exists", name => $params{name} + if $self->providers(name => $params{name})->count; + + my $code = Session::Token->new( length => 32 )->get; + my $request_base = $params{request_base}; + + my $provider = $self->create({ + name => $params{name}, + type => $params{type}, + saml2_firstname => $params{saml2_firstname}, + saml2_surname => $params{saml2_surname}, + xml => $params{xml}, + cacert => $params{cacert}, + sp_cert => $params{sp_cert}, + sp_key => $params{sp_key}, + saml2_relaystate => $params{saml2_relaystate}, + saml2_groupname => $params{saml2_groupname}, + }); + + my $audit = GADS::Audit->new(schema => $self->result_source->schema, user => $params{auth_provider_change}); + + $audit->login_change( + __x"Provider created, id: {id}, provider: {provider}", + id => $provider->id, provider => $params{name} + ); + + $provider->update_provider(%params); + + $guard->commit; + + return $provider; +} + 1; diff --git a/views/authentication/provider_edit.tt b/views/authentication/provider_edit.tt new file mode 100644 index 000000000..53aaf7227 --- /dev/null +++ b/views/authentication/provider_edit.tt @@ -0,0 +1,120 @@ +[%- + # add standardized page header + INCLUDE layouts/page_header.tt + title = "Edit provider: " _ editprovider.name + description = "In this window you can edit an authentication provider in the system." + back_button = { url = url.page _ "/authentication_providers/" }; +-%] +
+
+
+ My details + [%- + INCLUDE fields/hidden.tt name="csrf_token" value=csrf_token; + INCLUDE fields/hidden.tt name="page" value=page; + INCLUDE fields/hidden.tt name="username" value=user.username filter="html_entity"; + INCLUDE fields/hidden.tt name="id" value=editprovider.id; + + field_counter = 0; + + IF site.provider_fields.size > 0; + -%] +
+ [% END %] + [% FOREACH field IN site.provider_fields %] + [% IF field_counter > 0 AND field_counter % 2 == 0 %] +
+
+ [% END %] +
+ [% + IF field.type == "freetext"; + INCLUDE fields/input.tt + id = field.name + name = field.name + value = editprovider.${field.name} + label = field.description + placeholder = field.placeholder + type = "text" + filter = "html" + sub_field = "" + sub_params = {}; + + ELSIF field.type == "dropdown"; + INCLUDE fields/select_single.tt + id = field.name + name = field.name + value = editprovider.get_column(field.name) + label = field.description + placeholder = field.placeholder + items = values.${field.name} + sub_field = "" + sub_params = {}; + + ELSIF field.type == "textarea"; + INCLUDE fields/textarea.tt + id = field.name + name = field.name + value = editprovider.${field.name} + label = field.description + rows = 10 + placeholder = field.placeholder + filter = "html" + sub_field = "" + sub_params = {}; + + END; + %] +
+ [% + IF field.name == "email"; + field_counter = field_counter + 2; + ELSE; + field_counter = field_counter + 1; + END; + END; + %] + [% IF site.provider_fields.size > 0 %] +
+ [% END %] + +
+ + [% + INCLUDE navigation/button_bar.tt + row_class = "row mt-5" + columns = [{ + class = "col-sm-auto mb-3 mb-sm-0", + buttons = [{ + type = "link", + class = "btn btn-cancel", + target = url.page _ "/authentication_providers/", + label = "Cancel" + }] + }, { + class = "col-sm-auto", + buttons = [{ + type = "modal_button", + modalId = "deleteModal", + dataTitle = editprovider.name, + dataId = editprovider.id, + class = "btn btn-danger btn-js-delete", + label = "Delete provider" + }, { + type = "button", + name = "submit", + value = "submit", + class = "btn btn-default", + label = "Save changes" + }] + }]; + %] +
+
+ +[% + INCLUDE wizard/delete.tt + modalId = "deleteModal" + label = "Delete " _ editprovider.name + description = "Are you sure you want to delete this provider? You cannot undo this step."; +%] diff --git a/views/authentication/providers.tt b/views/authentication/providers.tt index 12c5d8fa1..bc98573e3 100644 --- a/views/authentication/providers.tt +++ b/views/authentication/providers.tt @@ -44,7 +44,7 @@ description = "This is an overview of all the authentication providers within your Linkspace system." aside_buttons = ! user.permission.useradmin ? [] : [{ type = "modal_button" - modalId = "userModal" + modalId = "providerModal" class = "btn btn-add" label = "Add an authentication provider" }]; @@ -68,6 +68,6 @@ [%- - INCLUDE wizard/user_add.tt endpoint="/api/user_account" modalId="userModal"; + INCLUDE wizard/provider_add.tt endpoint="/api/authentication_providers" modalId="providerModal"; INCLUDE wizard/user_email.tt modalId="emailUsersModal"; -%] diff --git a/views/wizard/provider_add.tt b/views/wizard/provider_add.tt new file mode 100644 index 000000000..8292792ae --- /dev/null +++ b/views/wizard/provider_add.tt @@ -0,0 +1,321 @@ +[%- + firstFrameFields = []; + secondFrameFields = []; + thirdFrameFields = []; + + FOREACH field IN site.provider_fields; + IF field.name == "name" OR field.name == "type"; + firstFrameFields.push(field); + ELSIF field.name == "entity_id" OR field.name == "sso_url" OR field.name == "saml2_firstname" OR + field.name == "saml2_lastname" OR field.name == "saml2_groupname"; + secondFrameFields.push(field); + ELSE; + thirdFrameFields.push(field); + END; + END; +-%] + From 1bd4e585729fe498cef1825a1466582815f4028e Mon Sep 17 00:00:00 2001 From: Timothy Legge Date: Tue, 26 Nov 2024 03:44:05 +0000 Subject: [PATCH 03/50] Set permissions based on SAML attributes --- lib/GADS.pm | 5 ++--- lib/GADS/Schema/Result/Site.pm | 5 +++-- lib/GADS/Schema/Result/User.pm | 25 +++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/GADS.pm b/lib/GADS.pm index 1b7cc21eb..35f08bfc7 100644 --- a/lib/GADS.pm +++ b/lib/GADS.pm @@ -489,7 +489,8 @@ post '/saml' => sub { my $callback = $saml->callback( saml_response => body_parameters->get('SAMLResponse'), - cacert => $authentication->cacert, + defined $authentication->cacert ? + (cacert => $authentication->cacert) : (), ); my $username = $callback->{nameid} @@ -503,8 +504,6 @@ post '/saml' => sub { return forwardHome({ danger => __x($msg, username => $username) }, 'login?password=1' ); } - # TIM FIXME add in the group attributes here - $user->update_attributes($callback->{attributes}); $user->update({ lastlogin => DateTime->now }); diff --git a/lib/GADS/Schema/Result/Site.pm b/lib/GADS/Schema/Result/Site.pm index d75e2e0d8..fde474b0e 100644 --- a/lib/GADS/Schema/Result/Site.pm +++ b/lib/GADS/Schema/Result/Site.pm @@ -275,8 +275,9 @@ sub provider_fields { name => 'type', description => 'Type', - type => 'dropdown', - placeholder => 'SAML2', + #FIXME - timlegge type => 'dropdown', + type => 'freetext', + placeholder => 'saml2', is_required => 1, }, { diff --git a/lib/GADS/Schema/Result/User.pm b/lib/GADS/Schema/Result/User.pm index 7de2dbe93..9df905759 100644 --- a/lib/GADS/Schema/Result/User.pm +++ b/lib/GADS/Schema/Result/User.pm @@ -1092,6 +1092,7 @@ sub has_draft sub update_attributes { my ($self, $attributes) = @_; my $authentication = $self->result_source->schema->resultset('Authentication')->saml2_provider; + my $site = $self->result_source->schema->resultset('Site')->next; if (my $at = $authentication->saml2_firstname) { $self->update({ firstname => $attributes->{$at}->[0] }); @@ -1100,6 +1101,30 @@ sub update_attributes { $self->update({ surname => $attributes->{$at}->[0] }); } + if (my $at = $authentication->saml2_groupname) + { + my %permission_map = ( + 'GADS-SuperAdmin' => 'superadmin', + 'GADS-UserAdmin' => 'useradmin', + 'GADS-Audit' => 'audit', + ); + + my @permissions; + for my $group (@{$attributes->{$at}}) { + push @permissions, $permission_map{$group} if defined $permission_map{$group} and $group =~ /^GADS-/; + } + if (@permissions) + { + error __"You do not have permission to set global user permissions" + if !$self->permission->{superadmin}; + $self->permissions(@permissions); + # Clear and rebuild permissions, in case of form submission failure. We + # need to rebuild now, otherwise the transaction may have rolled-back + # to the old version by the time it is built in the template + $self->clear_permission; + $self->permission; + } + } my $value = _user_value({firstname => $self->firstname, surname => $self->surname}); $self->update({ value => $value }); } From 188a6c9278688f41e0a1dc4c8b923b8a398409df Mon Sep 17 00:00:00 2001 From: Timothy Legge Date: Wed, 27 Nov 2024 00:20:20 +0000 Subject: [PATCH 04/50] Update the groups from the SAML2 assertion --- lib/GADS/Schema/Result/User.pm | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/GADS/Schema/Result/User.pm b/lib/GADS/Schema/Result/User.pm index 9df905759..a4ad45039 100644 --- a/lib/GADS/Schema/Result/User.pm +++ b/lib/GADS/Schema/Result/User.pm @@ -1103,6 +1103,7 @@ sub update_attributes } if (my $at = $authentication->saml2_groupname) { + #FIXME - Move this to the UI and allow users to map my %permission_map = ( 'GADS-SuperAdmin' => 'superadmin', 'GADS-UserAdmin' => 'useradmin', @@ -1110,8 +1111,8 @@ sub update_attributes ); my @permissions; - for my $group (@{$attributes->{$at}}) { - push @permissions, $permission_map{$group} if defined $permission_map{$group} and $group =~ /^GADS-/; + for my $permission (@{$attributes->{$at}}) { + push @permissions, $permission_map{$permission} if defined $permission_map{$permission} and $permission =~ /^GADS-/; } if (@permissions) { @@ -1124,6 +1125,22 @@ sub update_attributes $self->clear_permission; $self->permission; } + + my $schema = $self->result_source->schema; + + my @groups; + for my $group (@{$attributes->{$at}}) { + next if defined $permission_map{$group}; + #FIXME: There is likely a much better way to do this + my @groups1 = $schema->resultset('Group')->search({name => $group}, {order_by => 'me.name'})->first; + push @groups, $groups1[0]->id if $groups1[0]->name eq $group; + } + if (@groups) + { + $self->groups($self, \@groups); + $self->clear_has_group; + $self->has_group; + } } my $value = _user_value({firstname => $self->firstname, surname => $self->surname}); $self->update({ value => $value }); From 613f76297757097d70800e25cd96926a0266b190 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Dec 2024 01:31:58 +0000 Subject: [PATCH 05/50] Allow SAML response to set the groups and permissions --- lib/GADS/Schema/Result/User.pm | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/GADS/Schema/Result/User.pm b/lib/GADS/Schema/Result/User.pm index a4ad45039..eec42de4e 100644 --- a/lib/GADS/Schema/Result/User.pm +++ b/lib/GADS/Schema/Result/User.pm @@ -915,8 +915,10 @@ sub update_user if ($params{permissions} && ref $params{permissions} eq 'ARRAY') { - error __"You do not have permission to set global user permissions" - if !$current_user->permission->{superadmin}; + # FIXME: SAML should be able to set groups + # error __"You do not have permission to set global user permissions" + # if !$current_user->permission->{superadmin}; + # $self->permissions(@{$params{permissions}}); # Clear and rebuild permissions, in case of form submission failure. We # need to rebuild now, otherwise the transaction may have rolled-back @@ -1116,8 +1118,9 @@ sub update_attributes } if (@permissions) { - error __"You do not have permission to set global user permissions" - if !$self->permission->{superadmin}; + # FIXME: SAML should be able to set groups + # error __"You do not have permission to set global user permissions" + # if !$self->permission->{superadmin}; $self->permissions(@permissions); # Clear and rebuild permissions, in case of form submission failure. We # need to rebuild now, otherwise the transaction may have rolled-back From 7e907f4dfff29d8796aa58ed24fbf9566f6d180f Mon Sep 17 00:00:00 2001 From: Timothy Legge Date: Sat, 7 Dec 2024 15:28:53 -0400 Subject: [PATCH 06/50] Cleanup and review the previous changes --- lib/GADS.pm | 21 ++++++++++++------- lib/GADS/API.pm | 26 ++++++++++-------------- lib/GADS/Authentication.pm | 4 ++-- lib/GADS/SAML.pm | 3 ++- lib/GADS/Schema/Result/Authentication.pm | 22 +++++++------------- lib/GADS/Schema/Result/Site.pm | 2 +- lib/GADS/Schema/Result/User.pm | 7 +++++++ views/admin/admin_settings.tt | 1 - views/authentication/provider_edit.tt | 8 -------- views/authentication/providers.tt | 1 - views/wizard/provider_add.tt | 2 -- 11 files changed, 44 insertions(+), 53 deletions(-) diff --git a/lib/GADS.pm b/lib/GADS.pm index 35f08bfc7..cdbc0948e 100644 --- a/lib/GADS.pm +++ b/lib/GADS.pm @@ -1604,13 +1604,14 @@ any ['get', 'post'] => '/authentication_providers/:id' => require_any_role [qw/u my $audit = GADS::Audit->new(schema => schema, user => $user); if (!$id) { - error __x"User id not available"; + error __x"Authentication proovider not available"; } my $editProvider = rset('Authentication')->providers->search({id => $id})->next or error __x"Authentication provider id {id} not found", id => $id; - # The submit button will still be triggered on a new org/title creation, + # FIXME: Is this true in the case of a provider + # The submit button will still be triggered on a REPLACE_THIS creation, # if the user has pressed enter, in which case ignore it if (param('submit')) { @@ -1626,10 +1627,13 @@ any ['get', 'post'] => '/authentication_providers/:id' => require_any_role [qw/u saml2_relaystate => param('saml2_relaystate'), saml2_groupname => param('saml2_groupname'), ); + # FIXME: Remove permissions below $values{permissions} = [body_parameters->get_all('permission')] if logged_in_user->permission->{superadmin}; + # FIXME: Remove above if (process sub { + # FIXME: permissions note # Don't use DBIC update directly, so that permissions etc are updated properly $editProvider->update_provider(current_user => logged_in_user, %values); }) @@ -1644,8 +1648,10 @@ any ['get', 'post'] => '/authentication_providers/:id' => require_any_role [qw/u { danger => "You do not have permission to delete an authentication provider" } ) if !logged_in_user->permission->{superadmin}; my $usero = rset('Authentication')->find($delete_id); + + # FIXME: Should change this so cannot delete enabled provider for current user return forwardHome( - { danger => "Cannot delete currently active authentication provider" } ) + { danger => "Cannot delete currently an enabled authentication provider" } ) if $usero->enabled; if (process( sub { $usero->retire(current_user => logged_in_user) })) { @@ -1656,14 +1662,15 @@ any ['get', 'post'] => '/authentication_providers/:id' => require_any_role [qw/u } } + # FIXME need to revise what is passed to the template my $output = template 'authentication/provider_edit' => { editprovider => $editProvider, groups => GADS::Groups->new(schema => schema)->all, values => { - title => $userso->titles, - organisation => $userso->organisations, - department_id => $userso->departments, - team_id => $userso->teams, + title => $userso->titles, # FIXME delete + organisation => $userso->organisations, # FIXME delete + department_id => $userso->departments, # FIXME delete + team_id => $userso->teams, # FIXME delete }, permissions => $userso->permissions, page => 'admin', diff --git a/lib/GADS/API.pm b/lib/GADS/API.pm index 1ce002a7e..bd3ecc9e9 100644 --- a/lib/GADS/API.pm +++ b/lib/GADS/API.pm @@ -760,7 +760,8 @@ sub _post_add_user_account } sub _post_add_authentication_providers -{ my $body = _decode_json_body(); +{ + my $body = _decode_json_body(); my $logged_in_user = logged_in_user; @@ -788,6 +789,7 @@ sub _post_add_authentication_providers saml2_groupname => $body->{saml2_groupname}, ); + # FIXME remove permissions? $values{permissions} = $body->{permissions} if $logged_in_user->permission->{superadmin}; @@ -1383,13 +1385,6 @@ any ['get', 'post'] => '/api/providers' => require_any_role [qw/useradmin supera { # Get columns to be shown in the users table summary my @cols = qw/site_id type name xml saml2_firstname saml2_surname cacert sp_cert sp_key enabled error_messages/; - #push @cols, 'title' if $site->register_show_title; - #push @cols, 'email'; - #push @cols, 'organisation' if $site->register_show_organisation; - #push @cols, 'department' if $site->register_show_department; - #push @cols, 'team' if $site->register_show_team; - #push @cols, 'freetext1' if $site->register_freetext1_name; - #push @cols, qw/created lastlogin/; my @return = map { { name => $_, data => $_ } } @cols; content_type 'application/json; charset=UTF-8'; return encode_json \@return; @@ -1406,6 +1401,7 @@ any ['get', 'post'] => '/api/providers' => require_any_role [qw/useradmin supera my $dir = $params->get('order[0][dir]'); my $search = $params->get('search[value]'); + # FIXME possibly need to revise this if (my $sort_field = $site->user_field_by_description($sort_by)) { $sort_by = $sort_field->{name} eq 'site_id' @@ -1434,14 +1430,14 @@ any ['get', 'post'] => '/api/providers' => require_any_role [qw/useradmin supera $s or next; $s =~ s/\_/\\\_/g; # Escape special like char push @sr, [ - 'me.id' => $s =~ /^[0-9]+$/ ? $s : undef, - # surname and firstname are case sensitive in database - 'me.site_id' => { -like => "%$s%" }, - 'me.type' => { -like => "%$s%" }, - 'me.name' => { -like => "%$s%" }, + 'me.id' => $s =~ /^[0-9]+$/ ? $s : undef, + # FIXME should some of these be case sensitive in database + 'me.site_id' => { -like => "%$s%" }, + 'me.type' => { -like => "%$s%" }, + 'me.name' => { -like => "%$s%" }, 'me.saml2_firstname' => { -like => "%$s%" }, - 'me.saml2_surname' => { -like => "%$s%" }, - 'me.enabled' => { -like => "%$s%" }, + 'me.saml2_surname' => { -like => "%$s%" }, + 'me.enabled' => { -like => "%$s%" }, ]; } diff --git a/lib/GADS/Authentication.pm b/lib/GADS/Authentication.pm index 856893465..43a576547 100644 --- a/lib/GADS/Authentication.pm +++ b/lib/GADS/Authentication.pm @@ -1,6 +1,6 @@ =pod GADS - Globally Accessible Data Store -Copyright (C) 2014 Ctrl O Ltd +Copyright (C) 2024 Ctrl O Ltd This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -47,7 +47,7 @@ sub authentication_summary_rs my $summary = $self->authentication_rs->search_rs({},{ columns => [ 'me.id', 'me.site_id', 'me.type', 'me.name', 'me.enabled', 'me.error_messages', - 'me.xml', 'me.sp_key', + #FIXME remove 'me.xml', 'me.sp_key', ], order_by => 'name', collapse => 1, diff --git a/lib/GADS/SAML.pm b/lib/GADS/SAML.pm index d5d61d36e..79ce4d1aa 100644 --- a/lib/GADS/SAML.pm +++ b/lib/GADS/SAML.pm @@ -120,13 +120,14 @@ sub initiate $cacert_fh ? (cacert => $cacert_fh->filename) : (), ); + unlink $cacert_fh->filename; my $sso_url = $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'); my $authnreq = Net::SAML2::Protocol::AuthnRequest->new( issuer => $self->sso_xml, destination => $sso_url, nameid_format => $idp->format('emailAddress') || undef, - # assertion_url => "https://$www/app/saml", + # FIXME assertion_url => "https://$www/app/saml", ); $self->request_id($authnreq->id); diff --git a/lib/GADS/Schema/Result/Authentication.pm b/lib/GADS/Schema/Result/Authentication.pm index fd7d705ad..147292685 100644 --- a/lib/GADS/Schema/Result/Authentication.pm +++ b/lib/GADS/Schema/Result/Authentication.pm @@ -114,21 +114,19 @@ sub update_provider my $guard = $self->result_source->schema->txn_scope_guard; - # This was originally a delete call, does this now create an issue as it's required for the internal "update" call within `create user` + # FIXME: Is this comment required + # This was originally a delete call, does this now create an issue as + # it's required for the internal "update" call within `create user` my $current_user = $params{current_user}; my $site = $self->result_source->schema->resultset('Site')->next; + # FIXME: may need this for provider # Set null values where required for database insertions #delete $params{title} if !$params{title} && !$site->user_field_is_editable('title'); my $values; - if(defined $params{account_request}) { - #FIXME: Remove - $values->{account_request} = $params{account_request}; - $request = 1 if $self->account_request && !$params{account_request}; - } if($request) { #FIXME: Remove $self->result_source->schema->resultset('Authentication')->create_provider(%params); @@ -152,18 +150,12 @@ sub update_provider $audit->auth_provider_change("Provider $original_name (id ".$self->id.") being changed to ".$self->name) if $original_name && $self->is_column_changed('name'); - $self->update($values); - error __"You do not have permission to update an authentication provider" if !$current_user->permission->{superadmin}; - my $required = 0; - - error __x"Please select a {name} for the user", name => $site->team_name - if !$params{team_id} && $required; + $self->update($values); - error __x"Please select a {name} for the user", name => $site->department_name - if !$params{department_id} && $required; + my $required = 0; length $params{name} <= 128 or error __"Name must be less than 128 characters"; @@ -194,7 +186,7 @@ sub retire my $schema = $self->result_source->schema; my $current_user = $options{current_user}; - error __"You do not have permission to update an authentication provider" + error __"You do not have permission to delete an authentication provider" if !$current_user->permission->{superadmin}; my $audit = GADS::Audit->new(schema => $self->result_source->schema, user => $current_user); diff --git a/lib/GADS/Schema/Result/Site.pm b/lib/GADS/Schema/Result/Site.pm index fde474b0e..deaeaf493 100644 --- a/lib/GADS/Schema/Result/Site.pm +++ b/lib/GADS/Schema/Result/Site.pm @@ -275,7 +275,7 @@ sub provider_fields { name => 'type', description => 'Type', - #FIXME - timlegge type => 'dropdown', + #FIXME - timlegge type => 'dropdown', type => 'freetext', placeholder => 'saml2', is_required => 1, diff --git a/lib/GADS/Schema/Result/User.pm b/lib/GADS/Schema/Result/User.pm index eec42de4e..df8cbaaef 100644 --- a/lib/GADS/Schema/Result/User.pm +++ b/lib/GADS/Schema/Result/User.pm @@ -1095,6 +1095,10 @@ sub update_attributes { my ($self, $attributes) = @_; my $authentication = $self->result_source->schema->resultset('Authentication')->saml2_provider; my $site = $self->result_source->schema->resultset('Site')->next; + + # Automatically update the firstname and surname if the + # SAML provider has the proper attributes set + # How do we know if this provider is used for this user??? if (my $at = $authentication->saml2_firstname) { $self->update({ firstname => $attributes->{$at}->[0] }); @@ -1103,6 +1107,7 @@ sub update_attributes { $self->update({ surname => $attributes->{$at}->[0] }); } + # Automatically update the groups and permissions for the user from the SAML2 attributes if (my $at = $authentication->saml2_groupname) { #FIXME - Move this to the UI and allow users to map @@ -1114,6 +1119,7 @@ sub update_attributes my @permissions; for my $permission (@{$attributes->{$at}}) { + # FIXME: hard coded permission? push @permissions, $permission_map{$permission} if defined $permission_map{$permission} and $permission =~ /^GADS-/; } if (@permissions) @@ -1132,6 +1138,7 @@ sub update_attributes my $schema = $self->result_source->schema; my @groups; + # Automatically update the groups for the user from the SAML2 attributes for my $group (@{$attributes->{$at}}) { next if defined $permission_map{$group}; #FIXME: There is likely a much better way to do this diff --git a/views/admin/admin_settings.tt b/views/admin/admin_settings.tt index 476e5e35d..02e86f905 100644 --- a/views/admin/admin_settings.tt +++ b/views/admin/admin_settings.tt @@ -148,7 +148,6 @@ - [% END %] [% IF user.permission.audit %] diff --git a/views/authentication/provider_edit.tt b/views/authentication/provider_edit.tt index 53aaf7227..56050ac21 100644 --- a/views/authentication/provider_edit.tt +++ b/views/authentication/provider_edit.tt @@ -66,14 +66,6 @@ END; %] - [% - IF field.name == "email"; - field_counter = field_counter + 2; - ELSE; - field_counter = field_counter + 1; - END; - END; - %] [% IF site.provider_fields.size > 0 %] [% END %] diff --git a/views/authentication/providers.tt b/views/authentication/providers.tt index bc98573e3..1d751cb8c 100644 --- a/views/authentication/providers.tt +++ b/views/authentication/providers.tt @@ -69,5 +69,4 @@ [%- INCLUDE wizard/provider_add.tt endpoint="/api/authentication_providers" modalId="providerModal"; - INCLUDE wizard/user_email.tt modalId="emailUsersModal"; -%] diff --git a/views/wizard/provider_add.tt b/views/wizard/provider_add.tt index 8292792ae..3018bd34a 100644 --- a/views/wizard/provider_add.tt +++ b/views/wizard/provider_add.tt @@ -28,7 +28,6 @@ + [% UNLESS site.hide_account_request %]