From c8447e9f4b7c17ab0e04af34dbd5583e78b23677 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Thu, 29 Jan 2015 17:33:12 +0000 Subject: Bug 1045145: backport upstream bug 726696 to bmo/4.2 to allow use of api keys for authentication --- Bugzilla/Auth.pm | 2 +- Bugzilla/Auth/Login/APIKey.pm | 52 +++++++ Bugzilla/Auth/Login/Cookie.pm | 17 ++- Bugzilla/DB/Schema.pm | 22 ++- Bugzilla/Install/DB.pm | 21 +++ Bugzilla/Template.pm | 6 + Bugzilla/Token.pm | 34 ++++- Bugzilla/User/APIKey.pm | 154 +++++++++++++++++++++ Bugzilla/WebService.pm | 39 +++++- Bugzilla/WebService/Constants.pm | 3 + Bugzilla/WebService/User.pm | 22 ++- Bugzilla/WebService/Util.pm | 5 + js/bug.js | 10 +- js/comment-tagging.js | 8 +- js/field.js | 2 + skins/standard/global.css | 32 +++++ template/en/default/account/prefs/apikey.html.tmpl | 86 ++++++++++++ template/en/default/account/prefs/prefs.html.tmpl | 5 +- template/en/default/email/new-api-key.txt.tmpl | 35 +++++ template/en/default/global/header.html.tmpl | 3 + template/en/default/global/user-error.html.tmpl | 14 ++ userprefs.cgi | 60 ++++++++ 22 files changed, 609 insertions(+), 23 deletions(-) create mode 100644 Bugzilla/Auth/Login/APIKey.pm create mode 100644 Bugzilla/User/APIKey.pm create mode 100644 template/en/default/account/prefs/apikey.html.tmpl create mode 100644 template/en/default/email/new-api-key.txt.tmpl diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index 9f4fb8fa3..e9bd214fd 100644 --- a/Bugzilla/Auth.pm +++ b/Bugzilla/Auth.pm @@ -45,7 +45,7 @@ sub new { my $self = fields::new($class); $params ||= {}; - $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie'; + $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie,APIKey'; $params->{Verify} ||= Bugzilla->params->{'user_verify_class'}; $self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login}); diff --git a/Bugzilla/Auth/Login/APIKey.pm b/Bugzilla/Auth/Login/APIKey.pm new file mode 100644 index 000000000..902ce4da7 --- /dev/null +++ b/Bugzilla/Auth/Login/APIKey.pm @@ -0,0 +1,52 @@ +# 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/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Auth::Login::APIKey; + +use 5.10.1; +use strict; + +use base qw(Bugzilla::Auth::Login); + +use Bugzilla::Constants; +use Bugzilla::User::APIKey; +use Bugzilla::Util; +use Bugzilla::Error; + +use constant requires_persistence => 0; +use constant requires_verification => 0; +use constant can_login => 0; +use constant can_logout => 0; + +# This method is only available to web services. An API key can never +# be used to authenticate a Web request. +sub get_login_info { + my ($self) = @_; + my $params = Bugzilla->input_params; + my ($user_id, $login_cookie); + + my $api_key_text = trim(delete $params->{'Bugzilla_api_key'}); + if (!i_am_webservice() || !$api_key_text) { + return { failure => AUTH_NODATA }; + } + + my $api_key = Bugzilla::User::APIKey->new({ name => $api_key_text }); + + if (!$api_key or $api_key->api_key ne $api_key_text) { + # The second part checks the correct capitalisation. Silly MySQL + ThrowUserError("api_key_not_valid"); + } + elsif ($api_key->revoked) { + ThrowUserError('api_key_revoked'); + } + + $api_key->update_last_used(); + + return { user_id => $api_key->user_id }; +} + +1; diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm index e3b86d384..11d9012b8 100644 --- a/Bugzilla/Auth/Login/Cookie.pm +++ b/Bugzilla/Auth/Login/Cookie.pm @@ -22,8 +22,9 @@ use base qw(Bugzilla::Auth::Login); use fields qw(_login_token); use Bugzilla::Constants; -use Bugzilla::Util; use Bugzilla::Error; +use Bugzilla::Token; +use Bugzilla::Util; use List::Util qw(first); @@ -57,6 +58,20 @@ sub get_login_info { @{$cgi->{'Bugzilla_cookie_list'}}; $user_id = $cookie->value if $cookie; } + + # If the call is for a web service, and an api token is provided, check + # it is valid. + if (i_am_webservice() && Bugzilla->input_params->{Bugzilla_api_token}) { + my $api_token = Bugzilla->input_params->{Bugzilla_api_token}; + my ($token_user_id, undef, undef, $token_type) + = Bugzilla::Token::GetTokenData($api_token); + if (!defined $token_type + || $token_type ne 'api_token' + || $user_id != $token_user_id) + { + ThrowUserError('auth_invalid_token', { token => $api_token }); + } + } } # If no cookies were provided, we also look for a login token diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm index 84989fa13..0f57f5a11 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -1178,7 +1178,7 @@ use constant ABSTRACT_SCHEMA => { issuedate => {TYPE => 'DATETIME', NOTNULL => 1} , token => {TYPE => 'varchar(16)', NOTNULL => 1, PRIMARYKEY => 1}, - tokentype => {TYPE => 'varchar(8)', NOTNULL => 1} , + tokentype => {TYPE => 'varchar(16)', NOTNULL => 1} , eventdata => {TYPE => 'TINYTEXT'}, ], INDEXES => [ @@ -1746,6 +1746,26 @@ use constant ABSTRACT_SCHEMA => { bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'], ], }, + + user_api_keys => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, + PRIMARYKEY => 1}, + user_id => {TYPE => 'INT3', NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'CASCADE'}}, + api_key => {TYPE => 'VARCHAR(40)', NOTNULL => 1}, + description => {TYPE => 'VARCHAR(255)'}, + revoked => {TYPE => 'BOOLEAN', NOTNULL => 1, + DEFAULT => 'FALSE'}, + last_used => {TYPE => 'DATETIME'}, + ], + INDEXES => [ + user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, + user_api_keys_user_id_idx => ['user_id'], + ], + }, }; # Foreign Keys are added in Bugzilla::DB::bz_add_field_tables diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm index 040be6630..beac0ae5a 100644 --- a/Bugzilla/Install/DB.pm +++ b/Bugzilla/Install/DB.pm @@ -711,6 +711,13 @@ sub update_table_definitions { 'bug_user_last_visit_last_visit_ts_idx', ['last_visit_ts']); + # 2014-07-14 sgreen@redhat.com - Bug 726696 + $dbh->bz_alter_column('tokens', 'tokentype', + {TYPE => 'varchar(16)', NOTNULL => 1}); + + # 2014-07-27 LpSolit@gmail.com - Bug 1044561 + _fix_user_api_keys_indexes(); + # 2014-10-?? dkl@mozilla.com - Bug 1062940 $dbh->bz_alter_column('bugs', 'alias', { TYPE => 'varchar(40)' }); @@ -3799,6 +3806,20 @@ sub _fix_flagclusions_indexes { } } +sub _fix_user_api_keys_indexes { + my $dbh = Bugzilla->dbh; + + if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_key')) { + $dbh->bz_drop_index('user_api_keys', 'user_api_keys_key'); + $dbh->bz_add_index('user_api_keys', 'user_api_keys_api_key_idx', + { FIELDS => ['api_key'], TYPE => 'UNIQUE' }); + } + if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_user_id')) { + $dbh->bz_drop_index('user_api_keys', 'user_api_keys_user_id'); + $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_idx', ['user_id']); + } +} + 1; __END__ diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index be84bc66a..9f007bb6d 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -1038,6 +1038,12 @@ sub create { # Allow templates to generate a token themselves. 'issue_hash_token' => \&Bugzilla::Token::issue_hash_token, + 'get_api_token' => sub { + return '' unless Bugzilla->user->id; + my $cache = Bugzilla->request_cache; + return $cache->{api_token} //= issue_api_token(); + }, + # A way for all templates to get at Field data, cached. 'bug_fields' => sub { my $cache = Bugzilla->request_cache; diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm index 24df470ac..769cb8800 100644 --- a/Bugzilla/Token.pm +++ b/Bugzilla/Token.pm @@ -43,13 +43,28 @@ use Digest::MD5 qw(md5_hex); use base qw(Exporter); -@Bugzilla::Token::EXPORT = qw(issue_session_token check_token_data delete_token +@Bugzilla::Token::EXPORT = qw(issue_api_token issue_session_token + check_token_data delete_token issue_hash_token check_hash_token); ################################################################################ # Public Functions ################################################################################ +# Create a token used for internal API authentication +sub issue_api_token { + # Generates a random token, adds it to the tokens table if one does not + # already exist, and returns the token to the caller. + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my ($token) = $dbh->selectrow_array(" + SELECT token FROM tokens + WHERE userid = ? AND tokentype = 'api_token' + AND (" . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') . ") > NOW()", + undef, $user->id); + return $token // _create_token($user->id, 'api_token', ''); +} + # Creates and sends a token to create a new user account. # It assumes that the login has the correct format and is not already in use. sub issue_new_user_account_token { @@ -233,10 +248,9 @@ sub check_hash_token { sub CleanTokenTable { my $dbh = Bugzilla->dbh; - $dbh->do('DELETE FROM tokens - WHERE ' . $dbh->sql_to_days('NOW()') . ' - ' . - $dbh->sql_to_days('issuedate') . ' >= ?', - undef, MAX_TOKEN_AGE); + $dbh->do("DELETE FROM tokens WHERE " . + $dbh->sql_date_math('issuedate', '+', '?', 'HOUR') . " <= NOW()", + undef, MAX_TOKEN_AGE * 24); } sub GenerateUniqueToken { @@ -354,7 +368,7 @@ sub GetTokenData { trick_taint($token); my @token_data = $dbh->selectrow_array( - "SELECT token, userid, " . $dbh->sql_date_format('issuedate') . ", eventdata + "SELECT token, userid, " . $dbh->sql_date_format('issuedate') . ", eventdata, tokentype FROM tokens WHERE token = ?", undef, $token); @@ -486,6 +500,14 @@ Bugzilla::Token - Provides different routines to manage tokens. =over +=item C + + Description: Creates a token that can be used for API calls on the web page. + + Params: None. + + Returns: The token. + =item C Description: Creates and sends a token per email to the email address diff --git a/Bugzilla/User/APIKey.pm b/Bugzilla/User/APIKey.pm new file mode 100644 index 000000000..75a4a6beb --- /dev/null +++ b/Bugzilla/User/APIKey.pm @@ -0,0 +1,154 @@ +# 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/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::User::APIKey; + +use 5.10.1; +use strict; + +use parent qw(Bugzilla::Object); + +use Bugzilla::User; +use Bugzilla::Util qw(generate_random_password trim); + +##################################################################### +# Overriden Constants that are used as methods +##################################################################### + +use constant DB_TABLE => 'user_api_keys'; +use constant DB_COLUMNS => qw( + id + user_id + api_key + description + revoked + last_used +); + +use constant UPDATE_COLUMNS => qw(description revoked last_used); +use constant VALIDATORS => { + api_key => \&_check_api_key, + description => \&_check_description, + revoked => \&Bugzilla::Object::check_boolean, +}; +use constant LIST_ORDER => 'id'; +use constant NAME_FIELD => 'api_key'; + +# turn off auditing and exclude these objects from memcached +use constant { AUDIT_CREATES => 0, + AUDIT_UPDATES => 0, + AUDIT_REMOVES => 0, + USE_MEMCACHED => 0 }; + +# Accessors +sub id { return $_[0]->{id} } +sub user_id { return $_[0]->{user_id} } +sub api_key { return $_[0]->{api_key} } +sub description { return $_[0]->{description} } +sub revoked { return $_[0]->{revoked} } +sub last_used { return $_[0]->{last_used} } + +# Helpers +sub user { + my $self = shift; + $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1}); + return $self->{user}; +} + +sub update_last_used { + my $self = shift; + my $timestamp = shift + || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $self->set('last_used', $timestamp); + $self->update; +} + +# Setters +sub set_description { $_[0]->set('description', $_[1]); } +sub set_revoked { $_[0]->set('revoked', $_[1]); } + +# Validators +sub _check_api_key { return generate_random_password(40); } +sub _check_description { return trim($_[1]) || ''; } +1; + +__END__ + +=head1 NAME + +Bugzilla::User::APIKey - Model for an api key belonging to a user. + +=head1 SYNOPSIS + + use Bugzilla::User::APIKey; + + my $api_key = Bugzilla::User::APIKey->new($id); + my $api_key = Bugzilla::User::APIKey->new({ name => $api_key }); + + # Class Functions + $user_api_key = Bugzilla::User::APIKey->create({ + description => $description, + }); + +=head1 DESCRIPTION + +This package handles Bugzilla User::APIKey. + +C is an implementation of L, and +thus provides all the methods of L in addition to the methods +listed below. + +=head1 METHODS + +=head2 Accessor Methods + +=over + +=item C + +The internal id of the api key. + +=item C + +The Bugzilla::User object that this api key belongs to. + +=item C + +The user id that this api key belongs to. + +=item C + +The API key, which is a random string. + +=item C + +An optional string that lets the user describe what a key is used for. +For example: "Dashboard key", "Application X key". + +=item C + +If true, this api key cannot be used. + +=item C + +The date that this key was last used. undef if never used. + +=item C + +Updates the last used value to the current timestamp. This is updated even +if the RPC call resulted in an error. It is not updated when the description +or the revoked flag is changed. + +=item C + +Sets the new description + +=item C + +Sets the revoked flag + +=back diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm index beed7b63f..b0649f7b3 100644 --- a/Bugzilla/WebService.pm +++ b/Bugzilla/WebService.pm @@ -145,16 +145,27 @@ how this is implemented for those frontends. =head1 LOGGING IN -There are various ways to log in: +Some methods do not require you to log in. An example of this is Bug.get. +However, authenticating yourself allows you to see non public information. For +example, a bug that is not publicly visible. + +There are two ways to authenticate yourself: =over =item C You can use L to log in as a Bugzilla -user. This issues standard HTTP cookies that you must then use in future -calls, so your client must be capable of receiving and transmitting -cookies. +user. This issues a token that you must then use in future calls. + +=item C + +B + +You can specify C as an argument to any WebService method, and +you will be logged in as that user if the key is correct, and has not been +revoked. You can set up an API key by using the 'API Key' tab in the +Preferences pages. =item C and C @@ -182,17 +193,31 @@ not expire. The C and C options are only used when you have also specified C and -C. +C. This value will be deprecated in the release +after Bugzilla 5.0 and you will be required to pass the Bugzilla_login +and Bugzilla_password for every call. Note that Bugzilla will return HTTP cookies along with the method response when you use these arguments (just like the C method above). -For REST, you may also use the C and C variable +For REST, you may also use the C and C variable names instead of C and C as a convenience. -=item B +=back + +There are also two deprecreated methods of authentications. This will be +removed in the version after Bugzilla 5.0. + +=over + +=item C + +You can use L to log in as a Bugzilla +user. This issues a token that you must then use in future calls. + +=item B An error is now thrown if you pass invalid cookies or an invalid token. You will need to log in again to get new cookies or a new token. Previous diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index 83cae251b..34981c565 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -149,6 +149,9 @@ use constant WS_ERROR_CODE => { extern_id_conflict => -303, auth_failure => 304, password_current_too_short => 305, + api_key_not_valid => 306, + api_key_revoked => 306, + auth_invalid_token => 307, # Except, historically, AUTH_NODATA, which is 410. login_required => 410, diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index 2f38446b8..773a7cec3 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -439,9 +439,13 @@ where applicable. =head1 Logging In and Out +These method are now deprecated, and will be removed in the release after +Bugzilla 5.0. The correct way of use these REST and RPC calls is noted in +L + =head2 login -B +B =over @@ -504,11 +508,21 @@ A login or password parameter was not provided. =back +=item B + +=over + +=item C was added in Bugzilla B<4.4.3>. + +=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys. + +=back + =back =head2 logout -B +B =over @@ -526,7 +540,7 @@ Log out the user. Does nothing if there is no user logged in. =head2 valid_login -B +B =over @@ -564,6 +578,8 @@ for the provided username. =item Added in Bugzilla B<5.0>. +=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys. + =back =back diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index c21bcd344..44bfb1f70 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -279,6 +279,11 @@ sub fix_credentials { $params->{'Bugzilla_login'} = delete $params->{'login'}; $params->{'Bugzilla_password'} = delete $params->{'password'}; } + # Allow user to pass api_key=12345678 as a convenience which becomes + # "Bugzilla_api_key" which is what the auth code looks for. + if (exists $params->{api_key}) { + $params->{Bugzilla_api_key} = delete $params->{api_key}; + } # Allow user to pass token=12345678 as a convenience which becomes # "Bugzilla_token" which is what the auth code looks for. if (exists $params->{'token'}) { diff --git a/js/bug.js b/js/bug.js index 97a330af1..1089bce99 100644 --- a/js/bug.js +++ b/js/bug.js @@ -33,6 +33,7 @@ YAHOO.bugzilla.dupTable = { method : "Bug.possible_duplicates", id : YAHOO.bugzilla.dupTable.counter, params : { + Bugzilla_api_token: BUGZILLA.api_token, product : product_name, summary : summary_field.value, limit : 7, @@ -139,7 +140,10 @@ YAHOO.bugzilla.dupTable = { var args = JSON.stringify({ version: "1.1", method: 'BugUserLastVisit.update', - params: { ids: bug_ids }, + params: { + Bugzilla_api_token: BUGZILLA.api_token, + ids: bug_ids + } }); var callbacks = { failure: function(res) { @@ -158,7 +162,9 @@ YAHOO.bugzilla.dupTable = { var args = JSON.stringify({ version: "1.1", method: 'BugUserLastVisit.get', - params: { }, + params: { + Bugzilla_api_token: BUGZILLA.api_token + } }); var callbacks = { success: function(res) { done(JSON.parse(res.responseText)) }, diff --git a/js/comment-tagging.js b/js/comment-tagging.js index c110eb00e..3b254cfa8 100644 --- a/js/comment-tagging.js +++ b/js/comment-tagging.js @@ -54,7 +54,11 @@ YAHOO.bugzilla.commentTagging = { return YAHOO.lang.JSON.stringify({ method : "Bug.search_comment_tags", id : YAHOO.bugzilla.commentTagging.counter, - params : [ { query : query, limit : 10 } ] + params : { + Bugzilla_api_token: BUGZILLA.api_token, + query : query, + limit : 10 + } }); }; ac.minQueryLength = this.min_len; @@ -340,6 +344,7 @@ YAHOO.bugzilla.commentTagging = { version: "1.1", method: 'Bug.comments', params: { + Bugzilla_api_token: BUGZILLA.api_token, comment_ids: [ comment_id ], include_fields: [ 'tags' ] } @@ -372,6 +377,7 @@ YAHOO.bugzilla.commentTagging = { version: "1.1", method: 'Bug.update_comment_tags', params: { + Bugzilla_api_token: BUGZILLA.api_token, comment_id: comment_id, add: add, remove: remove diff --git a/js/field.js b/js/field.js index 286390ed1..b35cfe782 100644 --- a/js/field.js +++ b/js/field.js @@ -709,6 +709,7 @@ YAHOO.bugzilla.userAutocomplete = { method : "User.get", id : YAHOO.bugzilla.userAutocomplete.counter, params : [ { + Bugzilla_api_token: BUGZILLA.api_token, match : [ decodeURIComponent(enteredText) ], include_fields : [ "name", "real_name" ] } ] @@ -917,6 +918,7 @@ function show_comment_preview(bug_id) { version: "1.1", method: 'Bug.render_comment', params: { + Bugzilla_api_token: BUGZILLA.api_token, id: bug_id, text: comment.value } diff --git a/skins/standard/global.css b/skins/standard/global.css index 3a61dae83..e799f90ee 100644 --- a/skins/standard/global.css +++ b/skins/standard/global.css @@ -380,6 +380,38 @@ table#flags td { text-align: left; } +#email_prefs, #saved_search_prefs, #shared_search_prefs, +#bug_activity { + border: 1px solid black; + border-collapse: collapse; +} + +#email_prefs th, +#shared_search_prefs th, +#saved_search_prefs th { + text-align: center; +} + +#email_prefs th, #email_prefs td, +#shared_search_prefs th, #shared_search_prefs td, +#saved_search_prefs th, #saved_search_prefs td, +#bug_activity td { + border: 1px solid; + padding: 0.3em; +} + +#email_prefs th.role_header { + width: 10%; +} + +.column_header { + background-color: #66f; +} + +.column_header th { + text-align: center; +} + .flag_select { min-width: 3em; } diff --git a/template/en/default/account/prefs/apikey.html.tmpl b/template/en/default/account/prefs/apikey.html.tmpl new file mode 100644 index 000000000..ff9ed697a --- /dev/null +++ b/template/en/default/account/prefs/apikey.html.tmpl @@ -0,0 +1,86 @@ +[%# 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/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[%# INTERFACE: + # api_keys: array. Array of api keys this user has. + # any_revoked: boolean. True is any keys have been revoked. + #%] + +

+ API keys are used to authenticate WebService API calls. You can create more than + one API key if required. Each API key has an optional description which can help + you record what each key is used for. Documentation on how to log in is available from + + here. +

+ +

Existing API keys

+ +

You can update the description, and revoke or unrevoke existing API keys +here.

+ + + + + + + + + + [% FOREACH api_key IN api_keys %] + + + + [% IF api_key.last_used %] + + [% ELSE %] + + [% END %] + + + [% END %] + [% UNLESS api_keys.size %] + + [% END %] +
API keyDescription (optional)Last usedRevoked
[% api_key.api_key FILTER html %] + + [% api_key.last_used FILTER time %]never used + +
You don't have any API keys.
+ +[% IF any_revoked %] + Hide Revoked Keys + [%# Show the link if the browser supports JS %] + +[% END %] + +

New API key

+ +

You can generate a new API key by ticking the check box below and optionally +providing a description for the API key. The API key will be randomly +generated for you.

+ +

+ + + +

+ diff --git a/template/en/default/account/prefs/prefs.html.tmpl b/template/en/default/account/prefs/prefs.html.tmpl index 2e8b561de..65649b814 100644 --- a/template/en/default/account/prefs/prefs.html.tmpl +++ b/template/en/default/account/prefs/prefs.html.tmpl @@ -40,7 +40,7 @@ title = "User Preferences" subheader = filtered_login style_urls = ['skins/standard/admin.css'] - javascript_urls = ['js/util.js', 'js/field.js'] + javascript_urls = ['js/util.js', 'js/field.js', 'js/TUI.js'] doc_section = "userpreferences.html" yui = ['autocomplete'] %] @@ -53,6 +53,9 @@ link => "userprefs.cgi?tab=saved-searches", saveable => "1" }, { name => "account", label => "Account Information", link => "userprefs.cgi?tab=account", saveable => "1" }, + { name => "apikey", label => "API Keys", + link => "userprefs.cgi?tab=apikey", saveable => "1", + doc_section => "using.html#apikey" }, { name => "permissions", label => "Permissions", link => "userprefs.cgi?tab=permissions", saveable => "0" } ] %] diff --git a/template/en/default/email/new-api-key.txt.tmpl b/template/en/default/email/new-api-key.txt.tmpl new file mode 100644 index 000000000..5dc068b05 --- /dev/null +++ b/template/en/default/email/new-api-key.txt.tmpl @@ -0,0 +1,35 @@ +[%# 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/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[%# INTERFACE: + # user: The Bugzilla::User object of the user being created + # new_key: The API key created + #%] + +[% PROCESS global/variables.none.tmpl %] + +From: [% Param('mailfrom') %] +To: [% user.email %] +Subject: [% terms.Bugzilla %]: New API key created +X-Bugzilla-Type: admin + +[This e-mail has been automatically generated] + +A new [% terms.Bugzilla %] API key[% IF new_key.description %], with the +description '[% new_key.description %]'[% END %] has been created. You can view +or update the key at the following URL: + +[%+ urlbase %]userprefs.cgi?tab=apikey + +IMPORTANT: If you did not request a new key, your [% terms.Bugzilla %] account +may have been compromised. In this case, please disable the key at the above +URL, and change your password immediately. + +For security reasons, we have not included your new key in this e-mail. + +If you have any issues regarding your account, please contact [% Param('maintainer') %]. diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl index 4ea41ebbd..3151a96dd 100644 --- a/template/en/default/global/header.html.tmpl +++ b/template/en/default/global/header.html.tmpl @@ -226,6 +226,9 @@ version_required: 'You must select a Version for this [% terms.bug %].' } + [% IF javascript_urls.containsany(['js/bug.js', 'js/field.js', 'js/comment-tagging.js']) %] + , api_token: '[% get_api_token FILTER js FILTER html %]' + [% END %] }; [% IF NOT no_yui %] diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl index 3146d4a90..c36ae2c4c 100644 --- a/template/en/default/global/user-error.html.tmpl +++ b/template/en/default/global/user-error.html.tmpl @@ -109,6 +109,15 @@ [% terms.Bug %] aliases cannot be longer than 20 characters. Please choose a shorter alias. + [% ELSIF error == "api_key_not_valid" %] + [% title = "Invalid API key" %] + The API key you specified is invalid. Please check that you typed it + correctly. + + [% ELSIF error == "api_key_revoked" %] + [% title = "Invalid API key" %] + The API key you specified has been revoked by the user that created it. + [% ELSIF error == "attachment_bug_id_mismatch" %] [% title = "Invalid Attachments" %] You tried to perform an action on attachments from different [% terms.bugs %]. @@ -235,6 +244,11 @@ [% Hook.process("auth_failure") %] + [% ELSIF error == "auth_invalid_token" %] + [% title = 'A token error occurred' %] + The token is not valid. It could be because you loaded this page more than + [% constants.MAX_TOKEN_AGE FILTER html %] days ago. + [% ELSIF error == "attachment_deletion_disabled" %] [% title = "Attachment Deletion Disabled" %] Attachment deletion is disabled on this installation. diff --git a/userprefs.cgi b/userprefs.cgi index d33de74ad..1764bb2dd 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -29,11 +29,13 @@ use lib qw(. lib); use Bugzilla; use Bugzilla::BugMail; use Bugzilla::Constants; +use Bugzilla::Mailer; use Bugzilla::Search; use Bugzilla::Util; use Bugzilla::Error; use Bugzilla::User; use Bugzilla::User::Setting qw(clear_settings_cache); +use Bugzilla::User::APIKey; use Bugzilla::Token; my $template = Bugzilla->template; @@ -520,6 +522,59 @@ sub SaveSavedSearches { } +sub DoApiKey { + my $user = Bugzilla->user; + + my $api_keys = Bugzilla::User::APIKey->match({ user_id => $user->id }); + $vars->{api_keys} = $api_keys; + $vars->{any_revoked} = grep { $_->revoked } @$api_keys; +} + +sub SaveApiKey { + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # Do it in a transaction. + $dbh->bz_start_transaction; + + # Update any existing keys + my $api_keys = Bugzilla::User::APIKey->match({ user_id => $user->id }); + foreach my $api_key (@$api_keys) { + my $description = $cgi->param('description_' . $api_key->id); + my $revoked = $cgi->param('revoked_' . $api_key->id); + + if ($description ne $api_key->description + || $revoked != $api_key->revoked) + { + $api_key->set_all({ + description => $description, + revoked => $revoked, + }); + $api_key->update(); + } + } + + # Create a new API key if requested. + if ($cgi->param('new_key')) { + $vars->{new_key} = Bugzilla::User::APIKey->create({ + user_id => $user->id, + description => scalar $cgi->param('new_description'), + }); + + # As a security precaution, we always sent out an e-mail when + # an API key is created + my $template = Bugzilla->template_inner($user->setting('lang')); + my $message; + $template->process('email/new-api-key.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); + } + + $dbh->bz_commit_transaction; +} + ############################################################################### # Live code (not subroutine definitions) starts here ############################################################################### @@ -589,6 +644,11 @@ SWITCH: for ($current_tab_name) { DoSavedSearches(); last SWITCH; }; + /^apikey$/ && do { + SaveApiKey() if $save_changes; + DoApiKey(); + last SWITCH; + }; ThrowUserError("unknown_tab", { current_tab_name => $current_tab_name }); -- cgit v1.2.3-24-g4f1b