summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Lawrence <dkl@mozilla.com>2015-01-29 18:33:12 +0100
committerDavid Lawrence <dkl@mozilla.com>2015-01-29 18:33:12 +0100
commitc8447e9f4b7c17ab0e04af34dbd5583e78b23677 (patch)
tree573093df276637e98796717c325c3fc5c040263d
parent89d319922df1160f346321acfa6a9c5d69b5ed43 (diff)
downloadbugzilla-c8447e9f4b7c17ab0e04af34dbd5583e78b23677.tar.gz
bugzilla-c8447e9f4b7c17ab0e04af34dbd5583e78b23677.tar.xz
Bug 1045145: backport upstream bug 726696 to bmo/4.2 to allow use of api keys for authentication
-rw-r--r--Bugzilla/Auth.pm2
-rw-r--r--Bugzilla/Auth/Login/APIKey.pm52
-rw-r--r--Bugzilla/Auth/Login/Cookie.pm17
-rw-r--r--Bugzilla/DB/Schema.pm22
-rw-r--r--Bugzilla/Install/DB.pm21
-rw-r--r--Bugzilla/Template.pm6
-rw-r--r--Bugzilla/Token.pm34
-rw-r--r--Bugzilla/User/APIKey.pm154
-rw-r--r--Bugzilla/WebService.pm39
-rw-r--r--Bugzilla/WebService/Constants.pm3
-rw-r--r--Bugzilla/WebService/User.pm22
-rw-r--r--Bugzilla/WebService/Util.pm5
-rw-r--r--js/bug.js10
-rw-r--r--js/comment-tagging.js8
-rw-r--r--js/field.js2
-rw-r--r--skins/standard/global.css32
-rw-r--r--template/en/default/account/prefs/apikey.html.tmpl86
-rw-r--r--template/en/default/account/prefs/prefs.html.tmpl5
-rw-r--r--template/en/default/email/new-api-key.txt.tmpl35
-rw-r--r--template/en/default/global/header.html.tmpl3
-rw-r--r--template/en/default/global/user-error.html.tmpl14
-rwxr-xr-xuserprefs.cgi60
22 files changed, 609 insertions, 23 deletions
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<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 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<User.login>
You can use L<Bugzilla::WebService::User/login> 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<Bugzilla_api_key>
+
+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>
@@ -182,17 +193,31 @@ not expire.
The C<Bugzilla_restrictlogin> and C<Bugzilla_rememberlogin> options
are only used when you have also specified C<Bugzilla_login> and
-C<Bugzilla_password>.
+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.
Note that Bugzilla will return HTTP cookies along with the method
response when you use these arguments (just like the C<User.login> method
above).
-For REST, you may also use the C<username> and C<password> variable
+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.
-=item B<Added in Bugzilla 5.0>
+=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 B<Added in Bugzilla 4.4.3>
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<Bugzilla::WebService>
+
=head2 login
-B<STABLE>
+B<DEPRECATED>
=over
@@ -504,11 +508,21 @@ A login or password parameter was not provided.
=back
+=item B<History>
+
+=over
+
+=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
+
=back
=head2 logout
-B<STABLE>
+B<DEPRECATED>
=over
@@ -526,7 +540,7 @@ Log out the user. Does nothing if there is no user logged in.
=head2 valid_login
-B<UNSTABLE>
+B<DEPRECATED>
=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.
+ #%]
+
+<p>
+ 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
+ <a href="https://bugzilla.readthedocs.org/en/latest/api/core/v1/general.html#authentication">
+ 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"
+ onchange="if (this.checked) YAHOO.util.Dom.get('new_description').focus();">
+ <label for="new_key">
+ Generate a new API key with optional description</label>
+ <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 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 });