From 3563dcf27d62c16847b9afee59c082de4b4cb3bb Mon Sep 17 00:00:00 2001 From: Oliver Kurz Date: Wed, 24 Sep 2025 12:33:43 +0200 Subject: [PATCH 1/2] Allow to configure the worker host+key+secret as env variables --- lib/OpenQA/Worker/WebUIConnection.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/OpenQA/Worker/WebUIConnection.pm b/lib/OpenQA/Worker/WebUIConnection.pm index 65c638a3e40..577818a7a08 100644 --- a/lib/OpenQA/Worker/WebUIConnection.pm +++ b/lib/OpenQA/Worker/WebUIConnection.pm @@ -26,11 +26,12 @@ has 'service_port_delta'; # delta from web UI port on which to directly conne has 'websocket_connection'; sub new ($class, $webui_host, $cli_options) { + $webui_host = $ENV{OPENQA_WORKER_WEBUI_HOST} // $webui_host; my $url = $webui_host !~ '/' ? Mojo::URL->new->scheme('http')->host_port($webui_host) : Mojo::URL->new($webui_host); my $ua = OpenQA::Client->new( api => $url->host, - apikey => $cli_options->{apikey}, - apisecret => $cli_options->{apisecret}, + apikey => $ENV{OPENQA_WORKER_APIKEY} // $cli_options->{apikey}, + apisecret => $ENV{OPENQA_WORKER_APISECRET} // $cli_options->{apisecret}, ); $ua->base_url($url); From e7a6bb27b46b5c9a1298ad754f72fa247e4d4d85 Mon Sep 17 00:00:00 2001 From: Oliver Kurz Date: Wed, 24 Sep 2025 13:15:28 +0200 Subject: [PATCH 2/2] Add support for worker authentication tokens --- lib/OpenQA/Worker.pm | 2 ++ lib/OpenQA/Worker/Settings.pm | 30 ++++++++++++++++++++++++++++ lib/OpenQA/Worker/WebUIConnection.pm | 12 ++++++++--- script/worker | 16 +++++++++++++-- t/24-worker-overall.t | 4 ++++ t/24-worker-settings.t | 17 ++++++++++++++++ 6 files changed, 76 insertions(+), 5 deletions(-) diff --git a/lib/OpenQA/Worker.pm b/lib/OpenQA/Worker.pm index e53d8c319d7..c9ab541ed1a 100644 --- a/lib/OpenQA/Worker.pm +++ b/lib/OpenQA/Worker.pm @@ -64,6 +64,8 @@ has 'current_error_is_fatal'; has 'worker_hostname'; has 'isotovideo_interface_version'; +sub encode_token ($entry_string) { OpenQA::Worker::Settings::encode_token(split /@/, $entry_string) } + sub new ($class, $cli_options) { # determine instance number my $instance_number = $cli_options->{instance}; diff --git a/lib/OpenQA/Worker/Settings.pm b/lib/OpenQA/Worker/Settings.pm index bead4d17732..3830d4d6e2a 100644 --- a/lib/OpenQA/Worker/Settings.pm +++ b/lib/OpenQA/Worker/Settings.pm @@ -8,6 +8,8 @@ use Mojo::File 'path'; use Mojo::URL; use Mojo::Util 'trim'; use Config::IniFiles; +use Feature::Compat::Try; +use MIME::Base64 qw(encode_base64url decode_base64url); use Time::Seconds; use OpenQA::Config; use OpenQA::Log qw(setup_log log_info); @@ -45,6 +47,15 @@ sub new ($class, $instance_number = undef, $cli_options = {}) { my @hosts = split(' ', $cli_options->{host} || $global_settings{HOST} || 'localhost'); delete $global_settings{HOST}; _read_section($cfg, $_, $webui_host_specific_settings{$_} = {}) for @hosts; + my %tokens = map { + my ($host, $key, $secret) = decode_token($_); + $host => {key => $key, secret => $secret} + } split /,/, $ENV{OPENQA_WORKER_TOKEN} // $cli_options->{token} // ''; + for (keys %tokens) { + $webui_host_specific_settings{$_} ||= {}; + $webui_host_specific_settings{$_}->{apikey} = $tokens{$_}{key}; + $webui_host_specific_settings{$_}->{apisecret} = $tokens{$_}{secret}; + } # Select sensible system CPU load15 threshold to prevent system overload # based on experiences with system stability so far @@ -144,4 +155,23 @@ sub has_class ($self, $worker_class) { return exists $c->{$worker_class}; } +# Generate a length-optimized, URL escaped, base64 encoded worker token string +# with a prefix abbreviated for "openQA worker token" +sub encode_token ($host, $key, $secret) { + my ($key_bin, $secret_bin) = map { pack 'H*', $_ } ($key, $secret); + return 'oqwt-' . encode_base64url join "\x00", $host, $key_bin, $secret_bin; +} + +sub decode_token ($token) { + return undef unless my $encoded_data = $token =~ s/^oqwt-//ir; + my $raw_data = decode_base64url($encoded_data); + return undef unless $raw_data; + my @parts = split /\x00/, $raw_data, 3; + return undef unless @parts == 3; + my $host = $parts[0]; + my $key = uc unpack 'H*', $parts[1]; + my $secret = uc unpack 'H*', $parts[2]; + return ($host, $key, $secret); +} + 1; diff --git a/lib/OpenQA/Worker/WebUIConnection.pm b/lib/OpenQA/Worker/WebUIConnection.pm index 577818a7a08..a463562da98 100644 --- a/lib/OpenQA/Worker/WebUIConnection.pm +++ b/lib/OpenQA/Worker/WebUIConnection.pm @@ -25,13 +25,19 @@ has 'service_port_delta'; # delta from web UI port on which to directly conne # the websocket connection to receive commands from the web UI and send the status (Mojo::Transaction::WebSockets instance) has 'websocket_connection'; -sub new ($class, $webui_host, $cli_options) { + + +sub new ($class, $webui_host, $args) { $webui_host = $ENV{OPENQA_WORKER_WEBUI_HOST} // $webui_host; my $url = $webui_host !~ '/' ? Mojo::URL->new->scheme('http')->host_port($webui_host) : Mojo::URL->new($webui_host); + my ($host, $key, $secret) = split /|/, $ENV{OPENQA_WORKER_TOKEN} if $ENV{OPENQA_WORKER_TOKEN}; + $host //= $ENV{OPENQA_WORKER_HOST}; + $key //= $ENV{OPENQA_WORKER_APIKEY}; + $secret //= $ENV{OPENQA_WORKER_APISECRET}; my $ua = OpenQA::Client->new( api => $url->host, - apikey => $ENV{OPENQA_WORKER_APIKEY} // $cli_options->{apikey}, - apisecret => $ENV{OPENQA_WORKER_APISECRET} // $cli_options->{apisecret}, + apikey => $ENV{OPENQA_WORKER_APIKEY} // $args->{apikey}, + apisecret => $ENV{OPENQA_WORKER_APISECRET} // $args->{apisecret}, ); $ua->base_url($url); diff --git a/script/worker b/script/worker index 0f72a28e645..1f558f58c0b 100755 --- a/script/worker +++ b/script/worker @@ -30,6 +30,16 @@ specify the public key needed for API authentication specify the secret key needed for API authentication +=item B<--token> + +specify a comma-separated list of tokens for C@C@C +combinations as alternative to the individual options. Alternatively use the +environment variable C. + +=item B<--encode-token> HOST@KEY@SECRET + +Encode a token as string for the specified host+key+secret combination + =item B<--isotovideo> PATH path to isotovideo script, useful for running from git @@ -97,11 +107,13 @@ my %options; sub usage ($r) { require Pod::Usage; Pod::Usage::pod2usage($r) } GetOptions( - \%options, "no-cleanup", "instance=i", "isotovideo=s", "host=s", "apikey:s", - "apisecret:s", "verbose|v|debug|d", "help|h", + \%options, "no-cleanup", "instance=i", "isotovideo=s", + "host=s", "apikey:s", "apisecret:s", "token:s", + "encode-token:s", "verbose|v|debug|d", "help|h", ) or usage(1); usage(0) if ($options{help}); +exit !OpenQA::Worker::encode_token($options{encode-token}) if $options{encode-token}; # count workers from 1 if not set - if tap devices are used worker would try to use tap -1 $options{instance} ||= 1; diff --git a/t/24-worker-overall.t b/t/24-worker-overall.t index 0dbbf095663..02c37cd0dc6 100644 --- a/t/24-worker-overall.t +++ b/t/24-worker-overall.t @@ -987,5 +987,9 @@ subtest 'worker ipmi' => sub { 'ipmitool called correctly'; }; +subtest 'worker token generation' => sub { + is OpenQA::Worker::encode_token('my_host@123456@789ABC'), 'owqt-', 'can generate token'; +}; + done_testing(); diff --git a/t/24-worker-settings.t b/t/24-worker-settings.t index aa9efe86cab..9da66ede13b 100644 --- a/t/24-worker-settings.t +++ b/t/24-worker-settings.t @@ -63,6 +63,23 @@ subtest 'check for local worker' => sub { ok $settings->is_local_worker, 'considered local with localhost:9527 and remotehost being changed to ::1'; }; +subtest 'worker tokens' => sub { + my $host = 'my_host'; + my $key = '1234567890ABCDEF'; + my $secret = '1234567890ABCDEF'; + my $token = 'oqwt-bXlfaG9zdAASNFZ4kKvN7wASNFZ4kKvN7w'; + is OpenQA::Worker::Settings::encode_token($host, $key, $secret), $token, 'worker can encode expected token'; + is_deeply [OpenQA::Worker::Settings::decode_token($token)], [$host, $key, $secret], 'worker can decode token'; + my $settings = OpenQA::Worker::Settings->new(1, {token => $token}); + is_deeply( + $settings->webui_host_specific_settings->{my_host}, + {apikey => $key, apisecret => $secret}, + 'credentials parsed correctly from optimized oqwt-token' + ) or always_explain $settings->webui_host_specific_settings; + is $settings->webui_host_specific_settings->{'https://remotehost'}{HOST_SPECIFIC}, 'specific setting (remotehost)', + 'other settings preserved'; +}; + subtest 'apply settings to app' => sub { my ($setup_log_called, $setup_log_app); my $mock = Test::MockModule->new('OpenQA::Worker::Settings');