Skip to content

Commit 8a202b5

Browse files
authored
Merge pull request #113 from Lomkit/feature/authorization-on-relation-operations
authorizations on relation operations
2 parents b5d6114 + 4dbc3c4 commit 8a202b5

24 files changed

+3082
-43
lines changed

src/Concerns/Authorizable.php

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ trait Authorizable
1414
/**
1515
* Determine if the current user has a given ability.
1616
*
17-
* @param \Illuminate\Http\Request $request
18-
* @param string $ability
17+
* @param string $ability
18+
* @param Model|string $model
1919
*
2020
* @throws \Illuminate\Auth\Access\AuthorizationException
2121
*
@@ -53,10 +53,10 @@ public function authorizeTo($ability, $model)
5353
}
5454

5555
/**
56-
* Determine if the current user can view the given resource.
56+
* Determine if the current user can perform an ability on the given model.
5757
*
58-
* @param \Illuminate\Http\Request $request
59-
* @param string $ability
58+
* @param string $ability
59+
* @param Model|string $model
6060
*
6161
* @return bool
6262
*/
@@ -88,4 +88,148 @@ public function authorizedTo($ability, $model)
8888

8989
return true;
9090
}
91+
92+
/**
93+
* Determine if the current user has a given ability.
94+
*
95+
* @param string $ability
96+
* * @param Model $model
97+
* * @param string $toActionModel
98+
*
99+
* @throws \Illuminate\Auth\Access\AuthorizationException
100+
*
101+
* @return void
102+
*/
103+
public function authorizeToPerformActionOnRelationship($ability, $model, $toActionModel)
104+
{
105+
$gate = Gate::getPolicyFor($model);
106+
$method = $ability.class_basename($toActionModel);
107+
108+
if (!is_null($gate) && method_exists($gate, $method) && $this->isAuthorizingEnabled()) {
109+
$resolver = function () use ($method, $gate, $model, $toActionModel) {
110+
return !is_null($gate) && method_exists($gate, $method)
111+
? Gate::authorize($method, [$model, $toActionModel])
112+
: true;
113+
};
114+
115+
if ($this->isAuthorizationCacheEnabled()) {
116+
$gatePasses = Cache::remember(
117+
$this->getAuthorizationCacheKey(
118+
app(RestRequest::class),
119+
sprintf(
120+
'%s.%s.%s.%s.%s',
121+
$ability,
122+
$model instanceof Model ? Str::snake((new \ReflectionClass($model))->getShortName()) : $model,
123+
$model instanceof Model ? $model->getKey() : null,
124+
$toActionModel instanceof Model ? Str::snake((new \ReflectionClass($toActionModel))->getShortName()) : $toActionModel,
125+
$toActionModel instanceof Model ? $toActionModel->getKey() : null,
126+
)
127+
),
128+
$this->cacheAuthorizationFor(),
129+
$resolver
130+
);
131+
} else {
132+
$gatePasses = $resolver();
133+
}
134+
135+
if (!$gatePasses) {
136+
Response::deny()->authorize();
137+
}
138+
}
139+
}
140+
141+
/**
142+
* Determine if the current user can perform an ability on the given model.
143+
*
144+
* @param string $ability
145+
* @param Model $model
146+
* @param string $toActionModel
147+
*
148+
* @return bool
149+
*/
150+
public function authorizedToPerformActionOnRelationship($ability, $model, $toActionModel)
151+
{
152+
$gate = Gate::getPolicyFor($model);
153+
$method = $ability.class_basename($toActionModel);
154+
155+
if (!is_null($gate) && method_exists($gate, $method) && $this->isAuthorizingEnabled()) {
156+
$resolver = function () use ($method, $toActionModel, $model) {
157+
return Gate::check($method, [$model, $toActionModel]);
158+
};
159+
160+
if ($this->isAuthorizationCacheEnabled()) {
161+
return Cache::remember(
162+
$this->getAuthorizationCacheKey(
163+
app(RestRequest::class),
164+
sprintf(
165+
'%s.%s.%s.%s.%s',
166+
$ability,
167+
$model instanceof Model ? Str::snake((new \ReflectionClass($model))->getShortName()) : $model,
168+
$model instanceof Model ? $model->getKey() : null,
169+
$toActionModel instanceof Model ? Str::snake((new \ReflectionClass($toActionModel))->getShortName()) : $toActionModel,
170+
$toActionModel instanceof Model ? $toActionModel->getKey() : null,
171+
)
172+
),
173+
$this->cacheAuthorizationFor(),
174+
$resolver
175+
);
176+
}
177+
178+
return $resolver();
179+
}
180+
181+
return true;
182+
}
183+
184+
/**
185+
* Determine if the user can attach models of the given type to the base model.
186+
*
187+
* @param \Illuminate\Database\Eloquent\Model|string $model
188+
* @param \Illuminate\Database\Eloquent\Model|string $toAttachModel
189+
*
190+
* @return bool
191+
*/
192+
public function authorizedToAttach($model, $toAttachModel)
193+
{
194+
return $this->authorizedToPerformActionOnRelationship('attach', $model, $toAttachModel);
195+
}
196+
197+
/**
198+
* Determine if the user can attach models of the given type to the base model.
199+
*
200+
* @param \Illuminate\Database\Eloquent\Model|string $model
201+
* @param \Illuminate\Database\Eloquent\Model|string $toAttachModel
202+
*
203+
* @return void
204+
*/
205+
public function authorizeToAttach($model, $toAttachModel)
206+
{
207+
$this->authorizeToPerformActionOnRelationship('attach', $model, $toAttachModel);
208+
}
209+
210+
/**
211+
* Determine if the user can detach models of the given type to the base model.
212+
*
213+
* @param \Illuminate\Database\Eloquent\Model|string $model
214+
* @param \Illuminate\Database\Eloquent\Model|string $toAttachModel
215+
*
216+
* @return bool
217+
*/
218+
public function authorizedToDetach($model, $toAttachModel)
219+
{
220+
return $this->authorizedToPerformActionOnRelationship('detach', $model, $toAttachModel);
221+
}
222+
223+
/**
224+
* Determine if the user can detach models of the given type to the base model.
225+
*
226+
* @param \Illuminate\Database\Eloquent\Model|string $model
227+
* @param \Illuminate\Database\Eloquent\Model|string $toAttachModel
228+
*
229+
* @return void
230+
*/
231+
public function authorizeToDetach($model, $toAttachModel)
232+
{
233+
$this->authorizeToPerformActionOnRelationship('detach', $model, $toAttachModel);
234+
}
91235
}

src/Relations/BelongsTo.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,24 @@ class BelongsTo extends Relation implements RelationResource
1717
*/
1818
public function beforeMutating(Model $model, Relation $relation, array $mutationRelations)
1919
{
20+
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
21+
->applyMutation($mutationRelations[$relation->relation]);
22+
23+
switch ($mutationRelations[$relation->relation]['operation']) {
24+
case 'create':
25+
case 'update':
26+
case 'attach':
27+
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
28+
break;
29+
case 'detach':
30+
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
31+
break;
32+
}
33+
2034
$model
2135
->{$relation->relation}()
2236
->{$mutationRelations[$relation->relation]['operation'] === 'detach' ? 'dissociate' : 'associate'}(
23-
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
24-
->applyMutation($mutationRelations[$relation->relation])
37+
$toPerformActionModel
2538
);
2639
}
2740
}

src/Relations/BelongsToMany.php

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,31 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
4747
{
4848
foreach ($mutationRelations[$relation->relation] as $mutationRelation) {
4949
if ($mutationRelation['operation'] === 'detach') {
50+
$toDetachModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
51+
->applyMutation($mutationRelation);
52+
53+
$this->resource()->authorizeToDetach($model, $toDetachModel);
54+
5055
$model
5156
->{$relation->relation}()
5257
->detach(
53-
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
54-
->applyMutation($mutationRelation)
55-
->getKey()
58+
$toDetachModel->getKey()
5659
);
5760
} elseif ($mutationRelation['operation'] === 'attach') {
61+
$toAttachModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
62+
->applyMutation($mutationRelation);
63+
64+
$this->resource()->authorizeToAttach($model, $toAttachModel);
65+
5866
$model
5967
->{$relation->relation}()
6068
->attach(
6169
[
62-
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
63-
->applyMutation($mutationRelation)
64-
->getKey() => $mutationRelation['pivot'] ?? [],
70+
$toAttachModel->getKey() => $mutationRelation['pivot'] ?? [],
6571
]
6672
);
6773
} elseif ($mutationRelation['operation'] === 'toggle') {
68-
$model
74+
$results = $model
6975
->{$relation->relation}()
7076
->toggle(
7177
[
@@ -74,8 +80,16 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
7480
->getKey() => $mutationRelation['pivot'] ?? [],
7581
]
7682
);
83+
84+
foreach ($results['attached'] as $attached) {
85+
$this->resource()->authorizeToAttach($model, $relation->resource()::$model::find($attached));
86+
}
87+
88+
foreach ($results['detached'] as $detached) {
89+
$this->resource()->authorizeToDetach($model, $relation->resource()::$model::find($detached));
90+
}
7791
} elseif ($mutationRelation['operation'] === 'sync') {
78-
$model
92+
$results = $model
7993
->{$relation->relation}()
8094
->sync(
8195
[
@@ -85,13 +99,25 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
8599
],
86100
!isset($mutationRelation['without_detaching']) || !$mutationRelation['without_detaching']
87101
);
102+
103+
foreach ($results['attached'] as $attached) {
104+
$this->resource()->authorizeToAttach($model, $relation->resource()::$model::find($attached));
105+
}
106+
107+
foreach ($results['detached'] as $detached) {
108+
$this->resource()->authorizeToDetach($model, $relation->resource()::$model::find($detached));
109+
}
88110
} elseif (in_array($mutationRelation['operation'], ['create', 'update'])) {
111+
$toAttachModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
112+
->applyMutation($mutationRelation);
113+
114+
$this->resource()->authorizeToAttach($model, $toAttachModel);
115+
89116
$model
90117
->{$relation->relation}()
91118
->syncWithoutDetaching(
92119
[
93-
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
94-
->applyMutation($mutationRelation)
120+
$toAttachModel
95121
->getKey() => $mutationRelation['pivot'] ?? [],
96122
]
97123
);

src/Relations/HasMany.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,19 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
2525
$model->{$relation->relation}()->getForeignKeyName() => $mutationRelation['operation'] === 'detach' ? null : $model->{$relation->relation}()->getParentKey(),
2626
];
2727

28-
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
28+
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
2929
->applyMutation($mutationRelation, $attributes);
30+
31+
switch ($mutationRelation['operation']) {
32+
case 'create':
33+
case 'update':
34+
case 'attach':
35+
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
36+
break;
37+
case 'detach':
38+
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
39+
break;
40+
}
3041
}
3142
}
3243
}

src/Relations/HasOne.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,18 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
2121
$model->{$relation->relation}()->getForeignKeyName() => $mutationRelations[$relation->relation]['operation'] === 'detach' ? null : $model->{$relation->relation}()->getParentKey(),
2222
];
2323

24-
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
24+
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
2525
->applyMutation($mutationRelations[$relation->relation], $attributes);
26+
27+
switch ($mutationRelations[$relation->relation]['operation']) {
28+
case 'create':
29+
case 'update':
30+
case 'attach':
31+
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
32+
break;
33+
case 'detach':
34+
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
35+
break;
36+
}
2637
}
2738
}

src/Relations/HasOneOfMany.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,18 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
2121
$model->{$relation->relation}()->getForeignKeyName() => $mutationRelations[$relation->relation]['operation'] === 'detach' ? null : $model->{$relation->relation}()->getParentKey(),
2222
];
2323

24-
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
24+
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
2525
->applyMutation($mutationRelations[$relation->relation], $attributes);
26+
27+
switch ($mutationRelations[$relation->relation]['operation']) {
28+
case 'create':
29+
case 'update':
30+
case 'attach':
31+
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
32+
break;
33+
case 'detach':
34+
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
35+
break;
36+
}
2637
}
2738
}

src/Relations/MorphMany.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,19 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
2626
$model->{$relation->relation}()->getMorphType() => $model::class,
2727
];
2828

29-
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
29+
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
3030
->applyMutation($mutationRelation, $attributes);
31+
32+
switch ($mutationRelation['operation']) {
33+
case 'create':
34+
case 'update':
35+
case 'attach':
36+
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
37+
break;
38+
case 'detach':
39+
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
40+
break;
41+
}
3142
}
3243
}
3344
}

src/Relations/MorphOne.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,21 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
1919
{
2020
$attributes = [
2121
$model->{$relation->relation}()->getForeignKeyName() => $mutationRelations[$relation->relation]['operation'] === 'detach' ? null : $model->{$relation->relation}()->getParentKey(),
22-
$model->{$relation->relation}()->getMorphType() => $model::class,
22+
$model->{$relation->relation}()->getMorphType() => $mutationRelations[$relation->relation]['operation'] === 'detach' ? null : $model::class,
2323
];
2424

25-
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
25+
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
2626
->applyMutation($mutationRelations[$relation->relation], $attributes);
27+
28+
switch ($mutationRelations[$relation->relation]['operation']) {
29+
case 'create':
30+
case 'update':
31+
case 'attach':
32+
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
33+
break;
34+
case 'detach':
35+
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
36+
break;
37+
}
2738
}
2839
}

0 commit comments

Comments
 (0)