Re-usable ACL checks on teams (#5925)

This commit is contained in:
Kevin Papst
2026-04-26 17:06:59 +02:00
committed by GitHub
parent 7a559a09e6
commit 20c7b03bd9
14 changed files with 1393 additions and 149 deletions

View File

@@ -13,6 +13,7 @@ use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Team;
use App\Entity\Timesheet;
use App\Entity\User;
use App\User\PermissionService;
use Doctrine\Common\Collections\Collection;
@@ -115,15 +116,15 @@ final class RolePermissionManager
}
/**
* @param Collection<int, Team> $teams
* @param array<int, Team>|Collection<int, Team> $teams
*/
private function checkTeamAccess(Collection $teams, User $user): bool
private function checkTeamAccess(Collection|array $teams, User $user): bool
{
if ($user->canSeeAllData()) {
return true;
}
if ($teams->count() === 0) {
if (\count($teams) === 0) {
return true;
}
@@ -136,6 +137,28 @@ final class RolePermissionManager
return false;
}
/**
* @param array<int, Team>|Collection<int, Team> $teams
*/
private function checkTeamLeadAccess(Collection|array $teams, User $user): bool
{
if ($user->canSeeAllData()) {
return true;
}
if (\count($teams) === 0) {
return true;
}
foreach ($teams as $team) {
if ($user->isTeamleadOf($team)) {
return true;
}
}
return false;
}
public function checkTeamAccessCustomer(Customer $customer, User $user): bool
{
return $this->checkTeamAccess($customer->getTeams(), $user);
@@ -158,4 +181,21 @@ final class RolePermissionManager
return $this->checkTeamAccess($activity->getTeams(), $user);
}
public function checkTeamAccessTimesheet(Timesheet $timesheet, User $user): bool
{
if ($user->getId() !== null && $user->getId() === $timesheet->getUser()?->getId()) {
return true;
}
if ($timesheet->getProject() !== null && !$this->checkTeamAccessProject($timesheet->getProject(), $user)) {
return false;
}
if ($timesheet->getActivity() !== null && !$this->checkTeamAccessActivity($timesheet->getActivity(), $user)) {
return false;
}
return $this->checkTeamLeadAccess($timesheet->getUser()?->getTeams() ?? [], $user);
}
}

View File

@@ -10,8 +10,6 @@
namespace App\Voter;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Team;
use App\Entity\User;
use App\Security\RolePermissionManager;
@@ -57,37 +55,6 @@ final class ActivityVoter extends Voter
return $subject instanceof Activity && $this->supportsAttribute($attribute);
}
private function checkTeamPermission(Activity|Project|Customer $subject, User $user): bool
{
if ($user->canSeeAllData()) {
return true;
}
if ($subject instanceof Activity && $subject->getProject() !== null) {
if (!$this->checkTeamPermission($subject->getProject(), $user)) {
return false;
}
}
if ($subject instanceof Project && $subject->getCustomer() !== null) {
if (!$this->checkTeamPermission($subject->getCustomer(), $user)) {
return false;
}
}
if ($subject->getTeams()->count() === 0) {
return true;
}
foreach ($subject->getTeams() as $team) {
if ($user->isInTeam($team)) {
return true;
}
}
return false;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
@@ -99,7 +66,7 @@ final class ActivityVoter extends Voter
// this is a virtual permission, only meant to be used by developer
// it checks if access to the given activity is potentially possible
if ($attribute === 'access') {
return $this->checkTeamPermission($subject, $user);
return $this->permissionManager->checkTeamAccessActivity($subject, $user);
}
if ($this->permissionManager->hasRolePermission($user, $attribute . '_activity')) {

View File

@@ -69,21 +69,7 @@ final class CustomerVoter extends Voter
// this is a virtual permission, only meant to be used by developer
// it checks if access to the given customer is potentially possible
if ($attribute === 'access') {
if ($subject->getTeams()->count() === 0) {
return true;
}
foreach ($subject->getTeams() as $team) {
if ($user->isInTeam($team)) {
return true;
}
}
if ($user->canSeeAllData()) {
return true;
}
return false;
return $this->permissionManager->checkTeamAccessCustomer($subject, $user);
}
if ($this->permissionManager->hasRolePermission($user, $attribute . '_customer')) {

View File

@@ -9,7 +9,6 @@
namespace App\Voter;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Team;
use App\Entity\User;
@@ -58,31 +57,6 @@ final class ProjectVoter extends Voter
return $subject instanceof Project && $this->supportsAttribute($attribute);
}
private function checkTeamPermission(Project|Customer $subject, User $user): bool
{
if ($user->canSeeAllData()) {
return true;
}
if ($subject instanceof Project && $subject->getCustomer() !== null) {
if (!$this->checkTeamPermission($subject->getCustomer(), $user)) {
return false;
}
}
if ($subject->getTeams()->count() === 0) {
return true;
}
foreach ($subject->getTeams() as $team) {
if ($user->isInTeam($team)) {
return true;
}
}
return false;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
@@ -94,7 +68,7 @@ final class ProjectVoter extends Voter
// this is a virtual permission, only meant to be used by developer
// it checks if access to the given project is potentially possible
if ($attribute === 'access') {
return $this->checkTeamPermission($subject, $user);
return $this->permissionManager->checkTeamAccessProject($subject, $user);
}
if ($this->permissionManager->hasRolePermission($user, $attribute . '_project')) {

View File

@@ -131,18 +131,15 @@ final class TimesheetVoter extends Voter
return false;
}
$permission .= '_';
// extend me for "team" support later on
if ($subject->getUser()?->getId() === $user->getId()) {
$permission .= 'own';
} else {
$permission .= 'other';
return $this->permissionManager->hasRolePermission($user, $permission . '_own_timesheet');
}
$permission .= '_timesheet';
if (!$this->permissionManager->checkTeamAccessTimesheet($subject, $user)) {
return false;
}
return $this->permissionManager->hasRolePermission($user, $permission);
return $this->permissionManager->hasRolePermission($user, $permission . '_other_timesheet');
}
private function canStart(Timesheet $timesheet): bool