Skip to content

Commit

Permalink
Merge pull request #113 from Lomkit/feature/authorization-on-relation…
Browse files Browse the repository at this point in the history
…-operations

authorizations on relation operations
  • Loading branch information
GautierDele authored Apr 6, 2024
2 parents b5d6114 + 4dbc3c4 commit 8a202b5
Show file tree
Hide file tree
Showing 24 changed files with 3,082 additions and 43 deletions.
154 changes: 149 additions & 5 deletions src/Concerns/Authorizable.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ trait Authorizable
/**
* Determine if the current user has a given ability.
*
* @param \Illuminate\Http\Request $request
* @param string $ability
* @param string $ability
* @param Model|string $model
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*
Expand Down Expand Up @@ -53,10 +53,10 @@ public function authorizeTo($ability, $model)
}

/**
* Determine if the current user can view the given resource.
* Determine if the current user can perform an ability on the given model.
*
* @param \Illuminate\Http\Request $request
* @param string $ability
* @param string $ability
* @param Model|string $model
*
* @return bool
*/
Expand Down Expand Up @@ -88,4 +88,148 @@ public function authorizedTo($ability, $model)

return true;
}

/**
* Determine if the current user has a given ability.
*
* @param string $ability
* * @param Model $model
* * @param string $toActionModel
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*
* @return void
*/
public function authorizeToPerformActionOnRelationship($ability, $model, $toActionModel)
{
$gate = Gate::getPolicyFor($model);
$method = $ability.class_basename($toActionModel);

if (!is_null($gate) && method_exists($gate, $method) && $this->isAuthorizingEnabled()) {
$resolver = function () use ($method, $gate, $model, $toActionModel) {
return !is_null($gate) && method_exists($gate, $method)
? Gate::authorize($method, [$model, $toActionModel])
: true;
};

if ($this->isAuthorizationCacheEnabled()) {
$gatePasses = Cache::remember(
$this->getAuthorizationCacheKey(
app(RestRequest::class),
sprintf(
'%s.%s.%s.%s.%s',
$ability,
$model instanceof Model ? Str::snake((new \ReflectionClass($model))->getShortName()) : $model,
$model instanceof Model ? $model->getKey() : null,
$toActionModel instanceof Model ? Str::snake((new \ReflectionClass($toActionModel))->getShortName()) : $toActionModel,
$toActionModel instanceof Model ? $toActionModel->getKey() : null,
)
),
$this->cacheAuthorizationFor(),
$resolver
);
} else {
$gatePasses = $resolver();
}

if (!$gatePasses) {
Response::deny()->authorize();
}
}
}

/**
* Determine if the current user can perform an ability on the given model.
*
* @param string $ability
* @param Model $model
* @param string $toActionModel
*
* @return bool
*/
public function authorizedToPerformActionOnRelationship($ability, $model, $toActionModel)
{
$gate = Gate::getPolicyFor($model);
$method = $ability.class_basename($toActionModel);

if (!is_null($gate) && method_exists($gate, $method) && $this->isAuthorizingEnabled()) {
$resolver = function () use ($method, $toActionModel, $model) {
return Gate::check($method, [$model, $toActionModel]);
};

if ($this->isAuthorizationCacheEnabled()) {
return Cache::remember(
$this->getAuthorizationCacheKey(
app(RestRequest::class),
sprintf(
'%s.%s.%s.%s.%s',
$ability,
$model instanceof Model ? Str::snake((new \ReflectionClass($model))->getShortName()) : $model,
$model instanceof Model ? $model->getKey() : null,
$toActionModel instanceof Model ? Str::snake((new \ReflectionClass($toActionModel))->getShortName()) : $toActionModel,
$toActionModel instanceof Model ? $toActionModel->getKey() : null,
)
),
$this->cacheAuthorizationFor(),
$resolver
);
}

return $resolver();
}

return true;
}

/**
* Determine if the user can attach models of the given type to the base model.
*
* @param \Illuminate\Database\Eloquent\Model|string $model
* @param \Illuminate\Database\Eloquent\Model|string $toAttachModel
*
* @return bool
*/
public function authorizedToAttach($model, $toAttachModel)
{
return $this->authorizedToPerformActionOnRelationship('attach', $model, $toAttachModel);
}

/**
* Determine if the user can attach models of the given type to the base model.
*
* @param \Illuminate\Database\Eloquent\Model|string $model
* @param \Illuminate\Database\Eloquent\Model|string $toAttachModel
*
* @return void
*/
public function authorizeToAttach($model, $toAttachModel)
{
$this->authorizeToPerformActionOnRelationship('attach', $model, $toAttachModel);
}

/**
* Determine if the user can detach models of the given type to the base model.
*
* @param \Illuminate\Database\Eloquent\Model|string $model
* @param \Illuminate\Database\Eloquent\Model|string $toAttachModel
*
* @return bool
*/
public function authorizedToDetach($model, $toAttachModel)
{
return $this->authorizedToPerformActionOnRelationship('detach', $model, $toAttachModel);
}

/**
* Determine if the user can detach models of the given type to the base model.
*
* @param \Illuminate\Database\Eloquent\Model|string $model
* @param \Illuminate\Database\Eloquent\Model|string $toAttachModel
*
* @return void
*/
public function authorizeToDetach($model, $toAttachModel)
{
$this->authorizeToPerformActionOnRelationship('detach', $model, $toAttachModel);
}
}
17 changes: 15 additions & 2 deletions src/Relations/BelongsTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,24 @@ class BelongsTo extends Relation implements RelationResource
*/
public function beforeMutating(Model $model, Relation $relation, array $mutationRelations)
{
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelations[$relation->relation]);

switch ($mutationRelations[$relation->relation]['operation']) {
case 'create':
case 'update':
case 'attach':
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
break;
case 'detach':
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
break;
}

$model
->{$relation->relation}()
->{$mutationRelations[$relation->relation]['operation'] === 'detach' ? 'dissociate' : 'associate'}(
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelations[$relation->relation])
$toPerformActionModel
);
}
}
46 changes: 36 additions & 10 deletions src/Relations/BelongsToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,31 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
{
foreach ($mutationRelations[$relation->relation] as $mutationRelation) {
if ($mutationRelation['operation'] === 'detach') {
$toDetachModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelation);

$this->resource()->authorizeToDetach($model, $toDetachModel);

$model
->{$relation->relation}()
->detach(
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelation)
->getKey()
$toDetachModel->getKey()
);
} elseif ($mutationRelation['operation'] === 'attach') {
$toAttachModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelation);

$this->resource()->authorizeToAttach($model, $toAttachModel);

$model
->{$relation->relation}()
->attach(
[
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelation)
->getKey() => $mutationRelation['pivot'] ?? [],
$toAttachModel->getKey() => $mutationRelation['pivot'] ?? [],
]
);
} elseif ($mutationRelation['operation'] === 'toggle') {
$model
$results = $model
->{$relation->relation}()
->toggle(
[
Expand All @@ -74,8 +80,16 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
->getKey() => $mutationRelation['pivot'] ?? [],
]
);

foreach ($results['attached'] as $attached) {
$this->resource()->authorizeToAttach($model, $relation->resource()::$model::find($attached));
}

foreach ($results['detached'] as $detached) {
$this->resource()->authorizeToDetach($model, $relation->resource()::$model::find($detached));
}
} elseif ($mutationRelation['operation'] === 'sync') {
$model
$results = $model
->{$relation->relation}()
->sync(
[
Expand All @@ -85,13 +99,25 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
],
!isset($mutationRelation['without_detaching']) || !$mutationRelation['without_detaching']
);

foreach ($results['attached'] as $attached) {
$this->resource()->authorizeToAttach($model, $relation->resource()::$model::find($attached));
}

foreach ($results['detached'] as $detached) {
$this->resource()->authorizeToDetach($model, $relation->resource()::$model::find($detached));
}
} elseif (in_array($mutationRelation['operation'], ['create', 'update'])) {
$toAttachModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelation);

$this->resource()->authorizeToAttach($model, $toAttachModel);

$model
->{$relation->relation}()
->syncWithoutDetaching(
[
app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelation)
$toAttachModel
->getKey() => $mutationRelation['pivot'] ?? [],
]
);
Expand Down
13 changes: 12 additions & 1 deletion src/Relations/HasMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,19 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
$model->{$relation->relation}()->getForeignKeyName() => $mutationRelation['operation'] === 'detach' ? null : $model->{$relation->relation}()->getParentKey(),
];

app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelation, $attributes);

switch ($mutationRelation['operation']) {
case 'create':
case 'update':
case 'attach':
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
break;
case 'detach':
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
break;
}
}
}
}
13 changes: 12 additions & 1 deletion src/Relations/HasOne.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
$model->{$relation->relation}()->getForeignKeyName() => $mutationRelations[$relation->relation]['operation'] === 'detach' ? null : $model->{$relation->relation}()->getParentKey(),
];

app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelations[$relation->relation], $attributes);

switch ($mutationRelations[$relation->relation]['operation']) {
case 'create':
case 'update':
case 'attach':
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
break;
case 'detach':
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
break;
}
}
}
13 changes: 12 additions & 1 deletion src/Relations/HasOneOfMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
$model->{$relation->relation}()->getForeignKeyName() => $mutationRelations[$relation->relation]['operation'] === 'detach' ? null : $model->{$relation->relation}()->getParentKey(),
];

app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelations[$relation->relation], $attributes);

switch ($mutationRelations[$relation->relation]['operation']) {
case 'create':
case 'update':
case 'attach':
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
break;
case 'detach':
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
break;
}
}
}
13 changes: 12 additions & 1 deletion src/Relations/MorphMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,19 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
$model->{$relation->relation}()->getMorphType() => $model::class,
];

app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelation, $attributes);

switch ($mutationRelation['operation']) {
case 'create':
case 'update':
case 'attach':
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
break;
case 'detach':
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
break;
}
}
}
}
15 changes: 13 additions & 2 deletions src/Relations/MorphOne.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,21 @@ public function afterMutating(Model $model, Relation $relation, array $mutationR
{
$attributes = [
$model->{$relation->relation}()->getForeignKeyName() => $mutationRelations[$relation->relation]['operation'] === 'detach' ? null : $model->{$relation->relation}()->getParentKey(),
$model->{$relation->relation}()->getMorphType() => $model::class,
$model->{$relation->relation}()->getMorphType() => $mutationRelations[$relation->relation]['operation'] === 'detach' ? null : $model::class,
];

app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
$toPerformActionModel = app()->make(QueryBuilder::class, ['resource' => $relation->resource()])
->applyMutation($mutationRelations[$relation->relation], $attributes);

switch ($mutationRelations[$relation->relation]['operation']) {
case 'create':
case 'update':
case 'attach':
$this->resource()->authorizeToAttach($model, $toPerformActionModel);
break;
case 'detach':
$this->resource()->authorizeToDetach($model, $toPerformActionModel);
break;
}
}
}
Loading

0 comments on commit 8a202b5

Please sign in to comment.