return {failure => AUTH_NODATA};
}
- my $api_key = Bugzilla::User::APIKey->new({name => $api_key_text});
+ my $api_key = Bugzilla::User::APIKey->new({name => $api_key_text});
+ my $remote_ip = remote_ip();
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->sticky
+ && $api_key->last_used_ip
+ && $api_key->last_used_ip ne $remote_ip)
+ {
+ ThrowUserError("api_key_not_valid");
+ }
elsif ($api_key->revoked) {
ThrowUserError('api_key_revoked');
}
- $api_key->update_last_used();
+ $api_key->update_last_used($remote_ip);
$self->set_app_id($api_key->app_id);
return {user_id => $api_key->user_id};
use base qw(Bugzilla::Object);
use Bugzilla::User;
-use Bugzilla::Util qw(generate_random_password trim remote_ip);
+use Bugzilla::Util qw(generate_random_password trim);
use Bugzilla::Error;
#####################################################################
revoked
last_used
last_used_ip
+ sticky
);
-use constant UPDATE_COLUMNS => qw(description revoked last_used last_used_ip);
+use constant UPDATE_COLUMNS => qw(description revoked last_used last_used_ip sticky);
use constant VALIDATORS => {
api_key => \&_check_api_key,
app_id => \&_check_app_id,
description => \&_check_description,
revoked => \&Bugzilla::Object::check_boolean,
+ sticky => \&Bugzilla::Object::check_boolean,
};
use constant LIST_ORDER => 'id';
use constant NAME_FIELD => 'api_key';
sub revoked { return $_[0]->{revoked} }
sub last_used { return $_[0]->{last_used} }
sub last_used_ip { return $_[0]->{last_used_ip} }
+sub sticky { return $_[0]->{sticky} }
# Helpers
sub user {
}
sub update_last_used {
- my $self = shift;
- my $timestamp
- = shift || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ my ($self, $remote_ip) = @_;
+ my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
$self->set('last_used', $timestamp);
- $self->set('last_used_ip', remote_ip());
+ $self->set('last_used_ip', $remote_ip);
$self->update;
}
# Setters
sub set_description { $_[0]->set('description', $_[1]); }
sub set_revoked { $_[0]->set('revoked', $_[1]); }
+sub set_sticky { $_[0]->set('sticky', $_[1]); }
# Validators
sub _check_api_key { return generate_random_password(40); }
You can create more than one API key if required. Each API key has an optional
description which can help you record what it is used for.
-On this page, you can unrevoke, revoke and change the description of existing
+On this page, you can unrevoke, revoke, make sticky, and change the description of existing
API keys for your login. A revoked key means that it cannot be used. The
description is optional and purely for your information.
+Sticky API keys may only be used from one IP address, which reduces the risk
+of the key being leaked. The IP address is the one the key was last used
+from. The expected workflow is that the sticky bit will be set once your application
+(or script) is setup. The sticky attribute may only be set, it can't ever be unset.
+
You can also create a new API key by selecting the checkbox under the 'New
API key' section of the page.
<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.<br>
- <br>
+ you record what each key is used for.
+</p>
+<p>
Documentation on how to log in is available
<a href="https://bmo.readthedocs.io/en/latest/api/core/v1/general.html#authentication">
here</a>.
<h3>Existing API keys</h3>
<p>You can update the description, and revoke or unrevoke existing API keys
-here.</p>
+here. Sticky keys may only be used from the last IP that used the API key, and cannot be unset.</p>
<table id="email_prefs">
<tr class="column_header">
<th>API key</th>
<th>Description (optional)</th>
<th>Last used</th>
+ <th>Sticky</th>
<th>Revoked</th>
</tr>
[% ELSE %]
<td class="center"><i>never used</i></td>
[% END %]
+ <td class="center">
+ <input type="checkbox" value="1"
+ name="sticky_[% api_key.id FILTER html %]"
+ id="sticky_[% api_key.id FILTER html %]"
+ [% IF api_key.sticky %] checked="checked" disabled="disabled" [% END %]>
+ </td>
<td class="center">
<input type="checkbox" value="1"
name="revoked_[% api_key.id FILTER html %]"
foreach my $api_key (@$api_keys) {
my $description = $cgi->param('description_' . $api_key->id);
my $revoked = !!$cgi->param('revoked_' . $api_key->id);
+ my $sticky = !!$cgi->param('sticky_' . $api_key->id);
- if ($description ne $api_key->description || $revoked != $api_key->revoked) {
+ if ( $description ne $api_key->description
+ || $revoked != $api_key->revoked
+ || $sticky != $api_key->sticky)
+ {
if ($user->mfa && !$revoked && $api_key->revoked) {
push @mfa_events,
{
};
}
else {
- $api_key->set_all({description => $description, revoked => $revoked,});
+ $sticky = 1 if $api_key->sticky;
+ $api_key->set_all(
+ {description => $description, revoked => $revoked, sticky => $sticky});
$api_key->update();
if ($revoked) {
Bugzilla->log_user_request(undef, undef, 'api-key-revoke');