From 1dd256557f757410b10289193c04ad0176387f50 Mon Sep 17 00:00:00 2001 From: Xavier L'Hour Date: Thu, 12 Nov 2026 19:21:48 +0100 Subject: [PATCH] Bug 2061445 - Re-land the native Mojo Bugzilla (system info) REST resource Reverts ef9e9b0d6: the native login accepts ?api_key= since bug 2073282, so /rest/jobqueue_status?api_key= works again (now tested). rest_legacy_credentials.t moves to /rest/component-watching, still legacy. --- Bugzilla/API/V1/Bugzilla.pm | 149 ++++++++ Bugzilla/WebService.pm | 13 +- Bugzilla/WebService/Bugzilla.pm | 352 ------------------ Bugzilla/WebService/Constants.pm | 7 +- Bugzilla/WebService/Server/REST.pm | 3 - .../Server/REST/Resources/Bugzilla.pm | 45 --- docs/en/md/api/core/v1/general.md | 6 +- qa/t/rest_bugzilla.t | 24 +- qa/t/rest_legacy_credentials.t | 13 +- 9 files changed, 188 insertions(+), 424 deletions(-) create mode 100644 Bugzilla/API/V1/Bugzilla.pm delete mode 100644 Bugzilla/WebService/Bugzilla.pm delete mode 100644 Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm diff --git a/Bugzilla/API/V1/Bugzilla.pm b/Bugzilla/API/V1/Bugzilla.pm new file mode 100644 index 0000000000..242208742c --- /dev/null +++ b/Bugzilla/API/V1/Bugzilla.pm @@ -0,0 +1,149 @@ +# 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::API::V1::Bugzilla; + +use 5.10.1; +use Mojo::Base qw( Mojolicious::Controller ); + +use DateTime; +use Try::Tiny; + +use Bugzilla::Constants; +use Bugzilla::Logging; +use Bugzilla::Util qw(datetime_from); + +sub setup_routes { + my ($class, $r) = @_; + + $r->get('/version')->to('V1::Bugzilla#version'); + $r->get('/extensions')->to('V1::Bugzilla#extensions'); + $r->get('/timezone')->to('V1::Bugzilla#timezone'); + $r->get('/time')->to('V1::Bugzilla#time'); + $r->get('/jobqueue_status')->to('V1::Bugzilla#jobqueue_status'); + + foreach my $path (qw(/version /extensions /timezone /time /jobqueue_status)) { + $r->options($path)->to('V1::Bugzilla#options'); + } +} + +sub options { + my ($self) = @_; + + $self->res->headers->header('Allow' => 'GET'); + $self->res->headers->header('Access-Control-Allow-Methods' => 'GET'); + + return $self->rendered(200); +} + +sub version { + my ($self) = @_; + Bugzilla->usage_mode(USAGE_MODE_MOJO_REST); + + return $self->render(json => {version => BUGZILLA_VERSION}); +} + +sub extensions { + my ($self) = @_; + Bugzilla->usage_mode(USAGE_MODE_MOJO_REST); + my $user = $self->bugzilla->login(LOGIN_REQUIRED); + $user->id || return $self->user_error('login_required'); + + my %extensions; + foreach my $extension (@{Bugzilla->extensions}) { + $extensions{$extension->NAME} = {version => '' . ($extension->VERSION || 0)}; + } + + return $self->render(json => {extensions => \%extensions}); +} + +sub timezone { + my ($self) = @_; + Bugzilla->usage_mode(USAGE_MODE_MOJO_REST); + + # All Webservices return times in UTC; Use UTC here for backwards compat. + return $self->render(json => {timezone => '+0000'}); +} + +sub time { + my ($self) = @_; + Bugzilla->usage_mode(USAGE_MODE_MOJO_REST); + my $user = $self->bugzilla->login(LOGIN_REQUIRED); + $user->id || return $self->user_error('login_required'); + + # All Webservices return times in UTC; Use UTC here for backwards compat. + my $dbh = Bugzilla->dbh; + my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $db_time = datetime_from($db_time, 'UTC')->iso8601() . 'Z'; + my $now_utc = DateTime->now()->iso8601() . 'Z'; + + return $self->render( + json => { + db_time => $db_time, + web_time => $now_utc, + web_time_utc => $now_utc, + tz_name => 'UTC', + tz_offset => '+0000', + tz_short_name => 'UTC', + } + ); +} + +sub jobqueue_status { + my ($self) = @_; + + Bugzilla->usage_mode(USAGE_MODE_MOJO_REST); + + my $user = $self->bugzilla->login; + $user->id || return $self->user_error('login_required'); + + my $dbh = Bugzilla->dbh; + my $query = q{ + SELECT + COUNT(*) AS total, + COALESCE( + (SELECT COUNT(*) + FROM ts_error + WHERE ts_error.jobid = j.jobid + ) + , 0) AS errors + FROM ts_job j + INNER JOIN ts_funcmap f + ON f.funcid = j.funcid + GROUP BY errors + }; + + my $status; + my $failed; + try { + $status = $dbh->selectrow_hashref($query); + } + catch { + ERROR($_); + $failed = 1; + }; + return $self->code_error('jobqueue_status_error') if $failed; + + return $self->render( + json => { + errors => 0 + ($status->{errors} // 0), + total => 0 + ($status->{total} // 0), + } + ); +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::V1::Bugzilla - Global functions for the webservice interface. + +=head1 DESCRIPTION + +This provides functions that tell you about Bugzilla in general. diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm index d4b2dfa518..d41a589d2c 100644 --- a/Bugzilla/WebService.pm +++ b/Bugzilla/WebService.pm @@ -72,9 +72,9 @@ L. =head1 CALLING METHODS -Methods are grouped into "packages", like C for -L. So, for example, -L, is called as C. +Methods are grouped into "packages", like C for +C. So, for example, +its C method is called as C. For REST, the "package" is more determined by the path used to access the resource. See each relevant method @@ -385,8 +385,5 @@ objects. =head2 WebService Methods -=over - -=item L - -=back +Only extensions still register methods here, through the C hook. +The core resources are served by the native API (C). diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm deleted file mode 100644 index 8c368d9bd5..0000000000 --- a/Bugzilla/WebService/Bugzilla.pm +++ /dev/null @@ -1,352 +0,0 @@ -# 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::WebService::Bugzilla; - -use 5.10.1; -use strict; -use warnings; - -use base qw(Bugzilla::WebService); -use Bugzilla::Constants; -use Bugzilla::Error; -use Bugzilla::Logging; -use Bugzilla::Util qw(datetime_from); -use JSON::XS; -use Try::Tiny; - -use DateTime; - -# Basic info that is needed before logins -use constant LOGIN_EXEMPT => {timezone => 1, version => 1,}; - -use constant READ_ONLY => qw( - extensions - timezone - time - version - jobqueue_status -); - -use constant PUBLIC_METHODS => qw( - extensions - time - timezone - version - jobqueue_status -); - -sub version { - my $self = shift; - return {version => $self->type('string', BUGZILLA_VERSION)}; -} - -sub extensions { - my $self = shift; - - my %retval; - foreach my $extension (@{Bugzilla->extensions}) { - my $version = $extension->VERSION || 0; - my $name = $extension->NAME; - $retval{$name}->{version} = $self->type('string', $version); - } - return {extensions => \%retval}; -} - -sub timezone { - my $self = shift; - - # All Webservices return times in UTC; Use UTC here for backwards compat. - return {timezone => $self->type('string', "+0000")}; -} - -sub time { - my ($self) = @_; - - # All Webservices return times in UTC; Use UTC here for backwards compat. - # Hardcode values where appropriate - my $dbh = Bugzilla->dbh; - - my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $db_time = datetime_from($db_time, 'UTC'); - my $now_utc = DateTime->now(); - - return { - db_time => $self->type('dateTime', $db_time), - web_time => $self->type('dateTime', $now_utc), - web_time_utc => $self->type('dateTime', $now_utc), - tz_name => $self->type('string', 'UTC'), - tz_offset => $self->type('string', '+0000'), - tz_short_name => $self->type('string', 'UTC'), - }; -} - -sub jobqueue_status { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - - my $dbh = Bugzilla->dbh; - my $query = q{ - SELECT - COUNT(*) AS total, - COALESCE( - (SELECT COUNT(*) - FROM ts_error - WHERE ts_error.jobid = j.jobid - ) - , 0) AS errors - FROM ts_job j - INNER JOIN ts_funcmap f - ON f.funcid = j.funcid - GROUP BY errors - }; - - my $status; - try { - $status = $dbh->selectrow_hashref($query); - } - catch { - ERROR($_); - ThrowCodeError('jobqueue_status_error'); - }; - - return { - errors => $self->type('int', $status->{errors} // 0), - total => $self->type('int', $status->{total} // 0), - }; -} - -1; - -__END__ - -=head1 NAME - -Bugzilla::WebService::Bugzilla - Global functions for the webservice interface. - -=head1 DESCRIPTION - -This provides functions that tell you about Bugzilla in general. - -=head1 METHODS - -See L for a description of how parameters are passed, -and what B, B, and B mean. - -Although the data input and output is the same for JSON-RPC and REST, -the directions for how to access the data via REST is noted in each method -where applicable. - -=head2 version - -B - -=over - -=item B - -Returns the current version of Bugzilla. - -=item B - -GET /version - -The returned data format is the same as below. - -=item B (none) - -=item B - -A hash with a single item, C, that is the version as a -string. - -=item B (none) - -=item B - -=over - -=item REST API call added in Bugzilla B<5.0>. - -=back - -=back - -=head2 extensions - -B - -=over - -=item B - -Gets information about the extensions that are currently installed and enabled -in this Bugzilla. - -=item B - -GET /extensions - -The returned data format is the same as below. - -=item B (none) - -=item B - -A hash with a single item, C. This points to a hash. I hash -contains the names of extensions as keys, and the values are a hash. -That hash contains a single key C, which is the version of the -extension, or C<0> if the extension hasn't defined a version. - -The return value looks something like this: - - extensions => { - Example => { - version => '3.6', - }, - BmpConvert => { - version => '1.0', - }, - } - -=item B - -=over - -=item Added in Bugzilla B<3.2>. - -=item As of Bugzilla B<3.6>, the names of extensions are canonical names -that the extensions define themselves. Before 3.6, the names of the -extensions depended on the directory they were in on the Bugzilla server. - -=item REST API call added in Bugzilla B<5.0>. - -=back - -=back - -=head2 timezone - -B This method may be removed in a future version of Bugzilla. -Use L instead. - -=over - -=item B - -Returns the timezone that Bugzilla expects dates and times in. - -=item B - -GET /timezone - -The returned data format is the same as below. - -=item B (none) - -=item B - -A hash with a single item, C, that is the timezone offset as a -string in (+/-)XXXX (RFC 2822) format. - -=item B - -=over - -=item As of Bugzilla B<3.6>, the timezone returned is always C<+0000> -(the UTC timezone). - -=item REST API call added in Bugzilla B<5.0>. - -=back - -=back - - -=head2 time - -B - -=over - -=item B - -Gets information about what time the Bugzilla server thinks it is, and -what timezone it's running in. - -=item B - -GET /time - -The returned data format is the same as below. - -=item B (none) - -=item B - -A struct with the following items: - -=over - -=item C - -C The current time in UTC, according to the Bugzilla -I. - -Note that Bugzilla assumes that the database and the webserver are running -in the same time zone. However, if the web server and the database server -aren't synchronized for some reason, I is the time that you should -rely on for doing searches and other input to the WebService. - -=item C - -C This is the current time in UTC, according to Bugzilla's -I. - -This might be different by a second from C since this comes from -a different source. If it's any more different than a second, then there is -likely some problem with this Bugzilla instance. In this case you should -rely on the C, not the C. - -=item C - -Identical to C. (Exists only for backwards-compatibility with -versions of Bugzilla before 3.6.) - -=item C - -C The literal string C. (Exists only for backwards-compatibility -with versions of Bugzilla before 3.6.) - -=item C - -C The literal string C. (Exists only for backwards-compatibility -with versions of Bugzilla before 3.6.) - -=item C - -C The literal string C<+0000>. (Exists only for backwards-compatibility -with versions of Bugzilla before 3.6.) - -=back - -=item B - -=over - -=item Added in Bugzilla B<3.4>. - -=item As of Bugzilla B<3.6>, this method returns all data as though the server -were in the UTC timezone, instead of returning information in the server's -local timezone. - -=item REST API call added in Bugzilla B<5.0>. - -=back - -=back diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index f8b4c0d489..756524cd24 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -312,11 +312,8 @@ sub WS_DISPATCH { my %hook_dispatch; Bugzilla::Hook::process('webservice', {dispatch => \%hook_dispatch}); - my $dispatch = { - 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', - %hook_dispatch - }; - return $dispatch; + # Only extensions still dispatch here; core resources are native Mojo API. + return {%hook_dispatch}; } # Custom HTTP headers that can be used for API authentication rather than diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm index 5c3f7ccdde..07ba0c1d7e 100644 --- a/Bugzilla/WebService/Server/REST.pm +++ b/Bugzilla/WebService/Server/REST.pm @@ -22,9 +22,6 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Util qw(fix_credentials taint_data); use Bugzilla::API::V1::Util qw(set_rest_cors_headers); -# Load resource modules -use Bugzilla::WebService::Server::REST::Resources::Bugzilla; - use List::MoreUtils qw(uniq); use Scalar::Util qw(blessed reftype); use MIME::Base64 qw(decode_base64); diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm deleted file mode 100644 index 868e2f4f67..0000000000 --- a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm +++ /dev/null @@ -1,45 +0,0 @@ -# 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::WebService::Server::REST::Resources::Bugzilla; - -use 5.10.1; -use strict; -use warnings; - -use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Bugzilla; - -BEGIN { - *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources; -} - -sub _rest_resources { - my $rest_resources = [ - qr{^/version$}, {GET => {method => 'version'}}, - qr{^/extensions$}, {GET => {method => 'extensions'}}, - qr{^/timezone$}, {GET => {method => 'timezone'}}, - qr{^/time$}, {GET => {method => 'time'}}, - qr{^/jobqueue_status$}, {GET => {method => 'jobqueue_status'}}, - ]; - return $rest_resources; -} - -1; - -__END__ - -=head1 NAME - -Bugzilla::WebService::Bugzilla - Global functions for the webservice interface. - -=head1 DESCRIPTION - -This provides functions that tell you about Bugzilla in general. - -See L for more details on how to use this part -of the REST API. diff --git a/docs/en/md/api/core/v1/general.md b/docs/en/md/api/core/v1/general.md index 6907f69fc1..f22722f8ba 100644 --- a/docs/en/md/api/core/v1/general.md +++ b/docs/en/md/api/core/v1/general.md @@ -105,14 +105,14 @@ Preferences pages. Send only one authentication method with each request. BMO does not combine credentials or choose the strongest method when more than one is supplied. -Most resources have been migrated off the legacy authentication path onto BMO's -native REST framework, which accepts a cookie, an `X-Bugzilla-API-Key` +All core resources have been migrated off the legacy authentication path onto +BMO's native REST framework, which accepts a cookie, an `X-Bugzilla-API-Key` header, or an OAuth2 bearer token (plus the deprecated `Bugzilla_api_key` and `api_key` query parameters described in the warning below, kept only as a stopgap for undiscovered callers). Legacy `Bugzilla_login` and `Bugzilla_password` credentials are **not** accepted on these resources, even though the old WebService dispatcher underneath BMO still supports them for -resources not yet migrated (currently `Bugzilla`). On those +resources not yet migrated (currently only some extension endpoints). On those not-yet-migrated resources, if a request supplies both `Bugzilla_login`/`Bugzilla_password` and an API key, BMO does not combine them or choose the strongest method: the legacy credentials take precedence, diff --git a/qa/t/rest_bugzilla.t b/qa/t/rest_bugzilla.t index e921a7466a..3d1389999a 100644 --- a/qa/t/rest_bugzilla.t +++ b/qa/t/rest_bugzilla.t @@ -35,7 +35,8 @@ ok($headers->header('Access-Control-Allow-Origin') eq '*'); ok($headers->header('Access-Control-Allow-Headers') =~ /x-bugzilla-api-key/); # Make sure list of enabled extensions is returned -$t->get_ok($url . 'rest/extensions')->status_is(200)->json_has('/extensions'); +$t->get_ok($url . 'rest/extensions' => {'X-Bugzilla-API-Key' => $api_key}) + ->status_is(200)->json_has('/extensions'); my $extensions = $t->tx->res->json->{extensions}; my @ext_names = sort keys %{$extensions}; @@ -45,17 +46,34 @@ ok(scalar @ext_names, ok($extensions->{QA}, 'The QA extension is enabled, with version ' . $extensions->{QA}->{version}); +# Anonymous access must return the JSON login_required error, not a 404 +$t->get_ok($url . 'rest/extensions')->status_is(401) + ->json_is('/error' => 1)->json_has('/message'); + # Check that the server timezone is returned $t->get_ok($url . 'rest/timezone')->status_is(200)->json_has('/timezone'); # Check that the server times are returned -$t->get_ok($url . 'rest/time')->status_is(200)->json_has('/db_time') - ->json_has('/web_time'); +$t->get_ok($url . 'rest/time' => {'X-Bugzilla-API-Key' => $api_key}) + ->status_is(200)->json_has('/db_time')->json_has('/web_time'); + +# Anonymous access must return the JSON login_required error, not a 404 +$t->get_ok($url . 'rest/time')->status_is(401) + ->json_is('/error' => 1)->json_has('/message'); # Make sure there are no jobqueue errors $t->get_ok($url . 'rest/jobqueue_status' => {'X-Bugzilla-API-Key' => $api_key}) ->status_is(200)->json_is('/errors' => 0); +# The deprecated ?api_key= query parameter must keep working here: a +# monitoring service still calls jobqueue_status that way (bug 2061445). +$t->get_ok($url . 'rest/jobqueue_status?api_key=' . $api_key)->status_is(200) + ->json_is('/errors' => 0); + +# Anonymous access must return the JSON login_required error, not a 404 +$t->get_ok($url . 'rest/jobqueue_status')->status_is(401) + ->json_is('/error' => 1)->json_has('/message'); + # Check the configuration data for this Bugzilla instance $t->get_ok($url . 'rest/configuration')->status_is(200) ->json_is('/version' => BUGZILLA_VERSION)->json_has('/product'); diff --git a/qa/t/rest_legacy_credentials.t b/qa/t/rest_legacy_credentials.t index 2f77ae1653..4058229610 100644 --- a/qa/t/rest_legacy_credentials.t +++ b/qa/t/rest_legacy_credentials.t @@ -8,8 +8,9 @@ ####################################################### # Test for Bugzilla_login/Bugzilla_password, which # -# resources still on the legacy REST server accept. # -# GET /rest/version # +# resources still on the legacy REST server accept # +# (only extensions are left there). # +# GET /rest/component-watching # # Delete with the last legacy resource. # ####################################################### @@ -37,8 +38,10 @@ my $t = Test::Mojo->new(); $t->ua->max_redirects(1); $t->get_ok(rest_get_url( - $url, 'rest/version', {Bugzilla_login => $user, Bugzilla_password => $pass} -))->status_is(200)->json_has('/version'); + $url, 'rest/component-watching', + {Bugzilla_login => $user, Bugzilla_password => $pass} +))->status_is(200); +is(ref $t->tx->res->json, 'ARRAY', 'Valid credentials return the watch list'); my @tests = ( {login => $user, password => '', test => "Empty password can't log in"}, @@ -64,7 +67,7 @@ my @tests = ( foreach my $test (@tests) { $t->get_ok(rest_get_url( - $url, 'rest/version', + $url, 'rest/component-watching', {Bugzilla_login => $test->{login}, Bugzilla_password => $test->{password}} ))->status_isnt(200); my $expected = $test->{error} // $error;