Skip to content

Commit 25c8c0c

Browse files
Merge pull request #88 from patrickrobrecht/introduce-dependencies-between-abilities
Introduce dependencies between abilities, own ability for API documentation
2 parents 1fd7c9f + 5ac9bf3 commit 25c8c0c

File tree

14 files changed

+298
-58
lines changed

14 files changed

+298
-58
lines changed

app/Enums/Ability.php

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,72 @@ enum Ability: string
7777
case ViewAccount = 'users.view_account';
7878
case ViewAbilities = 'users.view_account.abilities';
7979
case EditAccount = 'users.edit_account';
80+
81+
case ViewApiDocumentation = 'api.docs.view';
8082
case ManagePersonalAccessTokens = 'personal_access_tokens.manage_own';
8183

84+
public function dependsOnAbility(): ?self
85+
{
86+
return match ($this) {
87+
self::CreateEvents,
88+
self::EditEvents,
89+
self::ViewPrivateEvents,
90+
self::ViewResponsibilitiesOfEvents,
91+
self::ManageBookingOptionsOfEvent => self::ViewEvents,
92+
93+
self::ExportBookingsOfEvent,
94+
self::EditBookingsOfEvent,
95+
self::DeleteAndRestoreBookingsOfEvent,
96+
self::EditBookingComment,
97+
self::ViewPaymentStatus => self::ViewBookingsOfEvent,
98+
self::EditPaymentStatus => self::ViewPaymentStatus,
99+
100+
self::ExportGroupsOfEvent => self::ManageGroupsOfEvent,
101+
102+
self::CreateEventSeries,
103+
self::EditEventSeries,
104+
self::ViewPrivateEventSeries,
105+
self::ViewResponsibilitiesOfEventSeries => self::ViewEventSeries,
106+
107+
// Basic data
108+
self::CreateOrganizations,
109+
self::EditOrganizations => self::ViewOrganizations,
110+
111+
self::CreateLocations,
112+
self::EditLocations => self::ViewLocations,
113+
114+
// Documents
115+
self::ViewCommentsOnDocuments,
116+
self::ChangeApprovalStatusOfDocuments => self::ViewDocuments,
117+
self::CommentOnDocuments => self::ViewCommentsOnDocuments,
118+
119+
self::AddDocumentsToEvents,
120+
self::EditDocumentsOfEvents,
121+
self::DeleteDocumentsOfEvents => self::ViewDocumentsOfEvents,
122+
123+
self::AddDocumentsToEventSeries,
124+
self::EditDocumentsOfEventSeries,
125+
self::DeleteDocumentsOfEventSeries => self::ViewDocumentsOfEventSeries,
126+
127+
self::AddDocumentsToOrganizations,
128+
self::EditDocumentsOfOrganizations,
129+
self::DeleteDocumentsOfOrganizations => self::ViewDocumentsOfOrganizations,
130+
131+
// Users and abilities
132+
self::CreateUsers,
133+
self::EditUsers => self::ViewUsers,
134+
135+
self::CreateUserRoles,
136+
self::EditUserRoles => self::ViewUserRoles,
137+
138+
self::EditAccount => self::ViewAccount,
139+
140+
self::ManagePersonalAccessTokens => self::ViewApiDocumentation,
141+
142+
default => null,
143+
};
144+
}
145+
82146
public function getAbilityGroup(): AbilityGroup
83147
{
84148
return match ($this) {
@@ -140,8 +204,9 @@ public function getAbilityGroup(): AbilityGroup
140204
self::EditUserRoles => AbilityGroup::UserRoles,
141205
self::ViewAccount,
142206
self::ViewAbilities,
143-
self::EditAccount,
144-
self::ManagePersonalAccessTokens => AbilityGroup::OwnAccount,
207+
self::EditAccount => AbilityGroup::OwnAccount,
208+
self::ViewApiDocumentation,
209+
self::ManagePersonalAccessTokens => AbilityGroup::ApiAccess,
145210
};
146211
}
147212

@@ -216,7 +281,9 @@ public function getTranslatedName(): string
216281
self::ViewAccount => __('View own account'),
217282
self::ViewAbilities => __('View abilities'),
218283
self::EditAccount => __('Edit own account'),
284+
219285
self::ManagePersonalAccessTokens => __('Manage personal access tokens'),
286+
self::ViewApiDocumentation => __('View API documentation'),
220287
};
221288
}
222289

app/Enums/AbilityGroup.php

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ enum AbilityGroup
2222
case Users;
2323
case UserRoles;
2424
case OwnAccount;
25+
case ApiAccess;
2526

2627
/**
27-
* @return Ability[]
28+
* @param list<Ability> $abilities
29+
* @return list<Ability>
2830
*/
29-
public function getAbilities(): array
31+
public function filterAbilities(array $abilities): array
3032
{
31-
return array_filter(Ability::cases(), fn (Ability $ability) => $ability->getAbilityGroup() === $this);
33+
return array_filter($abilities, fn (Ability $ability) => $ability->getAbilityGroup() === $this);
3234
}
3335

3436
/**
@@ -52,15 +54,16 @@ public function getParent(): ?self
5254

5355
self::Users,
5456
self::UserRoles,
55-
self::OwnAccount => self::UsersAndAbilities,
57+
self::OwnAccount,
58+
self::ApiAccess => self::UsersAndAbilities,
5659

5760
default => null,
5861
};
5962
}
6063

61-
public function getIcon(): array|string
64+
public function getIcon(): string
6265
{
63-
return match ($this) {
66+
return 'fa fa-fw ' . match ($this) {
6467
self::Events => 'fa-calendar-days',
6568
self::Bookings => 'fa-file-contract',
6669
self::Groups => 'fa-people-group',
@@ -80,6 +83,7 @@ public function getIcon(): array|string
8083
self::Users => 'fa-users',
8184
self::UserRoles => 'fa-user-group',
8285
self::OwnAccount => 'fa-user-circle',
86+
self::ApiAccess => 'fa-file-code',
8387
};
8488
}
8589

@@ -105,19 +109,35 @@ public function getTranslatedName(): string
105109
self::Users => __('Users'),
106110
self::UserRoles => __('User roles'),
107111
self::OwnAccount => __('Own account'),
112+
self::ApiAccess => __('Access to the REST API'),
108113
};
109114
}
110115

111-
public function hasChildren(): bool
116+
/**
117+
* Checks whether one of the child groups contains at least one of the given abilities.
118+
*
119+
* @param Ability[] $abilities
120+
*/
121+
public function hasChildrenWithAbilities(array $abilities): bool
112122
{
113-
return count($this->getChildren()) > 0;
123+
foreach ($this->getChildren() as $childGroup) {
124+
if (count($childGroup->filterAbilities($abilities)) > 0) {
125+
return true;
126+
}
127+
128+
if ($childGroup->hasChildrenWithAbilities($abilities)) {
129+
return true;
130+
}
131+
}
132+
133+
return false;
114134
}
115135

116136
/**
117137
* @return self[]
118138
*/
119139
public static function casesAtRootLevel(): array
120140
{
121-
return array_filter(self::cases(), fn (self $abilityGroup) => $abilityGroup->getParent() === null);
141+
return array_filter(self::cases(), static fn (self $abilityGroup) => $abilityGroup->getParent() === null);
122142
}
123143
}

app/Http/Controllers/ApiDocumentationController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ class ApiDocumentationController extends Controller
1313
{
1414
public function index(): View
1515
{
16-
$this->authorize('create', PersonalAccessToken::class);
16+
$this->authorize('viewDocumentation', PersonalAccessToken::class);
1717

1818
return view('docs.docs');
1919
}
2020

2121
public function spec(): Response
2222
{
23-
$this->authorize('create', PersonalAccessToken::class);
23+
$this->authorize('viewDocumentation', PersonalAccessToken::class);
2424

2525
return response()->make(self::getYamlFileContents(), SymfonyResponse::HTTP_OK, [
2626
'Content-Type: text/yaml',

app/Http/Requests/PersonalAccessTokenRequest.php

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@
33
namespace App\Http\Requests;
44

55
use App\Enums\Ability;
6+
use App\Http\Requests\Traits\ValidatesAbilities;
7+
use App\Models\PersonalAccessToken;
68
use Illuminate\Foundation\Http\FormRequest;
9+
use Illuminate\Validation\Rule;
10+
use Stringable;
711

12+
/**
13+
* @property-read ?PersonalAccessToken $personal_access_token
14+
*/
815
class PersonalAccessTokenRequest extends FormRequest
916
{
10-
protected function prepareForValidation(): void
17+
use ValidatesAbilities;
18+
19+
protected function getSelectableAbilities(): array
1120
{
12-
$this->merge([
13-
'abilities' => $this->input('abilities', []), // Force array!
14-
]);
21+
return Ability::apiCases();
1522
}
1623

1724
/**
18-
* Get the validation rules that apply to the request.
19-
*
20-
* @return array<string, mixed>
25+
* @return array<string, array<int, string|Stringable>>
2126
*/
2227
public function rules(): array
2328
{
@@ -26,18 +31,15 @@ public function rules(): array
2631
'required',
2732
'string',
2833
'max:255',
34+
Rule::unique('personal_access_tokens', 'name')
35+
->where('tokenable_id', $this->user()->id)
36+
->ignore($this->personal_access_token->id ?? null),
2937
],
3038
'expires_at' => [
3139
'nullable',
3240
'date_format:Y-m-d\TH:i',
3341
],
34-
'abilities' => [
35-
'sometimes',
36-
'array',
37-
],
38-
'abilities.*' => [
39-
Ability::rule(),
40-
],
42+
...$this->rulesForAbilities(),
4143
];
4244
}
4345
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace App\Http\Requests\Traits;
4+
5+
use App\Enums\Ability;
6+
use Illuminate\Foundation\Http\FormRequest;
7+
use Stringable;
8+
9+
/**
10+
* @mixin FormRequest
11+
*/
12+
trait ValidatesAbilities
13+
{
14+
/**
15+
* @return list<Ability>
16+
*/
17+
abstract protected function getSelectableAbilities(): array;
18+
19+
protected function prepareForValidation(): void
20+
{
21+
$this->prepareAbilitiesForValidation();
22+
}
23+
24+
protected function prepareAbilitiesForValidation(): void
25+
{
26+
$abilities = $this->input('abilities', []);
27+
28+
foreach ($abilities as $value) {
29+
$ability = Ability::tryFrom($value);
30+
if ($ability) {
31+
$dependentAbility = $ability->dependsOnAbility();
32+
if (
33+
$dependentAbility
34+
// dependent ability is not selected yet
35+
&& !in_array($dependentAbility->value, $abilities, true)
36+
// but is selectable
37+
&& in_array($dependentAbility, $this->getSelectableAbilities(), true)
38+
) {
39+
$abilities[] = $dependentAbility->value;
40+
}
41+
}
42+
}
43+
44+
$this->merge(['abilities' => $abilities]);
45+
}
46+
47+
/**
48+
* @return array<string, array<int, string|Stringable>>
49+
*/
50+
protected function rulesForAbilities(): array
51+
{
52+
return [
53+
'abilities' => [
54+
'sometimes',
55+
'array',
56+
],
57+
'abilities.*' => [
58+
Ability::rule($this->getSelectableAbilities()),
59+
],
60+
];
61+
}
62+
63+
/**
64+
* @return array<string, string>
65+
*/
66+
public function attributes(): array
67+
{
68+
return $this->attributesForAbilities();
69+
}
70+
71+
/**
72+
* @return array<string, string>
73+
*/
74+
protected function attributesForAbilities(): array
75+
{
76+
return [
77+
'abilities.*' => __('validation.attributes.abilities'),
78+
];
79+
}
80+
}

app/Http/Requests/UserRoleRequest.php

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,26 @@
33
namespace App\Http\Requests;
44

55
use App\Enums\Ability;
6+
use App\Http\Requests\Traits\ValidatesAbilities;
67
use App\Models\UserRole;
78
use Illuminate\Foundation\Http\FormRequest;
89
use Illuminate\Validation\Rule;
10+
use Stringable;
911

1012
/**
1113
* @property-read ?UserRole $user_role
1214
*/
1315
class UserRoleRequest extends FormRequest
1416
{
15-
protected function prepareForValidation(): void
17+
use ValidatesAbilities;
18+
19+
protected function getSelectableAbilities(): array
1620
{
17-
$this->merge([
18-
'abilities' => $this->input('abilities', []), // Force array!
19-
]);
21+
return Ability::cases();
2022
}
2123

2224
/**
23-
* Get the validation rules that apply to the request.
25+
* @return array<string, array<int, string|Stringable>>
2426
*/
2527
public function rules(): array
2628
{
@@ -32,13 +34,7 @@ public function rules(): array
3234
Rule::unique('user_roles', 'name')
3335
->ignore($this->user_role->id ?? null),
3436
],
35-
'abilities' => [
36-
'sometimes',
37-
'array',
38-
],
39-
'abilities.*' => [
40-
Ability::rule(),
41-
],
37+
...$this->rulesForAbilities(),
4238
];
4339
}
4440
}

app/Policies/PersonalAccessTokenPolicy.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public function view(User $user, PersonalAccessToken $personalAccessToken): Resp
3737
return $this->update($user, $personalAccessToken);
3838
}
3939

40+
public function viewDocumentation(User $user): Response
41+
{
42+
return $this->requireAbility($user, Ability::ViewApiDocumentation);
43+
}
44+
4045
/**
4146
* Determine whether the user can create models.
4247
*/

0 commit comments

Comments
 (0)