Skip to content

Commit

Permalink
Bug 726696 - All authenticated WebServices methods should require use…
Browse files Browse the repository at this point in the history
…rname/pass, token or a valid API key for authentication

r=dkl, a=sgreen
  • Loading branch information
Simon Green committed Jul 27, 2014
1 parent 9f0f44b commit fd29ee5
Show file tree
Hide file tree
Showing 22 changed files with 553 additions and 18 deletions.
2 changes: 1 addition & 1 deletion Bugzilla/Auth.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down
52 changes: 52 additions & 0 deletions Bugzilla/Auth/Login/APIKey.pm
Original file line number Diff line number Diff line change
@@ -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;
14 changes: 13 additions & 1 deletion Bugzilla/Auth/Login/Cookie.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion Bugzilla/DB/Schema.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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 => [
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Bugzilla/Install/DB.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
################################################################
Expand Down
6 changes: 6 additions & 0 deletions Bugzilla/Template.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 17 additions & 1 deletion Bugzilla/Token.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
154 changes: 154 additions & 0 deletions Bugzilla/User/APIKey.pm
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit fd29ee5

Please sign in to comment.