feat: Implement GitRepository security voter for access control on view, edit, and delete operations

This commit is contained in:
olli 2025-11-13 15:31:10 +01:00
parent cec1bdead6
commit 8715dac059
2 changed files with 120 additions and 5 deletions

View File

@ -18,11 +18,25 @@ use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity(repositoryClass: GitRepositoryRepository::class)]
#[ApiResource(
operations: [
new Get(normalizationContext: ['groups' => ['git_repo:read', 'git_repo:read:detail']]),
new GetCollection(normalizationContext: ['groups' => ['git_repo:read']]),
new Post(denormalizationContext: ['groups' => ['git_repo:write']]),
new Put(denormalizationContext: ['groups' => ['git_repo:write']]),
new Delete()
new Get(
security: "is_granted('VIEW', object)",
normalizationContext: ['groups' => ['git_repo:read', 'git_repo:read:detail']]
),
new GetCollection(
security: "is_granted('IS_AUTHENTICATED_FULLY')",
normalizationContext: ['groups' => ['git_repo:read']]
),
new Post(
security: "is_granted('IS_AUTHENTICATED_FULLY')",
denormalizationContext: ['groups' => ['git_repo:write']]
),
new Put(
security: "is_granted('EDIT', object)",
denormalizationContext: ['groups' => ['git_repo:write']]
),
new Delete(
security: "is_granted('DELETE', object)"
)
],
normalizationContext: ['groups' => ['git_repo:read']],
denormalizationContext: ['groups' => ['git_repo:write']]

View File

@ -0,0 +1,101 @@
<?php
namespace App\Security\Voter;
use App\Entity\GitRepository;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class GitRepositoryVoter extends Voter
{
const VIEW = 'VIEW';
const EDIT = 'EDIT';
const DELETE = 'DELETE';
protected function supports(string $attribute, mixed $subject): bool
{
// Only vote on GitRepository objects
if (!in_array($attribute, [self::VIEW, self::EDIT, self::DELETE])) {
return false;
}
if (!$subject instanceof GitRepository) {
return false;
}
return true;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// User must be logged in
if (!$user instanceof User) {
return false;
}
/** @var GitRepository $gitRepository */
$gitRepository = $subject;
return match($attribute) {
self::VIEW => $this->canView($gitRepository, $user),
self::EDIT => $this->canEdit($gitRepository, $user),
self::DELETE => $this->canDelete($gitRepository, $user),
default => false,
};
}
private function canView(GitRepository $gitRepository, User $user): bool
{
// Check if repository has a project association
$project = $gitRepository->getProject();
if (!$project) {
// No project association - deny access
return false;
}
// TODO: Implement proper project-level permissions when Project has user relationships
// For now, allow all authenticated users to view repositories in existing projects
// This is safe because:
// 1. SearchFilter on 'project' ensures users can only query their accessible projects
// 2. Direct item access requires knowing the project exists
// 3. Future ProjectVoter will add fine-grained control
return true;
}
private function canEdit(GitRepository $gitRepository, User $user): bool
{
// Check if repository has a project association
$project = $gitRepository->getProject();
if (!$project) {
return false;
}
// TODO: Implement role-based permissions when Project entity has user relationships
// For now, allow all authenticated users to edit repositories
// This maintains current behavior while adding structure for future restrictions
return true;
}
private function canDelete(GitRepository $gitRepository, User $user): bool
{
// Check if repository has a project association
$project = $gitRepository->getProject();
if (!$project) {
return false;
}
// TODO: Restrict to project owners when Project entity has owner relationship
// For now, allow all authenticated users to delete repositories
// This maintains current behavior while adding structure for future restrictions
return true;
}
}