From f73043378907c2c7e44f633ad764c8bdd1c947d5 Mon Sep 17 00:00:00 2001
From: Martin Weinelt
Date: Tue, 27 Aug 2024 17:00:00 +0200
Subject: [PATCH] Create eval-jobset role and guard /api/push route
---
doc/manual/src/configuration.md | 3 ++-
src/lib/Hydra/Config.pm | 2 ++
src/lib/Hydra/Controller/API.pm | 7 ++++++-
src/lib/Hydra/Helper/CatalystUtils.pm | 22 ++++++++++++++++++++++
src/root/user.tt | 1 +
t/Hydra/Config/ldap_role_map.t | 2 ++
t/Hydra/Controller/API/checks.t | 27 ++++++++++++++++++++++++---
t/Hydra/Controller/User/ldap-legacy.t | 3 ++-
t/Hydra/Controller/User/ldap.t | 5 ++++-
9 files changed, 65 insertions(+), 7 deletions(-)
diff --git a/doc/manual/src/configuration.md b/doc/manual/src/configuration.md
index 4954040c6..d370312a7 100644
--- a/doc/manual/src/configuration.md
+++ b/doc/manual/src/configuration.md
@@ -208,7 +208,8 @@ Example configuration:
# Make all users in the hydra_admin group Hydra admins
hydra_admin = admin
- # Allow all users in the dev group to restart jobs and cancel builds
+ # Allow all users in the dev group to eval jobsets, restart jobs and cancel builds
+ dev = eval-jobset
dev = restart-jobs
dev = cancel-build
diff --git a/src/lib/Hydra/Config.pm b/src/lib/Hydra/Config.pm
index af686fca7..6aae5a5e5 100644
--- a/src/lib/Hydra/Config.pm
+++ b/src/lib/Hydra/Config.pm
@@ -95,6 +95,7 @@ sub get_legacy_ldap_config {
"hydra_bump-to-front" => [ "bump-to-front" ],
"hydra_cancel-build" => [ "cancel-build" ],
"hydra_create-projects" => [ "create-projects" ],
+ "hydra_eval-jobset" => [ "eval-jobset" ],
"hydra_restart-jobs" => [ "restart-jobs" ],
},
};
@@ -159,6 +160,7 @@ sub valid_roles {
"bump-to-front",
"cancel-build",
"create-projects",
+ "eval-jobset",
"restart-jobs",
];
}
diff --git a/src/lib/Hydra/Controller/API.pm b/src/lib/Hydra/Controller/API.pm
index cd0055517..9f8b7cba2 100644
--- a/src/lib/Hydra/Controller/API.pm
+++ b/src/lib/Hydra/Controller/API.pm
@@ -248,19 +248,24 @@ sub push : Chained('api') PathPart('push') Args(0) {
foreach my $s (@jobsets) {
my ($p, $j) = parseJobsetName($s);
my $jobset = $c->model('DB::Jobsets')->find($p, $j);
+ requireEvalJobsetPrivileges($c, $jobset->project);
next unless defined $jobset && ($force || ($jobset->project->enabled && $jobset->enabled));
triggerJobset($self, $c, $jobset, $force);
}
my @repos = split /,/, ($c->request->query_params->{repos} // "");
foreach my $r (@repos) {
- triggerJobset($self, $c, $_, $force) foreach $c->model('DB::Jobsets')->search(
+ my @jobsets = $c->model('DB::Jobsets')->search(
{ 'project.enabled' => 1, 'me.enabled' => 1 },
{
join => 'project',
where => \ [ 'exists (select 1 from JobsetInputAlts where project = me.project and jobset = me.name and value = ?)', [ 'value', $r ] ],
order_by => 'me.id DESC'
});
+ foreach my $jobset (@jobsets) {
+ requireEvalJobsetPrivileges($c, $jobset->project);
+ triggerJobset($self, $c, $jobset, $force)
+ }
}
$self->status_ok(
diff --git a/src/lib/Hydra/Helper/CatalystUtils.pm b/src/lib/Hydra/Helper/CatalystUtils.pm
index 15d50b1a1..6ccdbc4d5 100644
--- a/src/lib/Hydra/Helper/CatalystUtils.pm
+++ b/src/lib/Hydra/Helper/CatalystUtils.pm
@@ -15,6 +15,7 @@ our @EXPORT = qw(
forceLogin requireUser requireProjectOwner requireRestartPrivileges requireAdmin requirePost isAdmin isProjectOwner
requireBumpPrivileges
requireCancelBuildPrivileges
+ requireEvalJobsetPrivileges
trim
getLatestFinishedEval getFirstEval
paramToList
@@ -186,6 +187,27 @@ sub isProjectOwner {
defined $c->model('DB::ProjectMembers')->find({ project => $project, userName => $c->user->username }));
}
+sub hasEvalJobsetRole {
+ my ($c) = @_;
+ return $c->user_exists && $c->check_user_roles("eval-jobset");
+}
+
+sub mayEvalJobset {
+ my ($c, $project) = @_;
+ return
+ $c->user_exists &&
+ (isAdmin($c) ||
+ hasEvalJobsetRole($c) ||
+ isProjectOwner($c, $project));
+}
+
+sub requireEvalJobsetPrivileges {
+ my ($c, $project) = @_;
+ requireUser($c);
+ accessDenied($c, "Only the project members, administrators, and accounts with eval-jobset privileges can perform this operation.")
+ unless mayEvalJobset($c, $project);
+}
+
sub hasCancelBuildRole {
my ($c) = @_;
return $c->user_exists && $c->check_user_roles('cancel-build');
diff --git a/src/root/user.tt b/src/root/user.tt
index 76f858504..04eb6e683 100644
--- a/src/root/user.tt
+++ b/src/root/user.tt
@@ -91,6 +91,7 @@
[% INCLUDE roleoption mutable=mutable role="restart-jobs" %]
[% INCLUDE roleoption mutable=mutable role="bump-to-front" %]
[% INCLUDE roleoption mutable=mutable role="cancel-build" %]
+ [% INCLUDE roleoption mutable=mutable role="eval-jobset" %]
diff --git a/t/Hydra/Config/ldap_role_map.t b/t/Hydra/Config/ldap_role_map.t
index cb1adf46d..9287c7822 100644
--- a/t/Hydra/Config/ldap_role_map.t
+++ b/t/Hydra/Config/ldap_role_map.t
@@ -57,6 +57,7 @@ subtest "getLDAPConfig" => sub {
"hydra_cancel-build" => [ "cancel-build" ],
"hydra_create-projects" => [ "create-projects" ],
"hydra_restart-jobs" => [ "restart-jobs" ],
+ "hydra_eval-jobset" => [ "eval-jobset" ],
}
},
"The empty file and set env var make legacy mode active."
@@ -177,6 +178,7 @@ subtest "get_legacy_ldap_config" => sub {
"hydra_cancel-build" => [ "cancel-build" ],
"hydra_create-projects" => [ "create-projects" ],
"hydra_restart-jobs" => [ "restart-jobs" ],
+ "hydra_eval-jobset" => [ "eval-jobset" ],
}
},
"Legacy, default role maps are applied."
diff --git a/t/Hydra/Controller/API/checks.t b/t/Hydra/Controller/API/checks.t
index f0f51f1cf..e4c72ff2c 100644
--- a/t/Hydra/Controller/API/checks.t
+++ b/t/Hydra/Controller/API/checks.t
@@ -22,9 +22,24 @@ sub is_json {
}
my $ctx = test_context();
-
Catalyst::Test->import('Hydra');
+# Create a user to log in to
+my $user = $ctx->db->resultset('Users')->create({ username => 'alice', emailaddress => 'alice@example.com', password => '!' });
+$user->setPassword('foobar');
+$user->userroles->update_or_create({ role => 'admin' });
+
+# Login and save cookie for future requests
+my $req = request(POST '/login',
+ Referer => 'http://localhost/',
+ Content => {
+ username => 'alice',
+ password => 'foobar'
+ }
+);
+is($req->code, 302, "The login redirects");
+my $cookie = $req->header("set-cookie");
+
my $finishedBuilds = $ctx->makeAndEvaluateJobset(
expression => "one-job.nix",
build => 1
@@ -109,7 +124,10 @@ subtest "/api/push" => sub {
my $jobsetName = $jobset->name;
is($jobset->forceeval, undef, "The existing jobset is not set to be forced to eval");
- my $response = request(POST "/api/push?jobsets=$projectName:$jobsetName&force=1");
+ my $response = request(POST "/api/push?jobsets=$projectName:$jobsetName&force=1",
+ Cookie => $cookie,
+ Referer => 'http://localhost/',
+ );
ok($response->is_success, "The API enpdoint for triggering jobsets returns 200.");
my $data = is_json($response);
@@ -128,7 +146,10 @@ subtest "/api/push" => sub {
print STDERR $repo;
- my $response = request(POST "/api/push?repos=$repo&force=1");
+ my $response = request(POST "/api/push?repos=$repo&force=1",
+ Cookie => $cookie,
+ Referer => 'http://localhost/',
+ );
ok($response->is_success, "The API enpdoint for triggering jobsets returns 200.");
my $data = is_json($response);
diff --git a/t/Hydra/Controller/User/ldap-legacy.t b/t/Hydra/Controller/User/ldap-legacy.t
index 9cb197c0e..19f0c6bf8 100644
--- a/t/Hydra/Controller/User/ldap-legacy.t
+++ b/t/Hydra/Controller/User/ldap-legacy.t
@@ -24,6 +24,7 @@ $ldap->add_group("hydra_create-projects", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_restart-jobs", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_bump-to-front", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_cancel-build", $users->{"many_roles"}->{"username"});
+$ldap->add_group("hydra_eval-jobset", $users->{"many_roles"}->{"username"});
my $hydra_ldap_config = "${\$ldap->tmpdir()}/hydra_ldap_config.yaml";
LDAPContext::write_file($hydra_ldap_config, < sub {
unrelated => [],
admin => ["admin"],
not_admin => [],
- many_roles => [ "create-projects", "restart-jobs", "bump-to-front", "cancel-build" ],
+ many_roles => [ "create-projects", "restart-jobs", "bump-to-front", "cancel-build", "eval-jobset" ],
);
for my $username (keys %users_to_roles) {
my $user = $users->{$username};
diff --git a/t/Hydra/Controller/User/ldap.t b/t/Hydra/Controller/User/ldap.t
index 175b66aab..050fde233 100644
--- a/t/Hydra/Controller/User/ldap.t
+++ b/t/Hydra/Controller/User/ldap.t
@@ -24,6 +24,7 @@ $ldap->add_group("hydra_create-projects", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_restart-jobs", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_bump-to-front", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_cancel-build", $users->{"many_roles"}->{"username"});
+$ldap->add_group("hydra_eval-jobset", $users->{"many_roles"}->{"username"});
my $ctx = test_context(
@@ -76,10 +77,12 @@ my $ctx = test_context(
hydra_cancel-build = cancel-build
hydra_bump-to-front = bump-to-front
hydra_restart-jobs = restart-jobs
+ hydra_eval-jobset = eval-jobset
hydra_one_group_many_roles = create-projects
hydra_one_group_many_roles = cancel-build
hydra_one_group_many_roles = bump-to-front
+ hydra_one_group_many-roles = eval-jobset
CFG
@@ -92,7 +95,7 @@ subtest "Valid login attempts" => sub {
unrelated => [],
admin => ["admin"],
not_admin => [],
- many_roles => [ "create-projects", "restart-jobs", "bump-to-front", "cancel-build" ],
+ many_roles => [ "create-projects", "restart-jobs", "bump-to-front", "cancel-build", "eval-jobset" ],
many_roles_one_group => [ "create-projects", "bump-to-front", "cancel-build" ],
);
for my $username (keys %users_to_roles) {