Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package openQA for openSUSE:Factory checked in at 2025-09-03 21:08:52 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/openQA (Old) and /work/SRC/openSUSE:Factory/.openQA.new.1977 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openQA" Wed Sep 3 21:08:52 2025 rev:739 rq:1302611 version:5.1756905114.bb4fa746 Changes: -------- --- /work/SRC/openSUSE:Factory/openQA/openQA.changes 2025-09-01 17:17:46.713894844 +0200 +++ /work/SRC/openSUSE:Factory/.openQA.new.1977/openQA.changes 2025-09-03 21:09:26.069715354 +0200 @@ -1,0 +2,16 @@ +Wed Sep 03 14:13:20 UTC 2025 - ok...@suse.com + +- Update to version 5.1756905114.bb4fa746: + * Fix syntax error in nginx config + * Mark unconfigured api route log as uncoverable statement + * Increase test coverage for lib/OpenQA/WebAPI/Description.pm + * parser: ktap: Don't write diagnostic data into $subtest_name + * Extend tests for configuring subdomain to serve files + * Avoid job terminated unexpectedly with signal handler in delete needles + * Allow configuring subdomain for serving logs/assets more securely + * Do not invoke Mojo::IOLoop->remove twice + * Add support for Bearer token authentication + * Worker::Engines::isotovideo: Simplify using more Mojo::File + * Worker::Engines::isotovideo: Remove obsolete comment + +------------------------------------------------------------------- Old: ---- openQA-5.1756479924.9488e2cc.obscpio New: ---- openQA-5.1756905114.bb4fa746.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ openQA-client-test.spec ++++++ --- /var/tmp/diff_new_pack.fzhazQ/_old 2025-09-03 21:09:27.633781446 +0200 +++ /var/tmp/diff_new_pack.fzhazQ/_new 2025-09-03 21:09:27.637781615 +0200 @@ -18,7 +18,7 @@ %define short_name openQA-client Name: %{short_name}-test -Version: 5.1756479924.9488e2cc +Version: 5.1756905114.bb4fa746 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-devel-test.spec ++++++ --- /var/tmp/diff_new_pack.fzhazQ/_old 2025-09-03 21:09:27.665782798 +0200 +++ /var/tmp/diff_new_pack.fzhazQ/_new 2025-09-03 21:09:27.665782798 +0200 @@ -18,7 +18,7 @@ %define short_name openQA-devel Name: %{short_name}-test -Version: 5.1756479924.9488e2cc +Version: 5.1756905114.bb4fa746 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-test.spec ++++++ --- /var/tmp/diff_new_pack.fzhazQ/_old 2025-09-03 21:09:27.689783813 +0200 +++ /var/tmp/diff_new_pack.fzhazQ/_new 2025-09-03 21:09:27.693783982 +0200 @@ -18,7 +18,7 @@ %define short_name openQA Name: %{short_name}-test -Version: 5.1756479924.9488e2cc +Version: 5.1756905114.bb4fa746 Release: 0 Summary: Test package for openQA License: GPL-2.0-or-later ++++++ openQA-worker-test.spec ++++++ --- /var/tmp/diff_new_pack.fzhazQ/_old 2025-09-03 21:09:27.717784996 +0200 +++ /var/tmp/diff_new_pack.fzhazQ/_new 2025-09-03 21:09:27.721785164 +0200 @@ -18,7 +18,7 @@ %define short_name openQA-worker Name: %{short_name}-test -Version: 5.1756479924.9488e2cc +Version: 5.1756905114.bb4fa746 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA.spec ++++++ --- /var/tmp/diff_new_pack.fzhazQ/_old 2025-09-03 21:09:27.757786686 +0200 +++ /var/tmp/diff_new_pack.fzhazQ/_new 2025-09-03 21:09:27.761786855 +0200 @@ -97,7 +97,7 @@ %define devel_requires %devel_no_selenium_requires chromedriver Name: openQA -Version: 5.1756479924.9488e2cc +Version: 5.1756905114.bb4fa746 Release: 0 Summary: The openQA web-frontend, scheduler and tools License: GPL-2.0-or-later @@ -593,6 +593,8 @@ %dir %{_sysconfdir}/nginx %dir %{_sysconfdir}/nginx/vhosts.d %config %{_sysconfdir}/nginx/vhosts.d/openqa.conf.template +%config(noreplace) %{_sysconfdir}/nginx/vhosts.d/openqa-assets.inc +%config(noreplace) %{_sysconfdir}/nginx/vhosts.d/openqa-endpoints.inc %config(noreplace) %{_sysconfdir}/nginx/vhosts.d/openqa-locations.inc %config(noreplace) %{_sysconfdir}/nginx/vhosts.d/openqa-upstreams.inc # apparmor profile ++++++ openQA-5.1756479924.9488e2cc.obscpio -> openQA-5.1756905114.bb4fa746.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/Makefile new/openQA-5.1756905114.bb4fa746/Makefile --- old/openQA-5.1756479924.9488e2cc/Makefile 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/Makefile 2025-09-03 15:11:54.000000000 +0200 @@ -124,7 +124,7 @@ done install -d -m 755 "$(DESTDIR)"/etc/nginx/vhosts.d - for i in openqa-locations.inc openqa-upstreams.inc openqa.conf.template; do \ + for i in openqa-assets.inc openqa-endpoints.inc openqa-locations.inc openqa-upstreams.inc openqa.conf.template; do \ install -m 644 etc/nginx/vhosts.d/$$i "$(DESTDIR)"/etc/nginx/vhosts.d ;\ done diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/dist/rpm/openQA.spec new/openQA-5.1756905114.bb4fa746/dist/rpm/openQA.spec --- old/openQA-5.1756479924.9488e2cc/dist/rpm/openQA.spec 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/dist/rpm/openQA.spec 2025-09-03 15:11:54.000000000 +0200 @@ -593,6 +593,8 @@ %dir %{_sysconfdir}/nginx %dir %{_sysconfdir}/nginx/vhosts.d %config %{_sysconfdir}/nginx/vhosts.d/openqa.conf.template +%config(noreplace) %{_sysconfdir}/nginx/vhosts.d/openqa-assets.inc +%config(noreplace) %{_sysconfdir}/nginx/vhosts.d/openqa-endpoints.inc %config(noreplace) %{_sysconfdir}/nginx/vhosts.d/openqa-locations.inc %config(noreplace) %{_sysconfdir}/nginx/vhosts.d/openqa-upstreams.inc # apparmor profile diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/docs/Client.asciidoc new/openQA-5.1756905114.bb4fa746/docs/Client.asciidoc --- old/openQA-5.1756479924.9488e2cc/docs/Client.asciidoc 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/docs/Client.asciidoc 2025-09-03 15:11:54.000000000 +0200 @@ -96,9 +96,9 @@ The authentication mechanism used by `openqa-cli` was specifically designed to allow secure access to the REST API even via unencrypted HTTP connections. But when your openQA server has been deployed with HTTPS (and for HTTP connections -originating from localhost) you can also use plain old Basic authentication -with a personal access token. That allows for almost any HTTP client to be used -with openQA. +originating from localhost) you can also use plain old Basic or Bearer token +authentication with a personal access token. That allows for almost any HTTP +client to be used with openQA. This access token is made up of your username, and the same key/secret combo the `openqa-cli` authentication mechanism uses. All you have to do is combine @@ -111,6 +111,16 @@ https://openqa.example.com/api/v1/assets/1 ---- +And for HTTP clients that don't support Basic authentication or where the use +of plain HTTP headers might be more convenient, you can also send the +personal access token in the form of a Bearer token. + +[source,sh] +---- +curl -H 'Authorization: Bearer arthur:1234567890ABCDEF:ABCDEF1234567890'\ + -X DELETE https://openqa.example.com/api/v1/assets/1 +---- + == Features Many of the `openqa-cli api` features are designed to be similar to other diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/etc/nginx/vhosts.d/openqa-assets.inc new/openQA-5.1756905114.bb4fa746/etc/nginx/vhosts.d/openqa-assets.inc --- old/openQA-5.1756479924.9488e2cc/etc/nginx/vhosts.d/openqa-assets.inc 1970-01-01 01:00:00.000000000 +0100 +++ new/openQA-5.1756905114.bb4fa746/etc/nginx/vhosts.d/openqa-assets.inc 2025-09-03 15:11:54.000000000 +0200 @@ -0,0 +1,12 @@ +alias /var/lib/openqa/share/factory/; +## Optional to require authentication for asset downloads +#auth_request /api/v1/auth; +autoindex on; +tcp_nopush on; +sendfile on; +sendfile_max_chunk 1m; + +# Enforce download of assets so HTML assets cannot highjack session +# note: Can be disabled when using the alternative of making a redirect to a +# different subdomain mentioned in "openqa-locations.inc". +add_header Content-Disposition 'attachment; filename="$1"'; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/etc/nginx/vhosts.d/openqa-endpoints.inc new/openQA-5.1756905114.bb4fa746/etc/nginx/vhosts.d/openqa-endpoints.inc --- old/openQA-5.1756479924.9488e2cc/etc/nginx/vhosts.d/openqa-endpoints.inc 1970-01-01 01:00:00.000000000 +0100 +++ new/openQA-5.1756905114.bb4fa746/etc/nginx/vhosts.d/openqa-endpoints.inc 2025-09-03 15:11:54.000000000 +0200 @@ -0,0 +1,68 @@ +root /usr/share/openqa/public; + +client_max_body_size 0; + +# The "client_body_buffer_size" value should usually be larger +# than the UPLOAD_CHUNK_SIZE used by openQA workers, so there is +# no excessive buffering to disk +client_body_buffer_size 2m; + +# Default is exact which would need an exact match of Last-Modified +if_modified_since before; + +## Optional to make use of auth_request to require authentication for asset downloads +#location /api/v1/auth { +# internal; +# proxy_pass http://webui; +# tcp_nodelay on; +# proxy_read_timeout 900; +# proxy_send_timeout 900; +# proxy_pass_request_body off; +# proxy_set_header Content-Length ""; +# proxy_set_header Host $host; +# proxy_set_header X-Original-URI $request_uri; +# proxy_set_header X-Forwarded-Host $host:$server_port; +# proxy_set_header X-Forwarded-Server $host; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Proto $scheme; +#} + +## Optional faster image downloads for large deployments +#location /image { +# alias /var/lib/openqa/images/; +# tcp_nopush on; +# sendfile on; +# sendfile_max_chunk 1m; +#} + +location /api/v1/ws/ { + proxy_pass http://websocket; + proxy_http_version 1.1; + proxy_read_timeout 3600; + proxy_send_timeout 3600; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; +} + +location /liveviewhandler/ { + proxy_pass http://livehandler; + proxy_http_version 1.1; + proxy_read_timeout 3600; + proxy_send_timeout 3600; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; +} + +location / { + proxy_pass "http://webui"; + tcp_nodelay on; + proxy_read_timeout 900; + proxy_send_timeout 900; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/etc/nginx/vhosts.d/openqa-locations.inc new/openQA-5.1756905114.bb4fa746/etc/nginx/vhosts.d/openqa-locations.inc --- old/openQA-5.1756479924.9488e2cc/etc/nginx/vhosts.d/openqa-locations.inc 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/etc/nginx/vhosts.d/openqa-locations.inc 2025-09-03 15:11:54.000000000 +0200 @@ -1,82 +1,10 @@ -root /usr/share/openqa/public; - -client_max_body_size 0; - -# The "client_body_buffer_size" value should usually be larger -# than the UPLOAD_CHUNK_SIZE used by openQA workers, so there is -# no excessive buffering to disk -client_body_buffer_size 2m; - -# Default is exact which would need an exact match of Last-Modified -if_modified_since before; - ## Optional faster assets downloads for large deployments #location /assets { -# alias /var/lib/openqa/share/factory/; -# # Optional to require authentication for asset downloads -# #auth_request /api/v1/auth; -# autoindex on; -# tcp_nopush on; -# sendfile on; -# sendfile_max_chunk 1m; +# include vhosts.d/openqa-assets.inc; # -# # Enforce download of assets so HTML assets cannot highjack session -# add_header Content-Disposition 'attachment; filename="$1"'; -#} - -## Optional to make use of auth_request to require authentication for asset downloads -#location /api/v1/auth { -# internal; -# proxy_pass http://webui; -# tcp_nodelay on; -# proxy_read_timeout 900; -# proxy_send_timeout 900; -# proxy_pass_request_body off; -# proxy_set_header Content-Length ""; -# proxy_set_header Host $host; -# proxy_set_header X-Original-URI $request_uri; -# proxy_set_header X-Forwarded-Host $host:$server_port; -# proxy_set_header X-Forwarded-Server $host; -# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -# proxy_set_header X-Forwarded-Proto $scheme; -#} - -## Optional faster image downloads for large deployments -#location /image { -# alias /var/lib/openqa/images/; -# tcp_nopush on; -# sendfile on; -# sendfile_max_chunk 1m; +# ## Alternatively, make a redirect to a different subdomain in accordance +# ## with the file_subdomain in openQA config +# #return 301 http://file.$host$request_uri; #} -location /api/v1/ws/ { - proxy_pass http://websocket; - proxy_http_version 1.1; - proxy_read_timeout 3600; - proxy_send_timeout 3600; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; -} - -location /liveviewhandler/ { - proxy_pass http://livehandler; - proxy_http_version 1.1; - proxy_read_timeout 3600; - proxy_send_timeout 3600; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; -} - -location / { - proxy_pass "http://webui"; - tcp_nodelay on; - proxy_read_timeout 900; - proxy_send_timeout 900; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Host $host:$server_port; - proxy_set_header X-Forwarded-Server $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; -} +include vhosts.d/openqa-endpoints.inc; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/etc/nginx/vhosts.d/openqa.conf.template new/openQA-5.1756905114.bb4fa746/etc/nginx/vhosts.d/openqa.conf.template --- old/openQA-5.1756479924.9488e2cc/etc/nginx/vhosts.d/openqa.conf.template 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/etc/nginx/vhosts.d/openqa.conf.template 2025-09-03 15:11:54.000000000 +0200 @@ -17,3 +17,27 @@ # ssl_certificate_key /etc/dehydrated/certs/openqa.example.com/privkey.pem; # include vhosts.d/openqa-locations.inc; #} + +# Provide different servers for serving assets under a different subdomain +# (enable these in accordance to "Optional faster asset downloads …" in +# "openqa-locations.inc") +#server { +# listen 80; +# listen [::]:80; +# # Set server_name in accordance with the file_subdomain in openQA config +# server_name file.openqa.example.com; +# location /assets { +# include vhosts.d/openqa-assets.inc; +# } +# include vhosts.d/openqa-endpoints.inc; +#} +#server { +# listen 443; +# listen [::]:443; +# # Set server_name in accordance with the file_subdomain in openQA config +# server_name file.openqa.example.com; +# location /assets { +# include vhosts.d/openqa-assets.inc; +# } +# include vhosts.d/openqa-endpoints.inc; +#} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/etc/openqa/openqa.ini new/openQA-5.1756905114.bb4fa746/etc/openqa/openqa.ini --- old/openQA-5.1756479924.9488e2cc/etc/openqa/openqa.ini 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/etc/openqa/openqa.ini 2025-09-03 15:11:54.000000000 +0200 @@ -56,9 +56,17 @@ ## avoid potentially malicious JavaScript code from hijacking the user session. ## * Set to "insecure-browsing" so these files can be browsed directly. This is ## insecure as potentially malicious JavaScript can hijack the user session. +## * Set to "subdomain:…" to let openQA redirect these files to another +## subdomain, e.g. "subdomain:file" will redirect file downloads from e.g. +## "example.openqa.org" to "file.example.openqa.org". The files will no longer +## be served as attachments to HTML files can be browsed conveniently and +## securely from that subdomain. ## note: Does *not* affect files served via a reverse proxy. The default NGINX ## config contained by the openQA repo shows how to enforce a download -## prompt for assets served via NGINX. +## prompt for assets served via NGINX. It also shows how to setup +## redirections to a different subdomain which is a little bit more config +## effort and you also need to make sure your certificate is valid for +## this subdomain. #file_security_policy = download-prompt ## space-separated list of domains recognized by job labeling diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Parser/Format/KTAP.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Parser/Format/KTAP.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Parser/Format/KTAP.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Parser/Format/KTAP.pm 2025-09-03 15:11:54.000000000 +0200 @@ -48,7 +48,7 @@ $self->test or return; my $line = $result->as_string; - return unless $line =~ /^#\s*(?<status>ok|not ok)\s+\d+\s+(?<name>.+)/; + return unless $line =~ /^#\s*(?<status>ok|not ok)\s+\d+\s+(?<name>[^#]*)/; my ($status, $subtest_name) = @+{qw(status name)}; my $has_todo = $line =~ /#\s*TODO\b/i; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Setup.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Setup.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Setup.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Setup.pm 2025-09-03 15:11:54.000000000 +0200 @@ -72,6 +72,7 @@ # deprecated alternate for git_auto_commit below scm => undef, hsts => 365, + file_subdomain => undef, audit_enabled => 1, max_rss_limit => 0, profiling_enabled => 0, @@ -307,7 +308,10 @@ } sub _validate_security_policy ($app, $config) { - if ($config->{file_security_policy} !~ m/^(download-prompt|insecure-browsing)$/) { + if ($config->{file_security_policy} =~ m/^(download-prompt|insecure-browsing|subdomain:(.+))$/) { + if (defined(my $subdomain = $2)) { $config->{file_subdomain} = "$subdomain." } + } + else { $config->{file_security_policy} = 'download-prompt'; $app->log->warn('Invalid file_security_policy specified, defaulting to "download-prompt"'); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Shared/Controller/Auth.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Shared/Controller/Auth.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Shared/Controller/Auth.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Shared/Controller/Auth.pm 2025-09-03 15:11:54.000000000 +0200 @@ -10,9 +10,9 @@ use Mojo::URL; sub check ($self) { - if ($self->app->config->{no_localhost_auth}) { - return 1 if $self->is_local_request; - } + my $config = $self->app->config; + return 1 if $config->{no_localhost_auth} && $self->is_local_request; + return 0 if $self->via_subdomain($config->{global}->{file_subdomain}); my $req = $self->req; my $headers = $req->headers; @@ -46,6 +46,12 @@ sub auth ($self) { my $log = $self->app->log; + # Prevent authentication via file subdomain (where potentially untrusted HTML is served) + if ($self->via_subdomain($self->config->{global}->{file_subdomain})) { + $self->render(json => {error => 'Forbidden via file subdomain'}, status => 403); + return 0; + } + # Browser with a logged in user my ($user, $reason) = (undef, 'Not authorized'); if ($user = $self->current_user) { @@ -56,7 +62,10 @@ else { # Personal access token - if (my $userinfo = $self->req->url->to_abs->userinfo) { + if (($self->req->headers->authorization // '') =~ /^Bearer\s+(.+)$/) { + ($user, $reason) = $self->_token_auth($reason, $1); + } + elsif (my $userinfo = $self->req->url->to_abs->userinfo) { ($user, $reason) = $self->_token_auth($reason, $userinfo); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Shared/Controller/Running.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Shared/Controller/Running.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Shared/Controller/Running.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Shared/Controller/Running.pm 2025-09-03 15:11:54.000000000 +0200 @@ -111,9 +111,7 @@ my $close_connection = sub ($self) { my $worker_id = $worker->id; log_debug "Connection to worker with ID $worker_id will be closed because $logfile changed"; - Mojo::IOLoop->remove($timer_id); $self->finish; - close $log; }; $timer_id = Mojo::IOLoop->recurring( TEXT_STREAMING_INTERVAL() => sub (@) { @@ -143,7 +141,11 @@ }); # Stop monitoring the logfile when the connection closes - $self->on(finish => sub (@) { Mojo::IOLoop->remove($timer_id); }); + $self->on( + finish => sub (@) { + Mojo::IOLoop->remove($timer_id); + close $log; + }); } sub livelog ($self) { @@ -196,7 +198,6 @@ # setup a function to stop streaming again my $timer_id; my $close_connection = sub ($self, @) { - Mojo::IOLoop->remove($timer_id); $self->finish; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Shared/Controller/Session.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Shared/Controller/Session.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Shared/Controller/Session.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Shared/Controller/Session.pm 2025-09-03 15:11:54.000000000 +0200 @@ -53,8 +53,11 @@ sub create { my ($self) = @_; my $ref = $self->req->headers->referrer; - my $auth_method = $self->app->config->{auth}->{method}; + my $config = $self->app->config; + my $auth_method = $config->{auth}->{method}; my $auth_module = "OpenQA::WebAPI::Auth::$auth_method"; + return $self->render(text => 'Forbidden via file subdomain', status => 403) + if $self->via_subdomain($config->{global}->{file_subdomain}); # prevent redirecting loop when referrer is login page $ref = 'index' if !$ref or $ref eq $self->url_for('login'); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Shared/Plugin/SharedHelpers.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Shared/Plugin/SharedHelpers.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Shared/Plugin/SharedHelpers.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Shared/Plugin/SharedHelpers.pm 2025-09-03 15:11:54.000000000 +0200 @@ -21,6 +21,7 @@ $app->helper(is_admin => \&_is_admin); $app->helper(is_local_request => \&_is_local_request); $app->helper(render_specific_not_found => \&_render_specific_not_found); + $app->helper(via_subdomain => \&_via_subdomain); } # returns the isotovideo command server web socket URL and the VNC argument for the given job or undef if not available @@ -79,4 +80,9 @@ return $c->render(status => 404, text => "$title - $error_message"); } +sub _via_subdomain ($c, $subdomain) { + return 0 unless defined $subdomain; + return index($c->req->url->to_abs->host, $subdomain) == 0; +} + 1; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Task/Needle/Delete.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Task/Needle/Delete.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Task/Needle/Delete.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Task/Needle/Delete.pm 2025-09-03 15:11:54.000000000 +0200 @@ -2,20 +2,23 @@ # SPDX-License-Identifier: GPL-2.0-or-later package OpenQA::Task::Needle::Delete; -use Mojo::Base 'Mojolicious::Plugin'; +use Mojo::Base 'Mojolicious::Plugin', -signatures; use OpenQA::Utils; use Scalar::Util 'looks_like_number'; use Time::Seconds 'ONE_HOUR'; +use OpenQA::Task::SignalGuard; sub register { my ($self, $app) = @_; $app->minion->add_task(delete_needles => sub { _delete_needles($app, @_) }); } -sub _delete_needles { - my ($app, $minion_job, $args) = @_; - +sub _delete_needles ($app, $minion_job, $args) { + # SignalGuard will prevent the delete task to interrupt with no recovery, + # instead will retry once the gru server returned up and running. The popup + # on the frontend will wait until the retried job finished. + my $signal_guard = OpenQA::Task::SignalGuard->new($minion_job); my $schema = $app->schema; my $needles = $schema->resultset('Needles'); my $user = $schema->resultset('Users')->find($args->{user_id}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/WebAPI/Controller/File.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/WebAPI/Controller/File.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/WebAPI/Controller/File.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/WebAPI/Controller/File.pm 2025-09-03 15:11:54.000000000 +0200 @@ -108,7 +108,8 @@ return $self->reply->not_found if $path =~ qr/\.\./; my $file = path(assetdir(), $path)->to_string; - $self->_set_headers($file); + my $is_text = $self->_set_headers($file); + return if $self->_redirect_if_configured($is_text); return $self->reply->not_found unless -f $file && -r _; $self->reply->file($file); } @@ -146,19 +147,41 @@ sub _set_headers ($self, $path) { my $filename = basename($path); - # guess content type from extension my $headers = $self->res->headers; - return $headers->content_type('application/octet-stream') unless $filename =~ m/\.([^\.]+)$/; - my $ext = $1; - my $as_attachment = 1; - if (my $filetype = $self->app->types->type($ext)) { - $headers->content_type($filetype); - $headers->header('X-Content-Type-Options', 'nosniff') if $filetype =~ qr|^text/plain;?|; - my $allow_insecure = $self->app->config->{global}->{file_security_policy} ne 'download-prompt'; - $as_attachment = 0 if ($allow_insecure || $filetype !~ m|html|) && $ext ne 'iso'; + my $is_text = 0; + if ($filename =~ m/\.([^\.]+)$/) { + # guess content type from extension + my $ext = $1; + my $as_attachment = 1; + if (my $filetype = $self->app->types->type($ext)) { + $headers->content_type($filetype); + if ($filetype =~ qr|^text/plain;?|) { + $headers->header('X-Content-Type-Options', 'nosniff'); + $is_text = 1; + } + my $allow_insecure = $self->app->config->{global}->{file_security_policy} ne 'download-prompt'; + $as_attachment = 0 if ($allow_insecure || $filetype !~ m|html|) && $ext ne 'iso'; + } + # force saveAs + $headers->content_disposition("attachment; filename=$filename;") if $as_attachment; } - # force saveAs - $headers->content_disposition("attachment; filename=$filename;") if $as_attachment; + else { + $headers->content_type('application/octet-stream'); + } + return $is_text; +} + +sub _redirect_if_configured ($self, $is_text) { + # skip harmless text files as the viewer doesn't follow redirects and those files are not problematic anyway + return 0 if $is_text || !defined(my $subdomain = $self->app->config->{global}->{file_subdomain}); + # redirect to configured subdomain so potentially dangerious HTML files cannot use the current session + my $url = $self->req->url->to_abs; + my $host = $url->host; + # skip if already redirected + return 0 unless index($host, $subdomain) == -1; + $url->host($subdomain . $host); + $self->redirect_to($url); + return 1; } sub _serve_static ($self, $asset) { @@ -170,7 +193,9 @@ return $self->reply->not_found unless $asset; $log->debug('found ' . pp($asset)); - $self->_set_headers($asset->path) if blessed $asset && $asset->isa('Mojo::Asset::File'); + my $is_text = blessed $asset && $asset->isa('Mojo::Asset::File') && $self->_set_headers($asset->path); + return 1 if $self->_redirect_if_configured($is_text); + $static->serve_asset($self, $asset); return !!$self->rendered; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/WebAPI/Description.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/WebAPI/Description.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/WebAPI/Description.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/WebAPI/Description.pm 2025-09-03 15:11:54.000000000 +0200 @@ -55,7 +55,7 @@ log_warning('get_pod_from_controllers: could not parse file: [' . $ctrlrpath->child($controllers{$ctrl})->to_string . '] for POD. Error: [' - . $tree->error() + . $parser->error() . ']'); next; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/WebAPI/Plugin/ObsRsync.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/WebAPI/Plugin/ObsRsync.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/WebAPI/Plugin/ObsRsync.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/WebAPI/Plugin/ObsRsync.pm 2025-09-03 15:11:54.000000000 +0200 @@ -44,7 +44,9 @@ die("ssh-keygen is not available. Aborting.\n") unless which('ssh-keygen'); if (!$plugin_r) { - $app->log->error('Routes not configured, plugin ObsRsync will be disabled') unless $plugin_r; + # uncoverable statement + $app->log->error('Routes not configured, plugin ObsRsync will be disabled') + unless $plugin_r; } else { $app->helper('obs_rsync.home' => sub { shift->app->config->{obs_rsync}->{home} }); @@ -155,7 +157,9 @@ } if (!$plugin_api_r) { - $app->log->error('API routes not configured, plugin ObsRsync will not have API configured') unless $plugin_r; + # uncoverable statement + $app->log->error('API routes not configured, plugin ObsRsync will not have API configured') + unless $plugin_r; } else { $plugin_api_r->get('/obs_rsync')->name('plugin_obs_rsync_api_list') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Worker/Engines/isotovideo.pm new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Worker/Engines/isotovideo.pm --- old/openQA-5.1756479924.9488e2cc/lib/OpenQA/Worker/Engines/isotovideo.pm 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/lib/OpenQA/Worker/Engines/isotovideo.pm 2025-09-03 15:11:54.000000000 +0200 @@ -51,14 +51,7 @@ sub _save_vars ($pooldir, $vars) { die 'cannot get environment variables!\n' unless $vars; - my $fn = $pooldir . '/vars.json'; - unlink "$pooldir/vars.json" if -e "$pooldir/vars.json"; - open(my $fd, '>', $fn) or die "can not write vars.json: $!\n"; - fcntl($fd, F_SETLKW, pack('ssqql', F_WRLCK, 0, 0, 0, $$)) or die "cannot lock vars.json: $!\n"; - truncate($fd, 0) or die "cannot truncate vars.json: $!\n"; - - print $fd Cpanel::JSON::XS->new->pretty(1)->encode(\%$vars); - close($fd); + path($pooldir, 'vars.json')->spew(Cpanel::JSON::XS->new->pretty(1)->encode(\%$vars)); } sub detect_asset_keys ($vars) { @@ -294,20 +287,10 @@ log_info('+++ setup notes +++', channels => 'autoinst'); log_info(sprintf("Running on $hostname:%d ($sysname $release $version $machine)", $instance), channels => 'autoinst'); - log_error('Failed enabling subreaper mode', channels => 'autoinst') unless session->subreaper; - - # XXX: this should come from the worker table. Only included - # here for convenience when looking at the pool of - # debugging. my $job_settings = $job_info->{settings}; - for my $i (qw(QEMUPORT VNC OPENQA_HOSTNAME)) { - $job_settings->{$i} = $ENV{$i}; - } - if (open(my $fh, '>', 'job.json')) { - print $fh Cpanel::JSON::XS->new->pretty(1)->encode($job_info); - close $fh; - } + $job_settings->{$_} = $ENV{$_} for qw(QEMUPORT VNC OPENQA_HOSTNAME); + path('job.json')->spew(Cpanel::JSON::XS->new->pretty(1)->encode($job_info)); # pass worker instance and worker id to isotovideo # both used to create unique MAC and TAP devices if needed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/t/31-api_descriptions.t new/openQA-5.1756905114.bb4fa746/t/31-api_descriptions.t --- old/openQA-5.1756479924.9488e2cc/t/31-api_descriptions.t 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/t/31-api_descriptions.t 2025-09-03 15:11:54.000000000 +0200 @@ -2,13 +2,31 @@ # SPDX-License-Identifier: GPL-2.0-or-later use Test::Most; +use Test::MockModule; use Test::Warnings ':report_warnings'; +use Test::Output qw(combined_like); # no OpenQA::Test::TimeLimit for this trivial test +use File::Temp qw(tempdir); +use Mojo::File qw(path); + use Mojo::Base 'Mojolicious', -signatures; use_ok('OpenQA::WebAPI::Description', qw(get_pod_from_controllers set_api_desc)); my $app = Mojolicious->new; get_pod_from_controllers($app); +my $mock = Test::MockModule->new('Pod::POM'); +my $app_home = tempdir(); +my $route = $app->routes->any('/*wathever')->add_child($app->routes->any('/child')->to(controller => 'Main')); + +$mock->redefine(parse_file => undef); +path($app_home . '/lib/OpenQA/WebAPI/Controller/API/V1/')->make_path->child('Main.pm')->touch; +$app->home($app_home); + +combined_like { + get_pod_from_controllers($app, $route) +} +qr/\[WARN\].*get_pod_from_controllers/, 'Warning when file does not exist'; + done_testing; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/t/api/03-auth.t new/openQA-5.1756905114.bb4fa746/t/api/03-auth.t --- old/openQA-5.1756479924.9488e2cc/t/api/03-auth.t 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/t/api/03-auth.t 2025-09-03 15:11:54.000000000 +0200 @@ -12,6 +12,7 @@ use Test::Warnings qw(:all :report_warnings); use Mojo::URL; use Mojo::Util qw(encode hmac_sha1_sum); +use OpenQA::Shared::Controller::Auth; use OpenQA::Test::TimeLimit '10'; use OpenQA::Test::Case; use OpenQA::Test::Client 'client'; @@ -213,6 +214,28 @@ # Valid access token (again) $t->$userinfo('artie:ARTHURKEY01:EXCALIBUR')->delete_ok('/api/v1/assets/1')->status_is(404); + + subtest 'Bearer token' => sub { + subtest 'Valid token' => sub { + $t->post_ok('/api/v1/feature' => {Authorization => 'Bearer lance:LANCELOTKEY01:MANYPEOPLEKNOW'} => form => + {version => 100})->status_is(200); + }; + + subtest 'Invalid username' => sub { + $t->post_ok('/api/v1/feature' => {Authorization => 'Bearer invalid:LANCELOTKEY01:MANYPEOPLEKNOW'} => form => + {version => 100})->status_is(403)->json_is({error => 'invalid personal access token'}); + }; + + subtest 'Invalid key' => sub { + $t->post_ok('/api/v1/feature' => {Authorization => 'Bearer lance:LANCELOTKEY02:MANYPEOPLEKNOW'} => form => + {version => 100})->status_is(403)->json_is({error => 'invalid personal access token'}); + }; + + subtest 'Invalid secret' => sub { + $t->post_ok('/api/v1/feature' => {Authorization => 'Bearer lance:LANCELOTKEY01:MANYPEOPLEKNOWS'} => form => + {version => 100})->status_is(403)->json_is({error => 'invalid personal access token'}); + }; + }; }; subtest 'personal access token (with reverse proxy)' => sub { @@ -250,4 +273,23 @@ ->json_is({error => 'invalid personal access token'}); }; +subtest 'auth forbidden via subdomain' => sub { + my $rendered; + my $req = Mojo::Message::Request->new; + $req->url->parse('http://foobar.openqa.de/test/42'); + my $controller_mock = Test::MockModule->new('Mojolicious::Controller'); + $controller_mock->redefine(req => $req); + $controller_mock->redefine(render => sub ($c, @args) { $rendered = [@args] }); + my $c = OpenQA::Shared::Controller::Auth->new(app => $t->app, req => $req); + $c->config->{global}->{file_subdomain} = 'foobar.'; + is $c->auth, 0, 'auth denied via subdomain'; + my %expected_json = (error => 'Forbidden via file subdomain'); + my @expected = (json => \%expected_json, status => 403); + is_deeply $rendered, \@expected, 'expected error and status'; + $req->url->parse('http://openqa.de/test/42'); + is $c->auth, 0, 'auth denied via regular domain'; + $expected_json{error} = 'no api key'; + is_deeply $rendered, \@expected, 'normal auth error via regular domain'; +}; + done_testing(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/t/config.t new/openQA-5.1756905114.bb4fa746/t/config.t --- old/openQA-5.1756479924.9488e2cc/t/config.t 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/t/config.t 2025-09-03 15:11:54.000000000 +0200 @@ -333,6 +333,10 @@ $config{file_security_policy} = 'wrong'; combined_like { OpenQA::Setup::_validate_security_policy($app, \%config) } qr/Invalid.*security/, 'warning logged'; is $config{file_security_policy}, 'download-prompt', 'default to "download-prompt" on invalid value'; + is $config{file_subdomain}, undef, 'file_subdomain not populated yet'; + $config{file_security_policy} = 'subdomain:foo'; + OpenQA::Setup::_validate_security_policy($app, \%config); + is $config{file_subdomain}, 'foo.', 'file_subdomain populated via "subdomain:"'; }; subtest 'Multiple config files' => sub { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1756479924.9488e2cc/t/ui/07-file.t new/openQA-5.1756905114.bb4fa746/t/ui/07-file.t --- old/openQA-5.1756479924.9488e2cc/t/ui/07-file.t 2025-08-29 17:05:24.000000000 +0200 +++ new/openQA-5.1756905114.bb4fa746/t/ui/07-file.t 2025-09-03 15:11:54.000000000 +0200 @@ -141,4 +141,12 @@ $t->get_ok('/assets/hdd/foo.qcow2')->status_is(200)->content_type_is('application/octet-stream'); $t->get_ok('/assets/repo/testrepo/doesnotexist')->status_is(404); +subtest 'redirection to different subdomain' => sub { + my $config = $t->app->config->{global}; + $config->{file_security_policy} = 'subdomain:file'; + $config->{file_subdomain} = 'file.'; + $t->get_ok('/assets/repo/testrepo/README.html')->status_is(302); + $t->header_like(Location => qr|^http://file\.[^/]*/assets/repo/testrepo/README.html$|); +}; + done_testing(); ++++++ openQA.obsinfo ++++++ --- /var/tmp/diff_new_pack.fzhazQ/_old 2025-09-03 21:09:45.734546324 +0200 +++ /var/tmp/diff_new_pack.fzhazQ/_new 2025-09-03 21:09:45.738546494 +0200 @@ -1,5 +1,5 @@ name: openQA -version: 5.1756479924.9488e2cc -mtime: 1756479924 -commit: 9488e2cc713a2fe405429668d2a46e27295223b4 +version: 5.1756905114.bb4fa746 +mtime: 1756905114 +commit: bb4fa7461f44b3a626444a170616cc04952a97a1