Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
cd77535
Show the authentication providers in a table
timlegge Oct 24, 2024
302000f
Working create, update delete authentication providers
timlegge Nov 25, 2024
1bd4e58
Set permissions based on SAML attributes
timlegge Nov 26, 2024
188a6c9
Update the groups from the SAML2 assertion
timlegge Nov 27, 2024
613f762
Allow SAML response to set the groups and permissions
Dec 9, 2024
7e907f4
Cleanup and review the previous changes
timlegge Dec 7, 2024
bfd21ff
Update the javascript for the provider modals
Jan 2, 2025
243a70f
UX and DB changes to associate users to a authentication provider
Dec 8, 2024
2adc806
Only allow user to login via SAML if the user provider matches auth id
Dec 9, 2024
7f15020
Add provider match error
Dec 9, 2024
eebf8a5
Support Encrypted Assertions
Dec 9, 2024
411e68d
Enable login via saml login page
Dec 10, 2024
fb732ce
Prevent deletion of built in authentication provider
Dec 10, 2024
00c1820
Create metadata xml for a provider
Dec 11, 2024
bc5d2b8
Fix the fields for enabled and type
Dec 11, 2024
63e05e5
Implement IdP initiated login support
Dec 12, 2024
6d6f998
Replace hard coded values in metadata
Feb 15, 2025
2e2c3fb
Working generation and insertion of cert and key
Jan 3, 2025
c390aa7
Switch provider type to a small int - may switch back
Jan 3, 2025
a7d8e65
Really fix issue with things in after_create being overwritten
Jan 3, 2025
cb0535a
Really fix issue with things in after_create being overwritten
Jan 3, 2025
706bf64
Create a unique id for the relaystate - undecided if this is necessary
Jan 3, 2025
1fd562b
Never display the private key to the user
Jan 3, 2025
c130f82
Show correct value names for Provider Type
Feb 12, 2025
e6e4090
Do not allow login if SAML2 provider is disabled
Feb 12, 2025
45f862f
Fix issue updating user attributes from assertion
Feb 13, 2025
6737ae3
Get hostname from site
Feb 15, 2025
8c0e34a
fix acs url in AuthnRequest
Feb 16, 2025
6052204
Show error if CA Certificate validation fails
Feb 16, 2025
f9135e3
Set the new provider as disabled
Feb 16, 2025
a269f40
Redirect to the SAML login page if SAML failed
Feb 17, 2025
0c6d249
Fix panic on generating metadata
Feb 17, 2025
1b99c7e
Add a FIXME to document after_create issue
Feb 17, 2025
fcbd610
Redirect to the saml login page if the user does not exist
Feb 17, 2025
fd1dc87
Fix authentication provider missing on create user wizard
timlegge Aug 14, 2025
b040083
Fix layout of fields on authentication provider wizard
timlegge Aug 14, 2025
e5b52f0
Set the default Authentication provider to builtin
timlegge Aug 14, 2025
64127f8
Don't show editable fields for the builtin authenticator
timlegge Aug 14, 2025
30b036d
Stringify metadata issue installed Net::SAML2 version
timlegge Aug 14, 2025
486dd35
Fix wording issue on error message
timlegge Aug 14, 2025
76c74a6
Do not display fields the user cannot edit
timlegge Aug 15, 2025
6b1a91b
Make sso_xml and sso_url readonly on provider edit
timlegge Aug 15, 2025
47116ee
Set the default provide to builtin if undefined on user creation
timlegge Aug 16, 2025
cf1f5a5
Add support for nameid formats
timlegge Aug 17, 2025
1f079cd
The IdP might only accept HTTP-POST - JumpCloud
timlegge Aug 17, 2025
b18187b
Fix panic if no GADS Groups re included in the Groups attribute
timlegge Aug 22, 2025
ecd073c
Merge remote-tracking branch 'upstream/dev' into modal-changes
droberts-ctrlo Oct 6, 2025
8f1779e
Updates ready for merging
droberts-ctrlo Nov 5, 2025
de6b9ce
Update Authentication.pm
droberts-ctrlo Nov 5, 2025
48b88e0
Merge pull request #1 from droberts-ctrlo/modal-changes
timlegge Nov 10, 2025
a9feb3c
Improve the Redirect/POST for AuthnRequest
timlegge Nov 10, 2025
cdf0865
Change the IdP metadata to a file upload
timlegge Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 260 additions & 10 deletions lib/GADS.pm

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions lib/GADS/API.pm
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,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();
Expand Down Expand Up @@ -750,6 +754,7 @@ sub _post_add_user_account
freetext1 => $body->{freetext1},
freetext2 => $body->{freetext2},
title => $body->{title},
provider => $body->{provider} || 1,
organisation => $body->{organisation},
department_id => $body->{department_id},
team_id => $body->{team_id},
Expand All @@ -770,6 +775,51 @@ 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},
saml2_unique_id => $body->{saml2_unique_id},
saml2_nameid => $body->{saml2_nameid},
enabled => $body->{enabled},
);

# FIXME remove permissions?
$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;

Expand Down Expand Up @@ -1343,6 +1393,95 @@ 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 cacert sp_cert sp_key enabled error_messages/;
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]');
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]');

# 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'
? '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,
# 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%" },
];
}

$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
Expand Down
16 changes: 15 additions & 1 deletion lib/GADS/Audit.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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) = @_;

Expand Down
64 changes: 64 additions & 0 deletions lib/GADS/Authentication.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
=pod
GADS - Globally Accessible Data Store
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
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 <http://www.gnu.org/licenses/>.
=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',
#FIXME remove 'me.xml', 'me.sp_key',
],
order_by => 'name',
collapse => 1,
});
return $summary;
}

sub _build_all
{ my $self = shift;
my @authentication = $self->authentication_summary_rs->all;
\@authentication;
}

1;
16 changes: 16 additions & 0 deletions lib/GADS/DBIC.pm
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ sub insert
my $return = $self->next::method(@_);
$self->after_create
if $self->can('after_create');
$self->_after_create_or_update(@_);
$guard->commit;
$return;
}
Expand All @@ -45,8 +46,12 @@ sub delete

sub update
{ my $self = shift;

$self->_validate(@_);
my $guard = $self->result_source->schema->txn_scope_guard;
$self->_before_update(@_);
$self->next::method(@_);
$guard->commit;
}

sub _validate
Expand All @@ -68,5 +73,16 @@ sub _before_create
if $self->can('before_create');
};

sub _before_update
{ my $self = shift;
$self->before_update
if $self->can('before_update');
};

sub _after_create_or_update
{ my $self = shift;
$self->after_create_or_update
if $self->can('after_create_or_update');
};

1;
Loading