Skip to content

Fix user add and update api endpoints #3040

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 73 additions & 60 deletions webapp/src/Controller/API/UserController.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);
Comment on lines +1 to +3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason you wanted to change this? I don't think we want the newlines there.


namespace App\Controller\API;

Expand All @@ -23,7 +25,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
* @extends AbstractRestController<User, User>
Expand All @@ -41,7 +46,8 @@
DOMJudgeService $dj,
ConfigurationService $config,
EventLogService $eventLogService,
protected readonly ImportExportService $importExportService
protected readonly ImportExportService $importExportService,
protected readonly ValidatorInterface $validator,
) {
parent::__construct($entityManager, $dj, $config, $eventLogService);
}
Expand Down Expand Up @@ -86,9 +92,10 @@
$message = null;
$result = -1;
if ((($tsvFile && ($result = $this->importExportService->importTsv('groups', $tsvFile, $message))) ||
($jsonFile && ($result = $this->importExportService->importJson('groups', $jsonFile, $message)))) &&
$result >= 0) {
return "$result new group(s) successfully added.";
($jsonFile && ($result = $this->importExportService->importJson('groups', $jsonFile, $message)))) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you change anything here. Can either first run your linter on the file and commit or remove the changes your linter made (my preference)?

$result >= 0
) {
return "$result new group(s) successfully added.";
} else {
throw new BadRequestHttpException("Error while adding groups: $message");
}
Expand All @@ -110,7 +117,8 @@
property: 'json',
description: 'The organizations.json files to import.',
type: 'string',
format: 'binary'),
format: 'binary'
),
]
)
)
Expand All @@ -121,7 +129,8 @@
$message = null;
/** @var UploadedFile|null $jsonFile */
$jsonFile = $request->files->get('json');
if ($jsonFile &&
if (

Check failure on line 132 in webapp/src/Controller/API/UserController.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected 0 spaces after opening bracket; newline found
$jsonFile &&
($result = $this->importExportService->importJson('organizations', $jsonFile, $message)) &&
$result >= 0
) {
Expand Down Expand Up @@ -171,8 +180,9 @@
}
$message = null;
if ((($tsvFile && ($result = $this->importExportService->importTsv('teams', $tsvFile, $message))) ||
($jsonFile && ($result = $this->importExportService->importJson('teams', $jsonFile, $message)))) &&
$result >= 0) {
($jsonFile && ($result = $this->importExportService->importJson('teams', $jsonFile, $message)))) &&
$result >= 0
) {
// TODO: better return all teams here?
return "$result new team(s) successfully added.";
} else {
Expand Down Expand Up @@ -235,8 +245,9 @@

$message = null;
if ((($tsvFile && ($result = $this->importExportService->importTsv('accounts', $tsvFile, $message))) ||
($jsonFile && ($result = $this->importExportService->importJson('accounts', $jsonFile, $message)))) &&
$result >= 0) {
($jsonFile && ($result = $this->importExportService->importJson('accounts', $jsonFile, $message)))) &&
$result >= 0
) {
// TODO: better return all accounts here?
return "$result new account(s) successfully added.";
} else {
Expand All @@ -259,11 +270,13 @@
)
)]
#[OA\Parameter(ref: '#/components/parameters/idlist')]
#[OA\Parameter(
name: 'team_id',
description: 'Only show users for the given team',
in: 'query',
schema: new OA\Schema(type: 'string'))
#[
OA\Parameter(
name: 'team_id',
description: 'Only show users for the given team',
in: 'query',
schema: new OA\Schema(type: 'string')
)
]
public function listAction(Request $request): Response
{
Expand Down Expand Up @@ -291,13 +304,12 @@
* Add a new user.
*/
#[IsGranted('ROLE_API_WRITER')]
#[Rest\Post]
#[Rest\Post()]
#[OA\RequestBody(
required: true,
content: [
new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(ref: new Model(type: AddUser::class))
new OA\JsonContent(
ref: new Model(type: AddUser::class)
),
]
)]
Expand All @@ -307,24 +319,23 @@
content: new Model(type: User::class)
)]
public function addAction(
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST, validationGroups: ['add'])]
AddUser $addUser,
Request $request
): Response {
return $this->addOrUpdateUser($addUser, $request);
return $this->addOrUpdateUser($addUser, new User(), $request);
}

/**
* Update an existing User or create one with the given ID
* Update an existing User
*/
#[IsGranted('ROLE_API_WRITER')]
#[Rest\Put('/{id}')]
#[Rest\Patch('/{id}')]
#[OA\RequestBody(
required: true,
content: [
new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(ref: new Model(type: UpdateUser::class))
new OA\JsonContent(
ref: new Model(type: UpdateUser::class)
),
]
)]
Expand All @@ -335,58 +346,55 @@
)]
public function updateAction(
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
UpdateUser $updateUser,
UpdateUser $mutateUser,
string $id,
Request $request
): Response {
return $this->addOrUpdateUser($updateUser, $request);
/** @var User|null $user */
$user = $this->em->getRepository(User::class)->findOneBy(['externalid' => $id]);
if ($user === null) {
throw new NotFoundHttpException(sprintf("User with id %s not found", $id));
}
return $this->addOrUpdateUser($mutateUser, $user, $request);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name addOrUpdate is now misleading. You only allow for update here.

}

protected function addOrUpdateUser(AddUser $addUser, Request $request): Response
protected function addOrUpdateUser(AddUser|UpdateUser $mutateUser, User $user, Request $request): Response
{
if ($addUser instanceof UpdateUser && !$addUser->id) {
throw new BadRequestHttpException('`id` field is required');
if ($mutateUser->username !== null) {
$user->setUsername($mutateUser->username);
}

if ($this->em->getRepository(User::class)->findOneBy(['username' => $addUser->username])) {
throw new BadRequestHttpException(sprintf("User %s already exists", $addUser->username));
if ($mutateUser->name !== null) {
$user->setName($mutateUser->name);
}

$user = new User();
if ($addUser instanceof UpdateUser) {
$existingUser = $this->em->getRepository(User::class)->findOneBy(['externalid' => $addUser->id]);
if ($existingUser) {
$user = $existingUser;
}
if ($mutateUser->email !== null) {
$user->setEmail($mutateUser->email);
}
$user
->setUsername($addUser->username)
->setName($addUser->name)
->setEmail($addUser->email)
->setIpAddress($addUser->ip)
->setPlainPassword($addUser->password)
->setEnabled($addUser->enabled ?? true);

if ($addUser instanceof UpdateUser) {
$user->setExternalid($addUser->id);
if ($mutateUser->ip !== null) {
$user->setIpAddress($mutateUser->ip);
}

if ($addUser->teamId) {
if ($mutateUser->password !== null) {
$user->setPlainPassword($mutateUser->password);
}
if ($mutateUser->enabled !== null) {
$user->setEnabled($mutateUser->enabled);
}
if ($mutateUser->teamId) {
/** @var Team|null $team */
$team = $this->em->createQueryBuilder()
->from(Team::class, 't')
->select('t')
->andWhere('t.externalid = :team')
->setParameter('team', $addUser->teamId)
->setParameter('team', $mutateUser->teamId)
->getQuery()
->getOneOrNullResult();

if ($team === null) {
throw new BadRequestHttpException(sprintf("Team %s not found", $addUser->teamId));
throw new BadRequestHttpException(sprintf("Team %s not found", $mutateUser->teamId));
}
$user->setTeam($team);
}

$roles = $addUser->roles;
$roles = $mutateUser->roles;
// For the file import we change a CDS user to the roles needed for ICPC CDS.
if ($user->getUsername() === 'cds') {
$roles = ['cds'];
Expand All @@ -408,18 +416,23 @@
$user->addUserRole($role);
}

$validation = $this->validator->validate($user);
if (count($validation) > 0) {
throw new ValidationFailedException($user, $validation);
}

$this->em->persist($user);
$this->em->flush();
$this->dj->auditlog('user', $user->getUserid(), 'added');
$this->dj->auditlog('user', $user->getUserid(), 'updated');

return $this->renderCreateData($request, $user, 'user', $user->getUserid());
}

protected function getQueryBuilder(Request $request): QueryBuilder
{
$queryBuilder = $this->em->createQueryBuilder()
->from(User::class, 'u')
->select('u');
->from(User::class, 'u')
->select('u');

if ($request->query->has('team')) {
$queryBuilder
Expand Down
37 changes: 18 additions & 19 deletions webapp/src/DataTransferObject/AddUser.php
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace App\DataTransferObject;

use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

#[OA\Schema(required: ['username', 'name', 'roles'])]
class AddUser
{
#[Assert\NotBlank]
public string $username;
#[Assert\NotBlank]
public string $name;
public ?string $id = null;
public ?string $email = null;
public ?string $ip = null;
#[OA\Property(format: 'password')]
public ?string $password = null;
public ?bool $enabled = null;
public ?string $teamId = null;
/**
* @param array<string> $roles
* @var array<string>
*/
public function __construct(
public readonly string $username,
public readonly string $name,
#[OA\Property(format: 'email', nullable: true)]
public readonly ?string $email,
#[OA\Property(nullable: true)]
public readonly ?string $ip,
#[OA\Property(format: 'password', nullable: true)]
public readonly ?string $password,
#[OA\Property(nullable: true)]
public readonly ?bool $enabled,
#[OA\Property(nullable: true)]
public readonly ?string $teamId,
#[Serializer\Type('array<string>')]
public readonly array $roles,
) {}
#[Serializer\Type('array<string>')]
public array $roles = [];
}
35 changes: 19 additions & 16 deletions webapp/src/DataTransferObject/UpdateUser.php
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace App\DataTransferObject;

use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;

#[OA\Schema(required: ['id', 'username', 'name', 'roles'])]
class UpdateUser extends AddUser
class UpdateUser
{
public function __construct(
public readonly string $id,
string $username,
string $name,
?string $email,
?string $ip,
?string $password,
?bool $enabled,
?string $teamId,
array $roles
) {
parent::__construct($username, $name, $email, $ip, $password, $enabled, $teamId, $roles);
}
public ?string $id = null;
public ?string $username = null;
public ?string $name = null;
public ?string $email = null;
public ?string $ip = null;
#[OA\Property(format: 'password')]
public ?string $password = null;
public ?bool $enabled = null;
public ?string $teamId = null;
/**
* @var array<string>
*/
#[Serializer\Type('array<string>')]
public array $roles = [];
}
18 changes: 13 additions & 5 deletions webapp/src/Entity/User.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Controller\API\AbstractRestController as ARC;
Expand Down Expand Up @@ -132,7 +135,7 @@ class User extends BaseApiEntity implements
#[ORM\JoinTable(name: 'userrole')]
#[ORM\JoinColumn(name: 'userid', referencedColumnName: 'userid', onDelete: 'CASCADE')]
#[ORM\InverseJoinColumn(name: 'roleid', referencedColumnName: 'roleid', onDelete: 'CASCADE')]
#[Assert\Count(min: 1)]
#[Assert\Count(min: 1, minMessage: 'User should have at least {{ limit }} role')]
#[Serializer\Exclude]
private Collection $user_roles;

Expand Down Expand Up @@ -439,9 +442,14 @@ public function getType(): ?string
// Types allowed by the CCS Specs Contest API in order of most permissions to least.
// Either key=>value where key is the DOMjudge role and value is the API account type or
// only value, where both the DOMjudge role and API type are the same.
$allowedTypes = ['admin', 'api_writer' => 'admin', 'api_reader' => 'admin',
'jury' => 'judge', 'api_source_reader' => 'judge',
'team'];
$allowedTypes = [
'admin',
'api_writer' => 'admin',
'api_reader' => 'admin',
'jury' => 'judge',
'api_source_reader' => 'judge',
'team'
];
foreach ($allowedTypes as $role => $allowedType) {
if (is_numeric($role)) {
$role = $allowedType;
Expand Down
Loading
Loading