Skip to content
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

III-6146 Limit access to ownership GET endpoint #1959

Merged
merged 7 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 3 additions & 1 deletion app/Ownership/OwnershipRequestHandlerServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ public function register(): void
$container->addShared(
GetOwnershipRequestHandler::class,
fn () => new GetOwnershipRequestHandler(
$container->get(OwnershipServiceProvider::OWNERSHIP_JSONLD_REPOSITORY)
$container->get(OwnershipServiceProvider::OWNERSHIP_JSONLD_REPOSITORY),
$container->get(CurrentUser::class),
$container->get(OwnershipStatusGuard::class)
)
);

Expand Down
62 changes: 62 additions & 0 deletions features/ownership/get.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
Feature: Test getting a single ownership by ID
Background:
Given I am using the UDB3 base URL
And I am using an UiTID v1 API key of consumer "uitdatabank"
And I am authorized as JWT provider v1 user "centraal_beheerder"
And I send and accept "application/json"

Scenario: Get the ownership as an admin
Given I create a minimal organizer and save the "id" as "organizerId"
And I request ownership for "auth0|64089494e980aedd96740212" on the organizer with organizerId "%{organizerId}" and save the "id" as "ownershipId"
When I send a GET request to '/ownerships/%{ownershipId}'
Then the response status should be 200
And the JSON response at id should be "%{ownershipId}"
And the JSON response at ownerId should be "auth0|64089494e980aedd96740212"
And the JSON response at itemId should be "%{organizerId}"
And the JSON response at state should be "requested"
And the JSON response at itemType should be "organizer"
And the JSON response at requesterId should be "7a583ed3-cbc1-481d-93b1-d80fff0174dd"
And the JSON response at ownerEmail should be "[email protected]"

Scenario: Get the ownership as owner
Given I create a minimal organizer and save the "id" as "organizerId"
And I request ownership for "auth0|64089494e980aedd96740212" on the organizer with organizerId "%{organizerId}" and save the "id" as "ownershipId"
When I am authorized as JWT provider v2 user "dev_e2e_test"
And I send a GET request to '/ownerships/%{ownershipId}'
Then the response status should be 200
And the JSON response at id should be "%{ownershipId}"
And the JSON response at ownerId should be "auth0|64089494e980aedd96740212"
And the JSON response at itemId should be "%{organizerId}"
And the JSON response at state should be "requested"
And the JSON response at itemType should be "organizer"
And the JSON response at requesterId should be "7a583ed3-cbc1-481d-93b1-d80fff0174dd"
And the JSON response at ownerEmail should be "[email protected]"

Scenario: Not allowed to get the ownership as an unrelated user
Given I create a minimal organizer and save the "id" as "organizerId"
And I request ownership for "auth0|64089494e980aedd96740212" on the organizer with organizerId "%{organizerId}" and save the "id" as "ownershipId"
When I am authorized as JWT provider v2 user "invoerder"
And I send a GET request to '/ownerships/%{ownershipId}'
Then the response status should be 403
And the JSON response should be:
"""
{
"type": "https://api.publiq.be/probs/auth/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "You are not allowed to get this ownership"
}
"""

Scenario: Get an unexisting ownership
When I send a GET request to '/ownerships/a0a9f9c0-84b5-471b-869e-c2c692217cc0'
Then the response status should be 404
And the JSON response should be:
"""
{
"type": "https://api.publiq.be/probs/url/not-found",
"title": "Not Found",
"status": 404,
"detail": "The Ownership with id \"a0a9f9c0-84b5-471b-869e-c2c692217cc0\" was not found."
}
"""
14 changes: 12 additions & 2 deletions src/Http/Ownership/GetOwnershipRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,34 @@
use CultuurNet\UDB3\Http\Response\JsonLdResponse;
use CultuurNet\UDB3\ReadModel\DocumentDoesNotExist;
use CultuurNet\UDB3\ReadModel\DocumentRepository;
use CultuurNet\UDB3\User\CurrentUser;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class GetOwnershipRequestHandler implements RequestHandlerInterface
{
private DocumentRepository $ownershipRepository;
private CurrentUser $currentUser;
private OwnershipStatusGuard $ownershipStatusGuard;

public function __construct(DocumentRepository $ownershipRepository)
{
public function __construct(
DocumentRepository $ownershipRepository,
CurrentUser $currentUser,
OwnershipStatusGuard $ownershipStatusGuard
) {
$this->ownershipRepository = $ownershipRepository;
$this->currentUser = $currentUser;
$this->ownershipStatusGuard = $ownershipStatusGuard;
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
$routeParameters = new RouteParameters($request);
$ownershipId = $routeParameters->getOwnershipId();

$this->ownershipStatusGuard->isAllowedToGet($ownershipId, $this->currentUser);

try {
return new JsonLdResponse(
$this->ownershipRepository->fetch($ownershipId)->getRawBody()
Expand Down
30 changes: 29 additions & 1 deletion src/Http/Ownership/OwnershipStatusGuard.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ public function __construct(
$this->permissionVoter = $permissionVoter;
}

public function isAllowedToGet(string $ownershipId, CurrentUser $currentUser): void
{
try {
$ownership = $this->ownershipSearchRepository->getById($ownershipId);
} catch (OwnershipItemNotFound $exception) {
throw ApiProblem::ownershipNotFound($ownershipId);
}

if ($currentUser->isGodUser()) {
return;
}

$isOwner = $this->permissionVoter->isAllowed(
Permission::organisatiesBewerken(),
$ownership->getItemId(),
$currentUser->getId()
);
if ($isOwner) {
return;
}

if ($ownership->getOwnerId() === $currentUser->getId()) {
return;
}

throw ApiProblem::forbidden('You are not allowed to get this ownership');
}

public function isAllowedToRequest(string $itemId, string $requesterId, CurrentUser $currentUser): void
{
$isOwner = $this->permissionVoter->isAllowed(
Expand Down Expand Up @@ -73,7 +101,7 @@ private function isAllowedToUpdateOwnership(OwnershipItem $ownership, CurrentUse
}

return $this->permissionVoter->isAllowed(
Permission::organisatiesBeheren(),
Permission::organisatiesBewerken(),
$ownership->getItemId(),
$currentUser->getId()
);
Expand Down
143 changes: 135 additions & 8 deletions tests/Http/Ownership/GetOwnershipRequestHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@
use CultuurNet\UDB3\Http\ApiProblem\AssertApiProblemTrait;
use CultuurNet\UDB3\Http\Request\Psr7RequestBuilder;
use CultuurNet\UDB3\Json;
use CultuurNet\UDB3\Ownership\OwnershipState;
use CultuurNet\UDB3\Ownership\Repositories\OwnershipItem;
use CultuurNet\UDB3\Ownership\Repositories\OwnershipItemNotFound;
use CultuurNet\UDB3\Ownership\Repositories\Search\OwnershipSearchRepository;
use CultuurNet\UDB3\ReadModel\InMemoryDocumentRepository;
use CultuurNet\UDB3\ReadModel\JsonDocument;
use CultuurNet\UDB3\Security\Permission\PermissionVoter;
use CultuurNet\UDB3\User\CurrentUser;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class GetOwnershipRequestHandlerTest extends TestCase
Expand All @@ -18,35 +25,77 @@ class GetOwnershipRequestHandlerTest extends TestCase

private InMemoryDocumentRepository $ownershipRepository;

/** @var OwnershipSearchRepository&MockObject */
private $ownerShipSearchRepository;

/** @var PermissionVoter&MockObject */
private $permissionVoter;

private GetOwnershipRequestHandler $getOwnershipRequestHandler;

protected function setUp(): void
{
$this->ownershipRepository = new InMemoryDocumentRepository();

$this->getOwnershipRequestHandler = new GetOwnershipRequestHandler($this->ownershipRepository);
$this->ownerShipSearchRepository = $this->createMock(OwnershipSearchRepository::class);

$this->permissionVoter = $this->createMock(PermissionVoter::class);

$this->getOwnershipRequestHandler = new GetOwnershipRequestHandler(
$this->ownershipRepository,
new CurrentUser('auth0|63e22626e39a8ca1264bd29b'),
new OwnershipStatusGuard(
$this->ownerShipSearchRepository,
$this->permissionVoter
)
);

parent::setUp();
}

/**
* @test
*/
public function it_handles_getting_an_ownership(): void
public function it_handles_getting_an_ownership_as_owner(): void
{
CurrentUser::configureGodUserIds([]);

$ownershipId = 'e6e1f3a0-3e5e-4b3e-8e3e-3f3e3e3e3e3e';
$this->givenItFindsAnOwnershipForUser($ownershipId, 'auth0|63e22626e39a8ca1264bd29b');

$body = $this->givenThereIsAnOwnershipDocument($ownershipId);

$this->permissionVoter->expects($this->once())
->method('isAllowed')
->willReturn(false);

$getOwnershipRequest = (new Psr7RequestBuilder())
->withRouteParameter('ownershipId', $ownershipId)
->build('GET');
$response = $this->getOwnershipRequestHandler->handle($getOwnershipRequest);

$body = Json::encode([
'id' => 'e6e1f3a0-3e5e-4b3e-8e3e-3f3e3e3e3e3e',
'itemId' => '9e68dafc-01d8-4c1c-9612-599c918b981d',
]);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals($body, $response->getBody()->getContents());
}

$this->ownershipRepository->save(new JsonDocument($ownershipId, $body));
/**
* @test
*/
public function it_handles_getting_an_ownership_as_admin(): void
{
CurrentUser::configureGodUserIds(['auth0|63e22626e39a8ca1264bd29b']);

$ownershipId = 'e6e1f3a0-3e5e-4b3e-8e3e-3f3e3e3e3e3e';
$this->givenItFindsAnOwnershipForUser($ownershipId, 'auth0|63e22626e39a8ca1264bd29b');

$body = $this->givenThereIsAnOwnershipDocument($ownershipId);

$this->permissionVoter->expects($this->never())
->method('isAllowed');

$getOwnershipRequest = (new Psr7RequestBuilder())
->withRouteParameter('ownershipId', $ownershipId)
->build('GET');
$response = $this->getOwnershipRequestHandler->handle($getOwnershipRequest);

$this->assertEquals(200, $response->getStatusCode());
Expand All @@ -56,17 +105,95 @@ public function it_handles_getting_an_ownership(): void
/**
* @test
*/
public function it_throws_an_api_problem_when_ownership_is_not_found(): void
public function it_handles_getting_an_ownership_with_permission(): void
{
CurrentUser::configureGodUserIds([]);

$ownershipId = 'e6e1f3a0-3e5e-4b3e-8e3e-3f3e3e3e3e3e';
$this->givenItFindsAnOwnershipForUser($ownershipId, 'auth0|for_another_user');

$body = $this->givenThereIsAnOwnershipDocument($ownershipId);

$this->permissionVoter->expects($this->once())
->method('isAllowed')
->willReturn(true);

$getOwnershipRequest = (new Psr7RequestBuilder())
->withRouteParameter('ownershipId', $ownershipId)
->build('GET');
$response = $this->getOwnershipRequestHandler->handle($getOwnershipRequest);

$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals($body, $response->getBody()->getContents());
}

/**
* @test
*/
public function it_forbids_getting_an_ownership_without_permission(): void
{
CurrentUser::configureGodUserIds([]);

$ownershipId = 'e6e1f3a0-3e5e-4b3e-8e3e-3f3e3e3e3e3e';

$this->givenItFindsAnOwnershipForUser($ownershipId, 'auth0|for_another_user');

$this->permissionVoter->expects($this->once())
->method('isAllowed')
->willReturn(false);

$this->givenThereIsAnOwnershipDocument($ownershipId);

$getOwnershipRequest = (new Psr7RequestBuilder())
->withRouteParameter('ownershipId', $ownershipId)
->build('GET');
$this->assertCallableThrowsApiProblem(
ApiProblem::forbidden('You are not allowed to get this ownership'),
fn () => $this->getOwnershipRequestHandler->handle($getOwnershipRequest)
);
}

/**
* @test
*/
public function it_throws_an_api_problem_when_ownership_is_not_found(): void
{
$ownershipId = 'e6e1f3a0-3e5e-4b3e-8e3e-3f3e3e3e3e3e';

$this->ownerShipSearchRepository->expects($this->once())
->method('getById')
->willThrowException(OwnershipItemNotFound::byId($ownershipId));

$getOwnershipRequest = (new Psr7RequestBuilder())
->withRouteParameter('ownershipId', $ownershipId)
->build('GET');
$this->assertCallableThrowsApiProblem(
ApiProblem::ownershipNotFound($ownershipId),
fn () => $this->getOwnershipRequestHandler->handle($getOwnershipRequest)
);
}

private function givenItFindsAnOwnershipForUser(string $ownershipId, string $userId): void
{
$this->ownerShipSearchRepository->expects($this->once())
->method('getById')
->willReturn(new OwnershipItem(
$ownershipId,
'9e68dafc-01d8-4c1c-9612-599c918b981d',
'organizer',
$userId,
OwnershipState::requested()->toString()
));
}

private function givenThereIsAnOwnershipDocument(string $ownershipId): string
{
$body = Json::encode([
'id' => $ownershipId,
'itemId' => '9e68dafc-01d8-4c1c-9612-599c918b981d',
]);
$this->ownershipRepository->save(new JsonDocument($ownershipId, $body));

return $body;
}
}
Loading