Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
8c9eca8
Show the authentication providers in a table
timlegge Oct 24, 2024
91a7a3c
Working create, update delete authentication providers
timlegge Nov 25, 2024
8bb2d20
Set permissions based on SAML attributes
timlegge Nov 26, 2024
06a9e01
Update the groups from the SAML2 assertion
timlegge Nov 27, 2024
078da37
Allow SAML response to set the groups and permissions
Dec 9, 2024
81ecfe4
Cleanup and review the previous changes
timlegge Dec 7, 2024
fe0f139
Update the javascript for the provider modals
Jan 2, 2025
8f21ec0
UX and DB changes to associate users to a authentication provider
Dec 8, 2024
2b22ae0
Only allow user to login via SAML if the user provider matches auth id
Dec 9, 2024
16650b9
Add provider match error
Dec 9, 2024
d1d7ee5
Support Encrypted Assertions
Dec 9, 2024
af1d144
Enable login via saml login page
Dec 10, 2024
3bc141a
Prevent deletion of built in authentication provider
Dec 10, 2024
2bf77b9
Create metadata xml for a provider
Dec 11, 2024
3f38752
Fix the fields for enabled and type
Dec 11, 2024
2c4e5f2
Implement IdP initiated login support
Dec 12, 2024
b7276aa
Replace hard coded values in metadata
Feb 15, 2025
4b1b4b2
Working generation and insertion of cert and key
Jan 3, 2025
668fc65
Switch provider type to a small int - may switch back
Jan 3, 2025
60a5e09
Really fix issue with things in after_create being overwritten
Jan 3, 2025
d028e0b
Really fix issue with things in after_create being overwritten
Jan 3, 2025
5edbb8d
Create a unique id for the relaystate - undecided if this is necessary
Jan 3, 2025
280a050
Never display the private key to the user
Jan 3, 2025
cf7393e
Show correct value names for Provider Type
Feb 12, 2025
41bfcba
Do not allow login if SAML2 provider is disabled
Feb 12, 2025
b47df59
Fix issue updating user attributes from assertion
Feb 13, 2025
3816fc9
Get hostname from site
Feb 15, 2025
04b1a2f
fix acs url in AuthnRequest
Feb 16, 2025
13832dc
Show error if CA Certificate validation fails
Feb 16, 2025
fbfb553
Set the new provider as disabled
Feb 16, 2025
a9710d2
Redirect to the SAML login page if SAML failed
Feb 17, 2025
3f87fa0
Fix panic on generating metadata
Feb 17, 2025
12fbdf0
Add a FIXME to document after_create issue
Feb 17, 2025
145fa99
Redirect to the saml login page if the user does not exist
Feb 17, 2025
b94a48d
Fix authentication provider missing on create user wizard
timlegge Aug 14, 2025
31f9141
Fix layout of fields on authentication provider wizard
timlegge Aug 14, 2025
37055d6
Set the default Authentication provider to builtin
timlegge Aug 14, 2025
72aa6fc
Don't show editable fields for the builtin authenticator
timlegge Aug 14, 2025
9416071
Stringify metadata issue installed Net::SAML2 version
timlegge Aug 14, 2025
98a1358
Fix wording issue on error message
timlegge Aug 14, 2025
2a92ba2
Do not display fields the user cannot edit
timlegge Aug 15, 2025
5f03b31
Make sso_xml and sso_url readonly on provider edit
timlegge Aug 15, 2025
af90d79
Set the default provide to builtin if undefined on user creation
timlegge Aug 16, 2025
e213194
Add support for nameid formats
timlegge Aug 17, 2025
ae1e1c0
The IdP might only accept HTTP-POST - JumpCloud
timlegge Aug 17, 2025
3f03027
Fix panic if no GADS Groups re included in the Groups attribute
timlegge Aug 22, 2025
d05b292
Updates ready for merging
droberts-ctrlo Nov 5, 2025
f42d638
Update Authentication.pm
droberts-ctrlo Nov 5, 2025
5fa7362
Improve the Redirect/POST for AuthnRequest
timlegge Nov 10, 2025
44d441a
Change the IdP metadata to a file upload
timlegge Nov 11, 2025
a13665a
Added migration to 112 schema
droberts-ctrlo Sep 1, 2026
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 @@ -1346,6 +1396,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
Loading