Skip to content

Commit 265be80

Browse files
committed
Add new roles for problem/contest changes via API
This is for an usecase like EUC where there is an Ad-Hoc group which doesn't know each other yet (or even the system). The responsibility for the upload of the problems lies with one team which does not want admin access to make sure nothing gets broken. The same for changing the contest as BAPCtools does for example. Also extended the tests for admin access to now also check for the new roles while making sure admin also keeps the rights by transitivity.
1 parent ab267ec commit 265be80

File tree

8 files changed

+155
-34
lines changed

8 files changed

+155
-34
lines changed

webapp/config/packages/security.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
security:
44
role_hierarchy:
55
ROLE_JURY: [ROLE_CLARIFICATION_RW, ROLE_API, ROLE_API_READER, ROLE_API_SOURCE_READER]
6-
ROLE_ADMIN: [ROLE_JURY, ROLE_JUDGEHOST, ROLE_API_WRITER]
6+
ROLE_ADMIN: [ROLE_JURY, ROLE_JUDGEHOST, ROLE_API_WRITER,
7+
ROLE_API_PROBLEM_CHANGE, ROLE_API_CONTEST_CHANGE]
78
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
89

9-
1010
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
1111
password_hashers:
1212
App\Entity\User:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
final class Version20240629154640 extends AbstractMigration
11+
{
12+
private const NEW_ROLES = ['api_problem_change' => 'API Problem Changer',
13+
'api_contest_change' => 'API Contest Changer'];
14+
15+
public function getDescription(): string
16+
{
17+
return 'Add new roles to the database.';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
foreach (self::NEW_ROLES as $role => $description) {
23+
$this->addSql(
24+
'INSERT INTO role (`role`, `description`) VALUES (:role, :desc)',
25+
['role' => $role, 'desc' => $description]
26+
);
27+
}
28+
}
29+
30+
public function down(Schema $schema): void
31+
{
32+
foreach (array_keys(self::NEW_ROLES) as $role) {
33+
$this->addSql('DELETE FROM role WHERE role = ' . $role );
34+
}
35+
}
36+
37+
public function isTransactional(): bool
38+
{
39+
return false;
40+
}
41+
}

webapp/src/Controller/API/ContestController.php

+6-6
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public function __construct(
7474
* Add a new contest.
7575
* @throws BadRequestHttpException
7676
*/
77-
#[IsGranted('ROLE_ADMIN')]
77+
#[IsGranted('ROLE_API_CONTEST_CHANGE')]
7878
#[Rest\Post('')]
7979
#[OA\RequestBody(
8080
required: true,
@@ -200,7 +200,7 @@ public function bannerAction(Request $request, string $cid): Response
200200
/**
201201
* Delete the banner for the given contest.
202202
*/
203-
#[IsGranted('ROLE_ADMIN')]
203+
#[IsGranted('ROLE_API_CONTEST_CHANGE')]
204204
#[Rest\Delete('/{cid}/banner', name: 'delete_contest_banner')]
205205
#[OA\Response(response: 204, description: 'Deleting banner succeeded')]
206206
#[OA\Parameter(ref: '#/components/parameters/cid')]
@@ -220,7 +220,7 @@ public function deleteBannerAction(Request $request, string $cid): Response
220220
/**
221221
* Set the banner for the given contest.
222222
*/
223-
#[IsGranted('ROLE_ADMIN')]
223+
#[IsGranted('ROLE_API_CONTEST_CHANGE')]
224224
#[Rest\Post("/{cid}/banner", name: 'post_contest_banner')]
225225
#[Rest\Put("/{cid}/banner", name: 'put_contest_banner')]
226226
#[OA\RequestBody(
@@ -268,7 +268,7 @@ public function setBannerAction(Request $request, string $cid, ValidatorInterfac
268268
/**
269269
* Delete the problemset document for the given contest.
270270
*/
271-
#[IsGranted('ROLE_ADMIN')]
271+
#[IsGranted('ROLE_API_CONTEST_CHANGE')]
272272
#[Rest\Delete('/{cid}/problemset', name: 'delete_contest_problemset')]
273273
#[OA\Response(response: 204, description: 'Deleting problemset document succeeded')]
274274
#[OA\Parameter(ref: '#/components/parameters/cid')]
@@ -288,7 +288,7 @@ public function deleteProblemsetAction(Request $request, string $cid): Response
288288
/**
289289
* Set the problemset document for the given contest.
290290
*/
291-
#[IsGranted('ROLE_ADMIN')]
291+
#[IsGranted('ROLE_API_CONTEST_CHANGE')]
292292
#[Rest\Post("/{cid}/problemset", name: 'post_contest_problemset')]
293293
#[Rest\Put("/{cid}/problemset", name: 'put_contest_problemset')]
294294
#[OA\RequestBody(
@@ -384,7 +384,7 @@ public function problemsetAction(Request $request, string $cid): Response
384384
* Change the start time or unfreeze (thaw) time of the given contest.
385385
* @throws NonUniqueResultException
386386
*/
387-
#[IsGranted('ROLE_API_WRITER')]
387+
#[IsGranted(new Expression("is_granted('ROLE_API_WRITER') or is_granted('ROLE_API_CONTEST_CHANGE')"))]
388388
#[Rest\Patch('/{cid}')]
389389
#[OA\RequestBody(
390390
required: true,

webapp/src/Controller/API/ProblemController.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function __construct(
6161
* @throws BadRequestHttpException
6262
* @throws NonUniqueResultException
6363
*/
64-
#[IsGranted('ROLE_ADMIN')]
64+
#[IsGranted('ROLE_API_PROBLEM_CHANGE')]
6565
#[Rest\Post('/add-data')]
6666
#[OA\RequestBody(
6767
required: true,
@@ -176,7 +176,7 @@ public function listAction(Request $request): Response
176176
* @return array{problem_id: string, messages: array<string, string[]>}
177177
* @throws NonUniqueResultException
178178
*/
179-
#[IsGranted('ROLE_ADMIN')]
179+
#[IsGranted('ROLE_API_PROBLEM_CHANGE')]
180180
#[Rest\Post('')]
181181
#[OA\RequestBody(
182182
required: true,
@@ -237,7 +237,7 @@ public function addProblemAction(Request $request): array
237237
/**
238238
* Unlink a problem from this contest.
239239
*/
240-
#[IsGranted('ROLE_ADMIN')]
240+
#[IsGranted('ROLE_API_PROBLEM_CHANGE')]
241241
#[Rest\Delete('/{id}')]
242242
#[OA\Response(response: 204, description: 'Problem unlinked from contest succeeded')]
243243
#[OA\Parameter(ref: '#/components/parameters/id')]
@@ -290,7 +290,7 @@ public function unlinkProblemAction(Request $request, string $id): Response
290290
/**
291291
* Link an existing problem to this contest.
292292
*/
293-
#[IsGranted('ROLE_ADMIN')]
293+
#[IsGranted('ROLE_API_PROBLEM_CHANGE')]
294294
#[Rest\Put('/{id}')]
295295
#[OA\Response(
296296
response: 200,

webapp/src/DataFixtures/DefaultData/RoleFixture.php

+11-9
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@ public function load(ObjectManager $manager): void
2020
{
2121
// Mapping from role to description
2222
$roles = [
23-
'admin' => 'Administrative User',
24-
'jury' => 'Jury User',
25-
'team' => 'Team Member',
26-
'balloon' => 'Balloon runner',
27-
'judgehost' => '(Internal/System) Judgehost',
28-
'api_reader' => 'API reader',
29-
'api_writer' => 'API writer',
30-
'api_source_reader' => 'Source code reader',
31-
'clarification_rw' => 'Clarification handler',
23+
'admin' => 'Administrative User',
24+
'jury' => 'Jury User',
25+
'team' => 'Team Member',
26+
'balloon' => 'Balloon runner',
27+
'judgehost' => '(Internal/System) Judgehost',
28+
'api_reader' => 'API reader',
29+
'api_writer' => 'API writer',
30+
'api_source_reader' => 'Source code reader',
31+
'clarification_rw' => 'Clarification handler',
32+
'api_problem_change' => 'API Problem Changer',
33+
'api_contest_change' => 'API Contest Changer'
3234
];
3335
foreach ($roles as $roleName => $description) {
3436
if (!($role = $manager->getRepository(Role::class)->findOneBy(['dj_role' => $roleName]))) {

webapp/tests/Unit/Controller/API/BaseTestCase.php

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
abstract class BaseTestCase extends BaseBaseTestCase
1313
{
1414
protected static array $rootEndpoints = ['contests', 'judgehosts', 'users'];
15+
protected static string $testedRole = 'unset';
1516

1617
/** @var KernelBrowser */
1718
protected KernelBrowser $client;
@@ -373,4 +374,10 @@ public function provideSingleNotFound(): Generator
373374
yield [$id];
374375
}
375376
}
377+
378+
protected function provideAllowedUsers(): Generator
379+
{
380+
yield ['admin', ['admin']];
381+
yield ['team', [static::$testedRole]];
382+
}
376383
}

webapp/tests/Unit/Controller/API/ContestControllerAdminTest.php

+35-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
class ContestControllerAdminTest extends ContestControllerTest
2222
{
2323
protected ?string $apiUser = 'admin';
24+
protected static string $testedRole = 'api_contest_change';
2425

2526
private function parseSortYaml(string $yamlString): array
2627
{
@@ -29,7 +30,10 @@ private function parseSortYaml(string $yamlString): array
2930
return $new;
3031
}
3132

32-
public function testAddYaml(): void
33+
/**
34+
* @dataProvider provideAllowedUsers
35+
*/
36+
public function testAddYaml(string $user, array $newRoles): void
3337
{
3438
$yaml = <<<EOF
3539
duration: 2:00:00
@@ -69,6 +73,8 @@ public function testAddYaml(): void
6973
scoreboard_freeze_duration: 0:30:00
7074
EOF;
7175

76+
$this->roles = $newRoles;
77+
self::setUp();
7278
$url = $this->helperGetEndpointURL($this->apiEndpoint);
7379
$tempYamlFile = tempnam(sys_get_temp_dir(), "/contest-yaml-");
7480
file_put_contents($tempYamlFile, $yaml);
@@ -89,7 +95,10 @@ public function testAddYaml(): void
8995
self::assertNull($this->getContest($cid)->getDeactivatetime());
9096
}
9197

92-
public function testAddJson(): void
98+
/**
99+
* @dataProvider provideAllowedUsers
100+
*/
101+
public function testAddJson(string $user, array $newRoles): void
93102
{
94103
$json = <<<EOF
95104
{
@@ -103,6 +112,8 @@ public function testAddJson(): void
103112
}
104113
EOF;
105114

115+
$this->roles = $newRoles;
116+
self::setUp();
106117
$url = $this->helperGetEndpointURL($this->apiEndpoint);
107118
$tempJsonFile = tempnam(sys_get_temp_dir(), "/contest-json-");
108119
file_put_contents($tempJsonFile, $json);
@@ -121,8 +132,13 @@ protected function getContest(int|string $cid): Contest
121132
return static::getContainer()->get(EntityManagerInterface::class)->getRepository(Contest::class)->findOneBy(['externalid' => $cid]);
122133
}
123134

124-
public function testBannerManagement(): void
135+
/**
136+
* @dataProvider provideAllowedUsers
137+
*/
138+
public function testBannerManagement(string $user, array $newRoles): void
125139
{
140+
$this->roles = $newRoles;
141+
self::setUp();
126142
// First, make sure we have no banner
127143
$id = 1;
128144
if ($this->objectClassForExternalId !== null) {
@@ -163,8 +179,13 @@ public function testBannerManagement(): void
163179
self::assertArrayNotHasKey('banner', $object);
164180
}
165181

166-
public function testProblemsetManagement(): void
182+
/**
183+
* @dataProvider provideAllowedUsers
184+
*/
185+
public function testProblemsetManagement(string $user, array $newRoles): void
167186
{
187+
$this->roles = $newRoles;
188+
self::setUp();
168189
// First, make sure we have no problemset document
169190
$id = 1;
170191
if ($this->objectClassForExternalId !== null) {
@@ -233,7 +254,10 @@ public function testChangeTimes(
233254
array $extraFixtures = [],
234255
bool $checkUnfreezeTime = false,
235256
bool $convertRelativeTimes = false,
257+
array $newRoles = [],
236258
): void {
259+
$this->roles = $newRoles;
260+
self::setUp();
237261
$this->loadFixture(DemoPreStartContestFixture::class);
238262
$this->loadFixtures($extraFixtures);
239263
$id = 1;
@@ -299,14 +323,18 @@ public function provideChangeTimes(): Generator
299323
yield [['id' => 1, 'scoreboard_thaw_time' => '+15 seconds', 'force' => true], 204, null, [DemoPostUnfreezeContestFixture::class], false, true];
300324
yield [['id' => 1, 'scoreboard_thaw_time' => '+15 seconds'], 204, null, [], false, true];
301325
yield [['id' => 1, 'scoreboard_thaw_time' => '-15 seconds'], 200, 'Demo contest', [], true, true];
326+
327+
// Show that this works for both roles
328+
yield [['id' => 1, 'scoreboard_thaw_time' => '-14 seconds'], 200, 'Demo contest', [], true, true, ['admin']];
329+
yield [['id' => 1, 'scoreboard_thaw_time' => '-13 seconds'], 200, 'Demo contest', [], true, true, ['api_contest_change']];
302330
}
303331

304332
/**
305333
* @dataProvider provideNewContest
306334
*/
307335
public function testActivateTimeContestYaml(
308336
string $activateTime, string $startTime, ?string $deactivateTime,
309-
bool $setActivate, bool $setDeactivate
337+
bool $setActivate, bool $setDeactivate, array $newRoles = [],
310338
): void {
311339
$yaml = <<<EOF
312340
duration: 2:00:00
@@ -322,6 +350,8 @@ public function testActivateTimeContestYaml(
322350
id: anothereruption
323351
EOF;
324352

353+
$this->roles = $newRoles;
354+
self::setUp();
325355
if ($setActivate) {
326356
$yaml = "activate_time: ".$activateTime."\n".$yaml;
327357
}

0 commit comments

Comments
 (0)