]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 726696 - All authenticated WebServices methods should require username/pass,...
authorSimon Green <sgreen@redhat.com>
Sun, 27 Jul 2014 08:47:21 +0000 (18:47 +1000)
committerSimon Green <sgreen@redhat.com>
Sun, 27 Jul 2014 08:47:21 +0000 (18:47 +1000)
r=dkl, a=sgreen

22 files changed:
Bugzilla/Auth.pm
Bugzilla/Auth/Login/APIKey.pm [new file with mode: 0644]
Bugzilla/Auth/Login/Cookie.pm
Bugzilla/DB/Schema.pm
Bugzilla/Install/DB.pm
Bugzilla/Template.pm
Bugzilla/Token.pm
Bugzilla/User/APIKey.pm [new file with mode: 0644]
Bugzilla/WebService.pm
Bugzilla/WebService/Constants.pm
Bugzilla/WebService/User.pm
Bugzilla/WebService/Util.pm
docs/en/rst/using.rst
js/bug.js
js/comment-tagging.js
js/field.js
template/en/default/account/prefs/apikey.html.tmpl [new file with mode: 0644]
template/en/default/account/prefs/prefs.html.tmpl
template/en/default/email/new-api-key.txt.tmpl [new file with mode: 0644]
template/en/default/global/header.html.tmpl
template/en/default/global/user-error.html.tmpl
userprefs.cgi

index 42e4ee784f2501d503f5b9b0386bee839e4ffb6a..5d4c348dac2043c9d151b401d5ed522739724db1 100644 (file)
@@ -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 (file)
index 0000000..902ce4d
--- /dev/null
@@ -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;
index 29e2310a0a0a73137579dce74de691eb68ca405f..9c18903b69739c65decc12daf2af6829d49ec079 100644 (file)
@@ -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
index 632ab639b83de825cc91c10def1f5e5969b3194c..b175f15549dfa1880d23855a16b1d35acba15162 100644 (file)
@@ -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
index 9e197a907112a477086bfb684a1d4ec62dc5ac0b..bbddf162008eec63be9ea8ec0ba059a316d00e1c 100644 (file)
@@ -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 #
     ################################################################
index 8fe50fa4f8be8673445b903a370e1f7338b53b92..b36249b2f21872da39798b5fcd6ac39a59bb9f9b 100644 (file)
@@ -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;
index 5352638682b15d6125b6f9019ef9a3b7f980e012..71f24183bbbf25cd1353c5deedc6ff584ed05c3a 100644 (file)
@@ -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 (file)
index 0000000..75a4a6b
--- /dev/null
@@ -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
index 1dc04c1f6c9e1df03a65c00fb0102fb57fa51c60..d12d4dbac045444ef8ebed0a5776533935bfed2b 100644 (file)
@@ -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
index 2c21de15e8e59ca85dd0f2a516b0ea0c47b57189..2fa1bdc28fad8e2781908c640c7c7b9a3d789958 100644 (file)
@@ -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,
index f05b2b247f8cea8adb8006627c9770f9a0becba4..32d6f28ecb7265b183f320a4e585fc6944c07a49 100644 (file)
@@ -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
index 26558fc6ac040d37ff2ffaa5266a6d017d48332a..6290139a322f13dcd61cd8ea54c94679463cd2df 100644 (file)
@@ -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'}) {
index 5be1a2582e75193f7cee79f7413df51e8ff338ad..a98ff7680437185c3f1fc9fe244bad5c88ebad9f 100644 (file)
@@ -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
index abefbb22d581c0ada3c42155c96437156bda8e61..f0bf68a30283675d40e52714c9659153eae87d52 100644 (file)
--- a/js/bug.js
+++ b/js/bug.js
@@ -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)) },
index 035d05b0b73e996e291f39a5a7271191b4b31ee0..987dfd8dafbe7858d6b862282e984a58a9bcacff 100644 (file)
@@ -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
index 892c8669f6cd946445931a6554a7e4cdd322bd20..f865a141fa89c562715a15be0dbba79545813691 100644 (file)
@@ -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 (file)
index 0000000..79b2556
--- /dev/null
@@ -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>
+
index faa18d581948ab0b6f82304c94ea37b30c20d70e..8f11d0a6ff893afa91b86c0d457a9e72cb3abc73 100644 (file)
@@ -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 (file)
index 0000000..cfccefd
--- /dev/null
@@ -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') %].
index e6bd8f45d2ae7b59a4cd6cf3ee71e3f4666b9313..f4a4b66b6f36520380ab00863b505013066caaba 100644 (file)
                 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 %]
index 85e9fc488186d1bd514e1cff0753ba13af8fa236..48cbcad47c1f0790789c85308c9c81baa3360a73 100644 (file)
     [% 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 %].
 
     [% 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,
index 34a7249d20a85cd6b0777da9bfc7e8875339877d..13f817d534b757fe68303c7ae59a7c1a065f8480 100755 (executable)
@@ -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 });