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