diff options
author | Simon Green <sgreen@redhat.com> | 2014-07-27 18:47:21 +1000 |
---|---|---|
committer | Simon Green <sgreen@redhat.com> | 2014-07-27 18:47:21 +1000 |
commit | fd29ee56c4678749c00e7698ef245f7e2967ee10 (patch) | |
tree | 9d0696c9a89b8df8a6d46e2be6602a449b7354c3 | |
parent | Bump version post-release (diff) | |
download | bugzilla-fd29ee56c4678749c00e7698ef245f7e2967ee10.tar.gz bugzilla-fd29ee56c4678749c00e7698ef245f7e2967ee10.tar.bz2 bugzilla-fd29ee56c4678749c00e7698ef245f7e2967ee10.zip |
Bug 726696 - All authenticated WebServices methods should require username/pass, token or a valid API key for authentication
r=dkl, a=sgreen
-rw-r--r-- | Bugzilla/Auth.pm | 2 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/APIKey.pm | 52 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/Cookie.pm | 14 | ||||
-rw-r--r-- | Bugzilla/DB/Schema.pm | 22 | ||||
-rw-r--r-- | Bugzilla/Install/DB.pm | 4 | ||||
-rw-r--r-- | Bugzilla/Template.pm | 6 | ||||
-rw-r--r-- | Bugzilla/Token.pm | 18 | ||||
-rw-r--r-- | Bugzilla/User/APIKey.pm | 154 | ||||
-rw-r--r-- | Bugzilla/WebService.pm | 34 | ||||
-rw-r--r-- | Bugzilla/WebService/Constants.pm | 3 | ||||
-rw-r--r-- | Bugzilla/WebService/User.pm | 16 | ||||
-rw-r--r-- | Bugzilla/WebService/Util.pm | 5 | ||||
-rw-r--r-- | docs/en/rst/using.rst | 16 | ||||
-rw-r--r-- | js/bug.js | 10 | ||||
-rw-r--r-- | js/comment-tagging.js | 8 | ||||
-rw-r--r-- | js/field.js | 2 | ||||
-rw-r--r-- | template/en/default/account/prefs/apikey.html.tmpl | 84 | ||||
-rw-r--r-- | template/en/default/account/prefs/prefs.html.tmpl | 5 | ||||
-rw-r--r-- | template/en/default/email/new-api-key.txt.tmpl | 33 | ||||
-rw-r--r-- | template/en/default/global/header.html.tmpl | 3 | ||||
-rw-r--r-- | template/en/default/global/user-error.html.tmpl | 14 | ||||
-rwxr-xr-x | userprefs.cgi | 66 |
22 files changed, 553 insertions, 18 deletions
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index 42e4ee784..5d4c348da 100644 --- a/Bugzilla/Auth.pm +++ b/Bugzilla/Auth.pm @@ -30,7 +30,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 29e2310a0..9c18903b6 100644 --- a/Bugzilla/Auth/Login/Cookie.pm +++ b/Bugzilla/Auth/Login/Cookie.pm @@ -14,8 +14,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); @@ -49,6 +50,17 @@ 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 ($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 632ab639b..b175f1554 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -1177,7 +1177,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 => [ @@ -1733,6 +1733,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_key => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, + user_api_keys_user_id => {FIELDS => ['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 9e197a907..bbddf1620 100644 --- a/Bugzilla/Install/DB.pm +++ b/Bugzilla/Install/DB.pm @@ -719,6 +719,10 @@ 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}); + ################################################################ # New --TABLE-- changes should go *** A B O V E *** this point # ################################################################ diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 8fe50fa4f..b36249b2f 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -1001,6 +1001,12 @@ sub create { return $cookie ? issue_hash_token(['login_request', $cookie]) : ''; }, + '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 535263868..71f24183b 100644 --- a/Bugzilla/Token.pm +++ b/Bugzilla/Token.pm @@ -23,13 +23,21 @@ use Digest::SHA qw(hmac_sha256_base64); use parent 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, and returns + # the token to the caller. + return _create_token(Bugzilla->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 { @@ -466,6 +474,14 @@ Bugzilla::Token - Provides different routines to manage tokens. =over +=item C<issue_api_token($login_name)> + + Description: Creates a token that can be used for API calls on the web page. + + Params: None. + + Returns: The token. + =item C<issue_new_user_account_token($login_name)> 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<Bugzilla::User::APIKey> is an implementation of L<Bugzilla::Object>, and +thus provides all the methods of L<Bugzilla::Object> in addition to the methods +listed below. + +=head1 METHODS + +=head2 Accessor Methods + +=over + +=item C<id> + +The internal id of the api key. + +=item C<user> + +The Bugzilla::User object that this api key belongs to. + +=item C<user_id> + +The user id that this api key belongs to. + +=item C<api_key> + +The API key, which is a random string. + +=item C<description> + +An optional string that lets the user describe what a key is used for. +For example: "Dashboard key", "Application X key". + +=item C<revoked> + +If true, this api key cannot be used. + +=item C<last_used> + +The date that this key was last used. undef if never used. + +=item C<update_last_used> + +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<set_description> + +Sets the new description + +=item C<set_revoked> + +Sets the revoked flag + +=back diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm index 1dc04c1f6..d12d4dbac 100644 --- a/Bugzilla/WebService.pm +++ b/Bugzilla/WebService.pm @@ -134,14 +134,22 @@ 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<User.login> +=item C<Bugzilla_api_key> -You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla -user. This issues a token that you must then use in future calls. +B<Added in Bugzilla 5.0> + +You can specify C<Bugzilla_api_key> 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<Bugzilla_login> and C<Bugzilla_password> @@ -164,15 +172,29 @@ then your login will only be valid for your IP address. =back The C<Bugzilla_restrictlogin> option is only used when you have also -specified C<Bugzilla_login> and C<Bugzilla_password>. +specified C<Bugzilla_login> and C<Bugzilla_password>. 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. For REST, you may also use the C<login> and C<password> variable names instead of C<Bugzilla_login> and C<Bugzilla_password> as a convenience. You may also use C<token> instead of C<Bugzilla_token>. +=back + +There are also two deprecreated methods of authentications. This will be +removed in the version after Bugzilla 5.0. + +=over + +=item C<User.login> + +You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla +user. This issues a token that you must then use in future calls. + =item C<Bugzilla_token> -B<Added in Bugzilla 5.0> +B<Added in Bugzilla 4.4.3> You can specify C<Bugzilla_token> as argument to any WebService method, and you will be logged in as that user if the token is correct. This is diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index 2c21de15e..2fa1bdc28 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -142,6 +142,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 f05b2b247..32d6f28ec 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -432,9 +432,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<Bugzilla::WebService> + =head2 login -B<STABLE> +B<DEPRECATED> =over @@ -499,7 +503,9 @@ creates a login cookie. =item C<restrict_login> was added in Bugzilla B<5.0>. -=item C<token> was added in Bugzilla B<5.0>. +=item C<token> 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 @@ -507,7 +513,7 @@ creates a login cookie. =head2 logout -B<STABLE> +B<DEPRECATED> =over @@ -525,7 +531,7 @@ Log out the user. Does nothing if there is no user logged in. =head2 valid_login -B<UNSTABLE> +B<DEPRECATED> =over @@ -563,6 +569,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 26558fc6a..6290139a3 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -268,6 +268,11 @@ sub fix_credentials { $params->{'Bugzilla_login'} = $params->{'login'}; $params->{'Bugzilla_password'} = $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/docs/en/rst/using.rst b/docs/en/rst/using.rst index 5be1a2582..a98ff7680 100644 --- a/docs/en/rst/using.rst +++ b/docs/en/rst/using.rst @@ -1005,6 +1005,22 @@ If you attempt to change your email address, a confirmation email is sent to both the old and new addresses, with a link to use to confirm the change. This helps to prevent account hijacking. +.. _apikey: + +API Keys +======== + +API keys are used to authenticate REST 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. + +On this page, you can unrevoke, revoke and change the description of existing +API keys for your login. A revoked key means that it cannot be used. The +description for purely for your information, and is optional. + +You can also create a new API key by selecting the check box under the 'New +API key' section of the page. + .. _permissionsettings: Permissions @@ -22,6 +22,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, @@ -199,7 +200,10 @@ function set_assign_to(use_qa_contact) { var args = JSON.stringify({ version: "1.1", method: 'BugUserLastVisit.update', - params: { ids: bug_id }, + params: { + Bugzilla_api_token: BUGZILLA.api_token, + ids: bug_id + }, }); var callbacks = { failure: function(res) { @@ -218,7 +222,9 @@ function set_assign_to(use_qa_contact) { 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 035d05b0b..987dfd8da 100644 --- a/js/comment-tagging.js +++ b/js/comment-tagging.js @@ -50,7 +50,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; @@ -327,6 +331,7 @@ YAHOO.bugzilla.commentTagging = { version: "1.1", method: 'Bug.comments', params: { + Bugzilla_api_token: BUGZILLA.api_token, comment_ids: [ comment_id ], include_fields: [ 'tags' ] } @@ -359,6 +364,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 892c8669f..f865a141f 100644 --- a/js/field.js +++ b/js/field.js @@ -825,6 +825,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" ] } ] @@ -1047,6 +1048,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/template/en/default/account/prefs/apikey.html.tmpl b/template/en/default/account/prefs/apikey.html.tmpl new file mode 100644 index 000000000..79b25560c --- /dev/null +++ b/template/en/default/account/prefs/apikey.html.tmpl @@ -0,0 +1,84 @@ +[%# 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. + #%] + +<p> + API keys are used to authenticate REST 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 + <a href="docs/en/html/api/Bugzilla/WebService.html#LOGGING_IN">here</a>. +</p> + +<h3>Existing API keys</h3> + +<p>You can update the description, and revoke or unrevoke existing API keys +here.</p> + +<table id="email_prefs"> + <tr class="column_header"> + <th>API key</th> + <th>Description (optional)</th> + <th>Last used</th> + <th>Revoked?</th> + </tr> + + [% FOREACH api_key IN api_keys %] + <tr[% IF api_key.revoked %] class="apikey_revoked"[% END %]> + <td>[% api_key.api_key FILTER html %]</td> + <td> + <input name="description_[% api_key.id FILTER html %]" + id="description_[% api_key.id FILTER html %]" + value="[% api_key.description FILTER html %]"> + </td> + [% IF api_key.last_used %] + <td>[% api_key.last_used FILTER time %]</td> + [% ELSE %] + <td class="center"><i>never used</i></td> + [% END %] + <td class="center"> + <input type="checkbox" value="1" + name="revoked_[% api_key.id FILTER html %]" + id="revoked_[% api_key.id FILTER html %]" + [% IF api_key.revoked %] checked="checked" [% END %]> + </td> + </tr> + [% END %] + [% UNLESS api_keys.size %] + <tr><td colspan="4">You don't have any API keys.</td></tr> + [% END %] +</table> + +[% IF any_revoked %] + <a id="apikey_revoked_controller" class="bz_default_hidden" + href="javascript:TUI_toggle_class('apikey_revoked')">Hide Revoked Keys</a> + [%# Show the link if the browser supports JS %] + <script type="text/javascript"> + TUI_hide_default('apikey_revoked'); + TUI_alternates['apikey_revoked'] = 'Show Revoked Keys'; + YAHOO.util.Dom.removeClass('apikey_revoked_controller', + 'bz_default_hidden'); + </script> +[% END %] + +<h3>New API key</h3> + +<p>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.</p> + +<p> + <input type="checkbox" name="new_key" id="new_key"> + Generate a new API key with optional description + <input name="new_description" id="new_description"> +</p> + diff --git a/template/en/default/account/prefs/prefs.html.tmpl b/template/en/default/account/prefs/prefs.html.tmpl index faa18d581..8f11d0a6f 100644 --- a/template/en/default/account/prefs/prefs.html.tmpl +++ b/template/en/default/account/prefs/prefs.html.tmpl @@ -36,6 +36,9 @@ { name => "account", label => "Account Information", link => "userprefs.cgi?tab=account", saveable => "1", doc_section => "using.html#account-information" }, + { 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", doc_section => "using.html#permissions" } ] %] @@ -53,7 +56,7 @@ title = current_tab.label 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 = current_tab.doc_section yui = ['autocomplete'] %] 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..cfccefdcc --- /dev/null +++ b/template/en/default/email/new-api-key.txt.tmpl @@ -0,0 +1,33 @@ +[%# 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 + #%] + +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 e6bd8f45d..f4a4b66b6 100644 --- a/template/en/default/global/header.html.tmpl +++ b/template/en/default/global/header.html.tmpl @@ -166,6 +166,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 %] }; [% FOREACH yui_name = yui %] diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl index 85e9fc488..48cbcad47 100644 --- a/template/en/default/global/user-error.html.tmpl +++ b/template/en/default/global/user-error.html.tmpl @@ -97,6 +97,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 %]. @@ -219,6 +228,11 @@ [% Hook.process("auth_failure") %] + [% ELSIF error == "auth_invalid_token" %] + [% title = 'A token error occurred' %] + The token '[% token FILTER html %]' is not valid. It could be because + you loaded this page more than 3 days ago. + [% ELSIF error == "auth_untrusted_request" %] [% title = "Untrusted Authentication Request" %] You tried to log in using the <em>[% login FILTER html %]</em> account, diff --git a/userprefs.cgi b/userprefs.cgi index 34a7249d2..13f817d53 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -13,10 +13,12 @@ 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::APIKey; use Bugzilla::Token; my $template = Bugzilla->template; @@ -501,6 +503,65 @@ 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(); + } + } + + # Was a new api key requested + if ($cgi->param('new_key')) { + my $new_key = Bugzilla::User::APIKey->create({ + user_id => $user->id, + description => $cgi->param('new_description'), + }); + + # As a security precaution, we always sent out an e-mail when + # an API key is created + my $lang = $user->setting('lang') + // Bugzilla::User->new()->setting('lang'); + + my $template = Bugzilla->template_inner($lang); + my $message; + $template->process( + 'email/new-api-key.txt.tmpl', + { user => $user, new_key => $new_key }, + \$message + ) || ThrowTemplateError($template->error()); + + MessageToMTA($message); + } + + $dbh->bz_commit_transaction; +} + ############################################################################### # Live code (not subroutine definitions) starts here ############################################################################### @@ -570,6 +631,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 }); |