diff options
author | Jeff Fearn <jfearn@redhat.com> | 2020-05-07 11:41:39 +1000 |
---|---|---|
committer | Jeff Fearn <jfearn@redhat.com> | 2020-05-07 11:41:39 +1000 |
commit | 353af77714fd29aeab330255808636a9088eecc2 (patch) | |
tree | ecaadcb99519d2b53a96e7dfe9661b498156827a /extensions/AuthJWT | |
parent | Bumped version to 5.0.4 (diff) | |
download | bugzilla-353af77714fd29aeab330255808636a9088eecc2.tar.gz bugzilla-353af77714fd29aeab330255808636a9088eecc2.tar.bz2 bugzilla-353af77714fd29aeab330255808636a9088eecc2.zip |
Bug 478886 - Open Source Red Hat BugzillaRelease-5.0.4-rh44
Import Red Hat Bugzilla changes.
Diffstat (limited to 'extensions/AuthJWT')
-rw-r--r-- | extensions/AuthJWT/Config.pm | 18 | ||||
-rw-r--r-- | extensions/AuthJWT/Extension.pm | 140 | ||||
-rw-r--r-- | extensions/AuthJWT/lib/Login.pm | 130 | ||||
-rw-r--r-- | extensions/AuthJWT/lib/Source.pm | 178 | ||||
-rw-r--r-- | extensions/AuthJWT/lib/Util.pm | 47 | ||||
-rw-r--r-- | extensions/AuthJWT/template/en/default/authjwt/README | 16 | ||||
-rw-r--r-- | extensions/AuthJWT/template/en/default/hook/README | 5 | ||||
-rw-r--r-- | extensions/AuthJWT/template/en/default/hook/admin/admin-end_links_left.html.tmpl | 8 | ||||
-rw-r--r-- | extensions/AuthJWT/template/en/default/hook/global/user-error-errors.html.tmpl | 23 | ||||
-rw-r--r-- | extensions/AuthJWT/template/en/default/pages/authjwt/settings.html.tmpl | 110 |
10 files changed, 675 insertions, 0 deletions
diff --git a/extensions/AuthJWT/Config.pm b/extensions/AuthJWT/Config.pm new file mode 100644 index 000000000..f38ff40f1 --- /dev/null +++ b/extensions/AuthJWT/Config.pm @@ -0,0 +1,18 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package Bugzilla::Extension::AuthJWT; + +use 5.10.1; +use strict; +use warnings; + +use constant NAME => 'AuthJWT'; + +use constant REQUIRED_MODULES => + [{package => 'Crypt-JWT', module => 'Crypt::JWT', version => 0},]; + +use constant OPTIONAL_MODULES => []; + +__PACKAGE__->NAME; diff --git a/extensions/AuthJWT/Extension.pm b/extensions/AuthJWT/Extension.pm new file mode 100644 index 000000000..c8d6d2d45 --- /dev/null +++ b/extensions/AuthJWT/Extension.pm @@ -0,0 +1,140 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package Bugzilla::Extension::AuthJWT; + +use 5.10.1; +use strict; +use warnings; + +use parent qw(Bugzilla::Extension); + +# This code for this is in ../extensions/AuthJWT/lib/Util.pm +use Bugzilla::Extension::AuthJWT::Util; +use Bugzilla::Extension::AuthJWT::Source; + +our $VERSION = '0.01'; + +sub db_schema_abstract_schema { + my ($self, $args) = @_; + + my $schema = $args->{schema}; + + $schema->{authjwt_source} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + kid => {TYPE => 'LONGTEXT', NOTNULL => 1}, + cert => {TYPE => 'LONGTEXT', NOTNULL => 1}, + comment => {TYPE => 'LONGTEXT', NOTNULL => 1, DEFAULT => "''"}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [authjwt_source_kid_idx => {FIELDS => [qw(kid)], TYPE => 'UNIQUE'},], + }; + + # authjwt groups, specifies who is NOT allowed to use it! + $schema->{authjwt_groups} = { + FIELDS => [ + authjwt_source_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'authjwt_source', COLUMN => 'id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + authjwt_authjwt_groups_idx => + {FIELDS => [qw(authjwt_source_id group_id)], TYPE => 'UNIQUE'}, + ], + }; + + return; +} + +sub auth_login_methods { + my ($self, $args) = @_; + my $modules = $args->{'modules'}; + if (exists $modules->{'AuthJWT'}) { + $modules->{'AuthJWT'} = 'Bugzilla/Extension/AuthJWT/Login.pm'; + } + + return; +} + +sub page_before_template { + my ($self, $args) = @_; + + my $page = $args->{page_id}; + my $vars = $args->{vars}; + my $cgi = Bugzilla->cgi; + my $p = $cgi->Vars; + + # If we aren't viewing a AuthJWT page, we don't need to do anything + return unless ($page =~ m{^authjwt/}); + + if ($page eq 'authjwt/settings.html') { + my $action = $cgi->param('action') || ""; + my $params; + foreach my $key (keys(%$p)) { + next if ($key eq 'id'); + $params->{$key} = $p->{$key}; + } + delete($params->{action}); + + if ($action eq 'create') { + $params->{kid} = delete($params->{source_kid}); + my $groups = delete $params->{groups}; + my $source = Bugzilla::Extension::AuthJWT::Source->create($params); + $source->set_groups($groups); + $source->update(); + } + elsif ($action eq 'save') { + $params->{isactive} = 0 unless defined $params->{isactive}; + my $id = delete($params->{source_id}); + $params->{kid} = delete($params->{source_kid}); + my $groups = delete $params->{groups}; + my $source = Bugzilla::Extension::AuthJWT::Source->new($id); + $source->set_all($params); + $source->set_groups($groups); + $source->update(); + } + elsif ($action eq 'delete') { + my $id = delete($params->{source_id}); + my $source = Bugzilla::Extension::AuthJWT::Source->new($id); + $source->remove_from_db(); + } + + $vars->{'groups'} = [Bugzilla::Group->get_all]; + $args->{vars}->{sources} = Bugzilla::Extension::AuthJWT::Source->match(); + $args->{vars}->{'doc_section'} + = 'extensions/AuthJWT/index-admin.html#bugzilla-extension-authjwt-identity-providers'; + + } + else { + ThrowUserError('authjwt_no_such_page'); + } + + return; +} + +__PACKAGE__->NAME; + +__END__ + +=head1 Name + +Bugzilla::Extension::AuthJWT - JWT authentication support for Bugzilla + +=head1 Version + +Version 0.01 + +=head1 Description + +This module will allow you to set up your Bugzilla server as an SP and configure +multiple Sources to authenticate against. + diff --git a/extensions/AuthJWT/lib/Login.pm b/extensions/AuthJWT/lib/Login.pm new file mode 100644 index 000000000..66b225d1b --- /dev/null +++ b/extensions/AuthJWT/lib/Login.pm @@ -0,0 +1,130 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package Bugzilla::Extension::AuthJWT::Login; +use 5.10.1; +use strict; +use warnings; +use base qw(Bugzilla::Auth::Login); + +use Bugzilla; +use Bugzilla::Constants + qw(AUTH_NODATA AUTH_ERROR USAGE_MODE_BROWSER AUTH_NO_SUCH_USER AUTH_LOCKOUT AUTH_LOGINFAILED); +use Bugzilla::Util; + +use Bugzilla::Error; +use Bugzilla::Token; + +use Crypt::JWT qw(decode_jwt); +use Bugzilla::Extension::AuthJWT::Util qw(sources_kids user_can_use); +use Data::Dumper; +use File::Basename; + +use constant { + requires_verification => 0, + user_can_create_account => 0, + extern_id_used => 0, + can_change_email => 0, + requires_persistence => 0 +}; + +# This method is only available to web services. A JWT key can never +# be used to authenticate a Web request. +sub get_login_info { + my ($self) = @_; + my $params = Bugzilla->input_params; + my $cgi = Bugzilla->cgi; + + my ($jwt); + + if (!i_am_webservice()) { + return {failure => AUTH_NODATA}; + } + + # Only allow JWTs on JSONRPC + unless (basename($0) eq 'jsonrpc.cgi') { + return {failure => AUTH_NODATA}; + } + + if ($ENV{MOD_PERL}) { + + # use require as we may not be running in mod_perl mode. + require Apache2::RequestUtil; + require Apache2::Connection; + + my $req = Apache2::RequestUtil->request; + $jwt = $req->headers_in->{'Authorization'}; + } + else { + # You need to expose this header in Apache + # RewriteRule ^jsonrpc.cgi - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + $jwt = $ENV{HTTP_AUTHORIZATION}; + } + + my $origin = $cgi->http('Origin'); + + # If we aren't using a JWT, or there is no origin, bail out + if (!$jwt || !$origin || $origin eq '') { + return {failure => AUTH_NODATA}; + } + + # If we are an XHR and there is no JWT explode + if ("$origin/" ne Bugzilla->params->{'urlbase'} && !$jwt) { + return ({ + failure => AUTH_ERROR, + user_error => 'authjwt_invalid_xhr', + details => {origin => $origin} + }); + } + + $jwt =~ s/^Bearer\s*//; + + my $kid_keys = sources_kids(); + + my ($header, $token); + eval { + ($header, $token) + = decode_jwt(token => $jwt, kid_keys => $kid_keys, decode_header => 1); + }; + + if ($@ || !$header->{kid}) { + return ({ + failure => AUTH_ERROR, + user_error => 'authjwt_invalid_jwt', + details => {error => $@ // 'No JWT KID header detected'} + }); + } + +## BUGBUG some JWTs have a valid-for origin, should we validate $origin against that? + + my $field = 'email'; + my $val = $token->{$field}; + unless ($val && $val ne '') { + return ({ + failure => AUTH_ERROR, + user_error => 'authjwt_missing_mapfield', + details => {field => $field} + }); + } + + my $user; + $user = Bugzilla::User->new({name => $val}); + unless ($user) { + return {failure => AUTH_NO_SUCH_USER}; + } + + if ($user->account_is_locked_out) { + return {failure => AUTH_LOCKOUT, user => $user}; + } + + # enforce group restrictions + unless (user_can_use($header->{kid}, $user)) { + return {failure => AUTH_ERROR, user_error => 'authjwt_user_not_allowed'}; + } + + return {user_id => $user->id}; +} + +1; + diff --git a/extensions/AuthJWT/lib/Source.pm b/extensions/AuthJWT/lib/Source.pm new file mode 100644 index 000000000..3d82f4d68 --- /dev/null +++ b/extensions/AuthJWT/lib/Source.pm @@ -0,0 +1,178 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package Bugzilla::Extension::AuthJWT::Source; + +use strict; +use warnings; +use 5.10.1; +use Scalar::Util qw(blessed); + +use Bugzilla::Constants; +use Bugzilla::Field; +use Bugzilla::Util; +use Bugzilla::Error; + +use base qw(Bugzilla::Object); + +use Bugzilla::Extension::AuthJWT::Util; + +use constant DB_TABLE => 'authjwt_source'; +use constant NAME_FIELD => 'kid'; +use constant LIST_ORDER => 'kid'; + +use constant DB_COLUMNS => qw( + id + kid + cert + comment + isactive +); + +use constant UPDATE_COLUMNS => qw( + kid + cert + comment + isactive +); + +use constant VALIDATORS => { + kid => \&_check_cert, + cert => \&_check_cert, + comment => \&_check_cert, + isactive => \&Bugzilla::Object::check_boolean, +}; + +############################### +#### Validators #### +############################### + +sub _check_cert { + my ($invocant, $cert) = @_; + $cert = trim($cert); + return ($cert); +} + +############################### +#### Methods #### +############################### + +sub set_kid { $_[0]->set('kid', $_[1]); return; } +sub set_cert { $_[0]->set('cert', $_[1]); return; } +sub set_comment { $_[0]->set('comment', $_[1]); return; } +sub set_isactive { $_[0]->set('isactive', $_[1]); return; } + +# groups specify who is NOT allowed to use it! +sub set_groups { + my $self = shift; + my $group_ids = shift; + + my @groups; + if (ref $group_ids) { + @groups = @$group_ids; + } + else { + @groups = ($group_ids); + } + + my $dbh = Bugzilla->dbh; + + $dbh->do(q{DELETE FROM authjwt_groups WHERE authjwt_source_id = ?}, + undef, $self->id); + + my $sth = $dbh->prepare(q{INSERT INTO authjwt_groups VALUES (?, ?)}); + + foreach my $group_id (@groups) { + next unless $group_id && $group_id =~ /(\d+)/; + my $id = $1; + my $group = Bugzilla::Group->new($id); + $sth->execute($self->id, $id) if ($group); + } + + delete $self->{groups}; + + return; +} + +############################### +#### Accessors #### +############################### + +sub id { return ($_[0]->{id}); } +sub kid { return ($_[0]->{kid}); } +sub cert { return ($_[0]->{cert}); } +sub comment { return ($_[0]->{comment}); } +sub isactive { return ($_[0]->{isactive}); } + +sub groups { + my $self = shift; + + unless ($self->{groups}) { + my $dbh = Bugzilla->dbh; + + my $ids + = $dbh->selectcol_arrayref( + q{SELECT DISTINCT group_id FROM authjwt_groups WHERE authjwt_source_id = ?}, + undef, $self->id); + + $self->{groups} = $ids; + } + return ($self->{groups}); +} + +1; + +__END__ + +=head1 Description + +Bugzilla::Extension::AuthJWT::Source - A module for encapsulating Sources of JWTs. + +=head1 Fields + +=over 4 + +=item id + +The index for this Source in the database. + +=item kid + +The key ID to validate on. + +=item cert + +The public certificate to validate this kid. + +=back + +=head1 Accessors + +These methods allow you to get the specified field for the JWT Source. + +=over 4 + +=item id + +=item kid + +=item cert + +=item comment + +=back + +=head1 Methods + +These methods allow you to set the specified field for the JWT Source. + +=over 4 + +=item set_kid + +=item set_cert + +=item set comment + +=back diff --git a/extensions/AuthJWT/lib/Util.pm b/extensions/AuthJWT/lib/Util.pm new file mode 100644 index 000000000..0e613eb9d --- /dev/null +++ b/extensions/AuthJWT/lib/Util.pm @@ -0,0 +1,47 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package Bugzilla::Extension::AuthJWT::Util; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::Extension::AuthJWT::Source; +use JSON; +use Bugzilla::User; + +use parent qw(Exporter); +our @EXPORT = qw( + sources_kids + user_can_use +); + +# This file can be loaded by your extension via +# "use Bugzilla::Extension::AuthJWT::Util". You can put functions +# used by your extension in here. (Make sure you also list them in +# @EXPORT.) + +sub sources_kids { + my %sources; + + foreach my $src (@{Bugzilla::Extension::AuthJWT::Source->match()}) { + $sources{$src->kid} = $src->cert; + } + + my $sources = encode_json(\%sources); + return ($sources); +} + +sub user_can_use { + my ($kid, $user) = @_; + + my $can = 1; + my $source = Bugzilla::Extension::AuthJWT::Source->check({kid => $kid}); + $can = 0 if (any { $user->in_group_id($_->id) } @{$source->groups()}); + + return ($can); +} + +1; diff --git a/extensions/AuthJWT/template/en/default/authjwt/README b/extensions/AuthJWT/template/en/default/authjwt/README new file mode 100644 index 000000000..4e13121ed --- /dev/null +++ b/extensions/AuthJWT/template/en/default/authjwt/README @@ -0,0 +1,16 @@ +Normal templates go in this directory. You can load them in your +code like this: + +use Bugzilla::Error; +my $template = Bugzilla->template; +$template->process('authjwt/some-template.html.tmpl') + or ThrowTemplateError($template->error()); + +That would be how to load a file called <kbd>some-template.html.tmpl</kbd> that +was in this directory. + +Note that you have to be careful that the full path of your template +never conflicts with a template that exists in Bugzilla or in +another extension, or your template might override that template. That's why +we created this directory called 'authjwt' for you, so you +can put your templates in here to help avoid conflicts.
\ No newline at end of file diff --git a/extensions/AuthJWT/template/en/default/hook/README b/extensions/AuthJWT/template/en/default/hook/README new file mode 100644 index 000000000..e6c4add58 --- /dev/null +++ b/extensions/AuthJWT/template/en/default/hook/README @@ -0,0 +1,5 @@ +Template hooks go in this directory. Template hooks are called in normal +Bugzilla templates like [% Hook.process('some-hook') %]. +More information about them can be found in the documentation of +Bugzilla::Extension. (Do "perldoc Bugzilla::Extension" from the main +Bugzilla directory to see that documentation.)
\ No newline at end of file diff --git a/extensions/AuthJWT/template/en/default/hook/admin/admin-end_links_left.html.tmpl b/extensions/AuthJWT/template/en/default/hook/admin/admin-end_links_left.html.tmpl new file mode 100644 index 000000000..8a9311444 --- /dev/null +++ b/extensions/AuthJWT/template/en/default/hook/admin/admin-end_links_left.html.tmpl @@ -0,0 +1,8 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + #%] + +[% class = user.in_group('editcomponents') ? "" : "forbidden" %] +<dt class="[% class FILTER html %]"><a href="page.cgi?id=authjwt/settings.html">JWT Source Settings</a></dt> +<dd class="[% class FILTER html %]">Administration of AuthJWT Sources.</dd> diff --git a/extensions/AuthJWT/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/AuthJWT/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..267edb62e --- /dev/null +++ b/extensions/AuthJWT/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,23 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + #%] + +[% IF error == "authjwt_user_not_allowed" %] + [% title = "Permission denied" %] + The user is not permitted to use this JWT source. Usually due to + group restrictions. +[% ELSIF error == "authjwt_invalid_jwt" %] + [% title = "Invalid JWT" %] + An invalid JWT was provided. [% error FILTER html %] +[% ELSIF error == "authjwt_invalid_xhr" %] + [% title = "Invalid XHR" %] + ALERT An invalid XHR request was detected from [% origin FILTER html %] +[% ELSIF error == "authjwt_missing_mapfield" %] + [% title = "Missing map field" %] + The JWT was missing the expected mapping field '[% field FILTER html %]'. +[% ELSIF error == "authjwt_no_such_page" %] + [% title = "Invalid Page Selected" %] + The page you tried to load is not supported. +[% END %] + diff --git a/extensions/AuthJWT/template/en/default/pages/authjwt/settings.html.tmpl b/extensions/AuthJWT/template/en/default/pages/authjwt/settings.html.tmpl new file mode 100644 index 000000000..ad2bb8ec6 --- /dev/null +++ b/extensions/AuthJWT/template/en/default/pages/authjwt/settings.html.tmpl @@ -0,0 +1,110 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + #%] + +[%# INTERFACE: + # settings: array of product objects + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Edit AuthJWT Sources" + h1 = "This lets you add, edit, or delete Sources for JWTs" + style = "table {width: 100%;} table tr {vertical-align: top;} #source_kid {width: 32em}" +%] + +<h2>AuthJWT Sources</h2> + <table id="jwt_admin_table"> + <thead> + <tr> + <th>kid</th> + <th>Cert</th> + <th title="Adding groups here will prevent them from using this IDP">Exclude Groups</th> + <th>Comment</th> + <th>Active</th> + <th></th> + </tr> + </thead> + <tbody> + [% FOREACH source IN sources %] + <tr> +<form method="POST" action="page.cgi?id=authjwt/settings.html"> + <td> + <input type="hidden" name="source_id" value="[% source.id FILTER html %]"> + <input name="source_kid" id="source_kid" value="[% source.kid FILTER html %]"> + </td> + <td><textarea cols="64" rows="10" wrap="soft" name="cert" id="cert">[% source.cert FILTER html %]</textarea></td> + <td> + <select name="groups" id="groups" multiple="multiple"> + [% FOREACH group IN groups %] + <option value="[% group.id FILTER html %]" [% IF source.groups.contains(group.id) %] selected="selected"[% END %]> + [% group.name FILTER html %] + </option> + [% END %] + </select> + </td> + <td><input name="comment" id="comment" value="[% source.comment FILTER html %]"></td> + <td><input type="checkbox" name="isactive" id="isactive"[% ' checked="checked"' IF source.isactive %]></td> + <td><button type="submit" name="action" value="save">Save</button> + <button type="submit" name="action" value="delete">Delete</button></td> +</form> + </tr> + [% END %] + <tbody> + </table> + + +<h2>Create a new AuthJWT Source</h2> + <table> + <thead> + <tr> + <th>kid</th> + <th>Cert</th> + <th title="Adding groups here will prevent them from using this IDP">Exclude Groups</th> + <th>Comment</th> + <th>Active</th> + <th></th> + </tr> + </thead> + <tbody> + <form method="POST" action="page.cgi?id=authjwt/settings.html"> + <tr> + <td> + <input name="source_kid" id="source_kid"> + </td> + <td><textarea cols="64" rows="10" wrap="soft" name="cert" id="cert"></textarea></td> + <td> + <select name="groups" id="groups" multiple="multiple"> + [% FOREACH group IN groups %] + <option value="[% group.id FILTER html %]"> + [% group.name FILTER html %] + </option> + [% END %] + </select> + </td> + <td><input name="comment" id="comment"></td> + <td><input type="checkbox" name="isactive" id="isactive"></td> + <td><button type="submit" name="action" value="create">Create</button></td> + </tr> + </form> + <tbody> + </table> + +<script> +$(document).ready(function() { + $('#jwt_admin_table').DataTable({ + ordering: true, + stateSave: true, + pagingType: 'full_numbers', + aLengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, 'All']], + fixedHeader: true, +[% IF user.settings.def_table_size.value %] + iDisplayLength: [% user.settings.def_table_size.value FILTER html %], +[%- END %] + }); +}); +</script> + +[% PROCESS global/footer.html.tmpl %] |