enforce text ownership on node endpoints
getNodesOfText, createNode, and bulkCreateNodes now require the session user, look up the target text, and respond 403 unless the user owns the text or is an admin. paves the way for moving these endpoints out of the admin-only group.
This commit is contained in:
parent
e56cb56ce7
commit
7473af4163
1 changed files with 104 additions and 2 deletions
|
|
@ -8,7 +8,9 @@ use App\Node\NodeRepository;
|
||||||
use App\Node\UseCases\BulkCreateNodes;
|
use App\Node\UseCases\BulkCreateNodes;
|
||||||
use App\Node\UseCases\CreateNode;
|
use App\Node\UseCases\CreateNode;
|
||||||
use App\Node\UseCases\CreateNodeRequest;
|
use App\Node\UseCases\CreateNodeRequest;
|
||||||
|
use App\Text\Text;
|
||||||
use App\Text\TextRepository;
|
use App\Text\TextRepository;
|
||||||
|
use App\User\User;
|
||||||
use DomainException;
|
use DomainException;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
|
@ -20,14 +22,34 @@ class NodeController
|
||||||
private TextRepository $textRepository,
|
private TextRepository $textRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getNodesOfText(Response $response, int $textId): Response
|
public function getNodesOfText(
|
||||||
{
|
Request $request,
|
||||||
|
Response $response,
|
||||||
|
int $textId,
|
||||||
|
): Response {
|
||||||
|
$user = $request->getAttribute('user');
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
return $this->errorResponse(
|
||||||
|
$response,
|
||||||
|
401,
|
||||||
|
'unauthenticated'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$text = $this->textRepository->find($textId);
|
$text = $this->textRepository->find($textId);
|
||||||
|
|
||||||
if ($text === null) {
|
if ($text === null) {
|
||||||
return $response->withStatus(404);
|
return $response->withStatus(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->userMayAccessText($user, $text)) {
|
||||||
|
return $this->errorResponse(
|
||||||
|
$response,
|
||||||
|
403,
|
||||||
|
'forbidden'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$nodes = $this->nodeRepository->findByTextId($textId);
|
$nodes = $this->nodeRepository->findByTextId($textId);
|
||||||
|
|
||||||
$data = array_map(function ($node) {
|
$data = array_map(function ($node) {
|
||||||
|
|
@ -47,12 +69,32 @@ class NodeController
|
||||||
Response $response,
|
Response $response,
|
||||||
CreateNode $createNodeUseCase,
|
CreateNode $createNodeUseCase,
|
||||||
): Response {
|
): Response {
|
||||||
|
$user = $request->getAttribute('user');
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
return $this->errorResponse(
|
||||||
|
$response,
|
||||||
|
401,
|
||||||
|
'unauthenticated'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$data = json_decode((string) $request->getBody(), true) ?? [];
|
$data = json_decode((string) $request->getBody(), true) ?? [];
|
||||||
|
|
||||||
$textId = isset($data['textId']) ? (int) $data['textId'] : null;
|
$textId = isset($data['textId']) ? (int) $data['textId'] : null;
|
||||||
$title = $data['title'] ?? null;
|
$title = $data['title'] ?? null;
|
||||||
$parentNodeId = isset($data['parentNodeId']) ? (int) $data['parentNodeId'] : null;
|
$parentNodeId = isset($data['parentNodeId']) ? (int) $data['parentNodeId'] : null;
|
||||||
|
|
||||||
|
if ($textId !== null) {
|
||||||
|
$ownershipResponse = $this->checkTextOwnership(
|
||||||
|
$user,
|
||||||
|
$textId,
|
||||||
|
$response,
|
||||||
|
);
|
||||||
|
if ($ownershipResponse !== null) {
|
||||||
|
return $ownershipResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$node = $createNodeUseCase->execute(new CreateNodeRequest(
|
$node = $createNodeUseCase->execute(new CreateNodeRequest(
|
||||||
textId: $textId,
|
textId: $textId,
|
||||||
|
|
@ -80,6 +122,15 @@ class NodeController
|
||||||
Response $response,
|
Response $response,
|
||||||
BulkCreateNodes $bulkCreateNodesUseCase,
|
BulkCreateNodes $bulkCreateNodesUseCase,
|
||||||
): Response {
|
): Response {
|
||||||
|
$user = $request->getAttribute('user');
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
return $this->errorResponse(
|
||||||
|
$response,
|
||||||
|
401,
|
||||||
|
'unauthenticated'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$data = json_decode((string) $request->getBody(), true) ?? [];
|
$data = json_decode((string) $request->getBody(), true) ?? [];
|
||||||
|
|
||||||
$textId = isset($data['textId']) ? (int) $data['textId'] : null;
|
$textId = isset($data['textId']) ? (int) $data['textId'] : null;
|
||||||
|
|
@ -87,6 +138,17 @@ class NodeController
|
||||||
$titlePrefix = isset($data['titlePrefix']) ? (string) $data['titlePrefix'] : null;
|
$titlePrefix = isset($data['titlePrefix']) ? (string) $data['titlePrefix'] : null;
|
||||||
$count = isset($data['count']) ? (int) $data['count'] : null;
|
$count = isset($data['count']) ? (int) $data['count'] : null;
|
||||||
|
|
||||||
|
if ($textId !== null) {
|
||||||
|
$ownershipResponse = $this->checkTextOwnership(
|
||||||
|
$user,
|
||||||
|
$textId,
|
||||||
|
$response,
|
||||||
|
);
|
||||||
|
if ($ownershipResponse !== null) {
|
||||||
|
return $ownershipResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$nodes = $bulkCreateNodesUseCase->execute(new BulkCreateNodesRequest(
|
$nodes = $bulkCreateNodesUseCase->execute(new BulkCreateNodesRequest(
|
||||||
textId: $textId,
|
textId: $textId,
|
||||||
|
|
@ -113,4 +175,44 @@ class NodeController
|
||||||
$response->getBody()->write(json_encode(array_values($result)));
|
$response->getBody()->write(json_encode(array_values($result)));
|
||||||
return $response->withStatus(201)->withHeader('Content-Type', 'application/json');
|
return $response->withStatus(201)->withHeader('Content-Type', 'application/json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkTextOwnership(
|
||||||
|
User $user,
|
||||||
|
int $textId,
|
||||||
|
Response $response,
|
||||||
|
): ?Response {
|
||||||
|
$text = $this->textRepository->find($textId);
|
||||||
|
if ($text === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!$this->userMayAccessText($user, $text)) {
|
||||||
|
return $this->errorResponse(
|
||||||
|
$response,
|
||||||
|
403,
|
||||||
|
'forbidden'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function userMayAccessText(User $user, Text $text): bool
|
||||||
|
{
|
||||||
|
if ($user->isAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return $text->getUser()->getId() === $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function errorResponse(
|
||||||
|
Response $response,
|
||||||
|
int $status,
|
||||||
|
string $message,
|
||||||
|
): Response {
|
||||||
|
$response->getBody()->write(
|
||||||
|
json_encode(['error' => $message])
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response->withStatus($status)
|
||||||
|
->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue