diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..07764a7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf \ No newline at end of file diff --git a/README.md b/README.md index 31deb2c..e5eb5c1 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,42 @@ if ($user->allowed('edit.articles', $article, false)) { // now owner check is di } ``` +#### Checking Through Relationships + +If your models have a different relationship than a simple `belongsTo` you can use the `allowedThrough` method which will check to see if the entity exists within the supplied relationship. + +Let's say your articles have a many-to-many to users so you can have multiple authors. Within your user model you would add this relationship: + + ```php + // within User model + public function articles() + { + return $this->hasMany('App\Article'); + } + ``` + + Now you can substitute `allowed` with `allowedThrough` in the examples above, passing in the name of the dynamic property for ownership check. + + ```php + if ($user->allowedThrough('edit.articles', $article, 'articles')){ + // article exists in the relation, do stuff + } + ``` + + This method also includes the boolean value to skip the relationship check. + + #### And/Or + + By default both `allowed` and `allowedThrough` function using **OR** logic, they have the permission *or* they are the owner. This is useful for saying an admin *or* the owner can edit an article. But sometimes you need to check if a user has a permission **AND** is within a specific relationship. To accomplish this you can pass boolean `true` as the last argument to the functions to switch to AND mode. + +```php +// user must have the edit.articles permission AND $article->user_id === $user->id +$user->allowed('edit.articles', $article, true, 'user_id', true) + +// user must have the edit.articles permission AND the article is in $user->articles relationship +$user->allowedThrough('edit.articles', $article, 'articles', true, true) +``` + ### Blade Extensions There are four Blade extensions. Basically, it is replacement for classic if statements. @@ -310,6 +346,10 @@ There are four Blade extensions. Basically, it is replacement for classic if sta // show edit button @endallowed +@allowedThrough('edit', $article, 'articles') // @if(Auth::check() && Auth::user()->allowedThrough('edit', $article, 'articles')) + // show edit button +@endallowedthrough + @role('admin|moderator', 'all') // @if(Auth::check() && Auth::user()->is('admin|moderator', 'all')) // user is admin and also moderator @else diff --git a/src/Bican/Roles/RolesServiceProvider.php b/src/Bican/Roles/RolesServiceProvider.php index 95e4471..1beb7f9 100644 --- a/src/Bican/Roles/RolesServiceProvider.php +++ b/src/Bican/Roles/RolesServiceProvider.php @@ -76,5 +76,13 @@ protected function registerBladeExtensions() $blade->directive('endallowed', function () { return ""; }); + + $blade->directive('allowedThrough', function($expression) { + return "allowedThrough{$expression}): ?>"; + }); + + $blade->directive('endallowedthrough', function () { + return ""; + }); } } diff --git a/src/Bican/Roles/Traits/HasRoleAndPermission.php b/src/Bican/Roles/Traits/HasRoleAndPermission.php index 840cc1c..8fb57d1 100644 --- a/src/Bican/Roles/Traits/HasRoleAndPermission.php +++ b/src/Bican/Roles/Traits/HasRoleAndPermission.php @@ -2,9 +2,17 @@ namespace Bican\Roles\Traits; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Database\Eloquent\Relations\HasOneOrMany; +use Illuminate\Database\Eloquent\Relations\MorphOneOrMany; +use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Model; use InvalidArgumentException; +use ReflectionClass; trait HasRoleAndPermission { @@ -260,21 +268,230 @@ public function hasPermission($permission) * @param \Illuminate\Database\Eloquent\Model $entity * @param bool $owner * @param string $ownerColumn + * @param bool $and * @return bool */ - public function allowed($providedPermission, Model $entity, $owner = true, $ownerColumn = 'user_id') + public function allowed($providedPermission, Model $entity, $owner = true, $ownerColumn = 'user_id', $and = false) { if ($this->isPretendEnabled()) { return $this->pretend('allowed'); } + $related = false; if ($owner === true && $entity->{$ownerColumn} == $this->id) { - return true; + if($and === false){ + return true; + } else { + $related = true; + } + } - return $this->isAllowed($providedPermission, $entity); + $allowed = $this->isAllowed($providedPermission, $entity); + + if($and === false){ + return $related || $allowed; + } else { + return $related && $allowed; + } } + /** + * Checks if the user is allowed to manipulate the entity, but through a relationship + * + * @param $providedPermission + * @param Model $entity + * @param string $relation_name + * @param bool $owner + * @param bool $and + * @return bool + */ + public function allowedThrough($providedPermission, Model $entity, $relation_name, $owner = true, $and = false) + { + if ($this->isPretendEnabled()) { + return $this->pretend('allowed'); + } + + $related = false; + if($owner === true){ + $relation = $this->$relation_name(); + switch ((new ReflectionClass($relation))->getShortName()) { + case 'BelongsTo': + $related = $this->checkBelongsTo($relation, $entity); + break; + + case 'BelongsToMany': + $related = $this->checkbelongsToMany($relation, $entity); + break; + + case 'HasMany': + case 'HasOne': + $related = $this->checkHasOneOrMany($relation, $entity); + break; + + case 'HasManyThrough': + $related = $this->checkHasManyThrough($relation, $entity); + break; + + case 'MorphMany': + case 'MorphOne': + $related = $this->checkMorphOneOrMany($relation, $entity); + break; + + case 'MorphTo': + $related = $this->checkMorphTo($relation, $entity); + break; + + case 'MorphToMany': + $related = $this->checkMorphToMany($relation, $entity); + break; + } + + if($related && $and === false){ + return true; + } + + } + + $allowed = $this->isAllowed($providedPermission, $entity); + + if($and === false){ + return $related || $allowed; + } else { + return $related && $allowed; + } + } + + /** + * Checks if the provided entity is with the provided BelongsTo relationship + * + * @param BelongsTo $relation + * @param Model $entity + * @return bool + */ + protected function checkBelongsTo(BelongsTo $relation, Model $entity) + { + $this_key = $relation->getForeignKey(); + $entity_key = $relation->getOtherKey(); + + return $this->$this_key === $entity->$entity_key; + } + + /** + * Check if the provided entity is within the provided BelongsToMany relationship + * + * @param BelongsToMany $relation + * @param Model $entity + * @return bool + */ + protected function checkBelongsToMany(BelongsToMany $relation, Model $entity) + { + $entity_qualified_key_name = $relation->getOtherKey(); + + return $relation + ->where($entity_qualified_key_name, $entity->getKey()) + ->exists(); + } + + /** + * Checks if the provided entity is within the provided HasOneOrMany relationship + * + * @param HasOneOrMany $relation + * @param Model $entity + * @return bool + */ + protected function checkHasOneOrMany(HasOneOrMany $relation, Model $entity) + { + $this_key = $this->unqualifiedKeyName($relation->getQualifiedParentKeyName()); + $entity_key = $relation->getPlainForeignKey(); + + return $this->$this_key === $entity->$entity_key; + } + + /** + * Checks if the provided entity is within the provided HasManyThrough relationship + * + * @param HasManyThrough $relation + * @param Model $entity + * @return bool + */ + protected function checkHasManyThrough(HasManyThrough $relation, Model $entity) + { + $entity_qualified_key_name = $relation->getRelated()->getQualifiedKeyName(); + $entity_key_name = $relation->getRelated()->getKeyName(); + + return $relation + ->where($entity_qualified_key_name, $entity->$entity_key_name) + ->exists(); + } + + /** + * Checks if the provided entity is within the provided MorphOneOrMany relationship + * + * @param MorphOneOrMany $relation + * @param Model $entity + * @return bool + */ + protected function checkMorphOneOrMany(MorphOneOrMany $relation, Model $entity) + { + $entity_qualified_key_name = $relation->getRelated()->getQualifiedKeyName(); + $entity_key_name = $relation->getRelated()->getKeyName(); + + return $relation + ->where($entity_qualified_key_name, $entity->$entity_key_name) + ->exists(); + } + + /** + * Checks if the provided entity is within the provided MorphTo relationship + * + * @param MorphTo $relation + * @param Model $entity + * @return bool + */ + protected function checkMorphTo(MorphTo $relation, Model $entity) + { + $morphed_type = $this->{$relation->getMorphType()}; + $this_key = $relation->getForeignKey(); + $entity_key = $relation->getOtherKey(); + + return ( + $entity instanceof $morphed_type + && $this->$this_key === $entity->$entity_key + ); + } + + /** + * Checks if the provided entity is within the provided MorphToMany relationship + * + * @param MorphToMany $relation + * @param Model $entity + * @return bool + */ + protected function checkMorphToMany(MorphToMany $relation, Model $entity) + { + $entity_qualified_key_name = $relation->getOtherKey(); + $entity_key_name = $relation->getRelated()->getKeyName(); + + return $relation + ->where($entity_qualified_key_name, $entity->$entity_key_name) + ->exists(); + } + + + /** + * Takes a qualified key name and returns the unqualified version + * + * @param $key + * @return mixed + */ + private function unqualifiedKeyName($key) + { + $segments = explode('.', $key); + + return array_pop($segments); + } + /** * Check if the user is allowed to manipulate with provided entity. *