Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get Model Where User Has PermissionX #622

Open
SMPEJake opened this issue Feb 14, 2023 · 3 comments
Open

Get Model Where User Has PermissionX #622

SMPEJake opened this issue Feb 14, 2023 · 3 comments

Comments

@SMPEJake
Copy link

Hello!

I am probably missing something simple here but is there a way i can use a users role or ability as a where clause on an eloquent query?

Thanks!

@lrljoe
Copy link

lrljoe commented Feb 19, 2023

Couple of relatively easy options, depends on how you're using this.

If you're using a mix of roles and direct abilities, then use getAbilities() and filter on the model you're wanting to look at. Then add the role's abilities in.

You can either trust it or filter it further through an each() loop.

Or if you're using scopes then that's a little easier.

Or if you're using the Owns approach then you should already have the approach for that. May be the easiest solution for you?

@timyourivh
Copy link

I do something similar to this in a scope:

// Example to filter entities I'm allowed to "view":
$user = Auth::user(); // Currently logged in user (could be any user with roles and permissions).
$abilities = $user->getAbilities() // Get all the abilities (including the ones inherited from roles).
                ->where('entity_type', get_class(Entity::class)) // Filter permissions that concern this model.
                ->where('name', 'view') // Filter specific permission.
                ->pluck('id'); // Get all ID's of the resulting entities.

Entity::whereIn('id', $abilities)->get(); // Expected output: Enities where user has permissions to "view".

For anyone interested here's my scope:

My scope method

Run php artisan make:scope PermissionScope and add this to it:

<?php
namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Facades\Auth;

class PermissionScope implements Scope
{
  /**
   * Apply the scope to a given Eloquent query builder.
   */
  public function apply(Builder $builder, Model $model): void
  {
      if (Auth::hasUser()) {
          // Exclude forbidden items.
          $builder->whereNotIn('id', Auth::user()->getForbiddenAbilities()->where('entity_type', get_class($model))->pluck('entity_id'));

          // Filter explicit allowed items.
          $allowed = Auth::user()->getAbilities()->where('entity_type', get_class($model));
          if ($allowed->whereNotNull('entity_id')->isNotEmpty() && $allowed->where('name', '*')->whereNull('entity_id')->isEmpty()) {
              $builder->whereIn('id', $allowed->pluck('entity_id'));
          }
      }
  }
}

What this scope does:

  • Filters out forbidden records
  • Filters out records when user is allowed to "viewAny" but only "view" specific records

I then put it in a trait:

  1. Create a file app\Concerns\FilterAllowed.php
  2. Add this to the file:
<?php

namespace App\Concerns;

use App\Models\Scopes\PermissionScope;

trait FilterAllowed {
  /**
   * Boot the filter allowed trait for a model.
   */
  public static function bootFilterAllowed(): void
  {
      static::addGlobalScope(new PermissionScope);
  }
}
  1. Add trait to model to make use of it:
use App\Concerns\FilterAllowed;

class Entity extends Model
{
    use FilterAllowed;
}

Now, whenever I want the records automatically to be filtered I just add the trait to the model.

@EriBloo
Copy link

EriBloo commented May 16, 2023

I have something similiar to @timyourivh in my app:

/**
 * Class PermissionConstrained
 *
 * Implements the Scope interface to apply permission constraints to a given Eloquent query builder.
 */
class PermissionConstrained implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  Builder  $builder The Eloquent query builder instance.
     * @param  Model&IsPermissionConstrained  $model The model instance to which the scope is applied.
     */
    public function apply(Builder $builder, Model $model): void
    {
        // If checkPermissions is not true or the user is a guest,
        // return without applying the scope.
        if (! $model::$checkPermissions || auth()->guest()) {
            return;
        }

        // Get the permissions that the authenticated user is forbidden from viewing.
        $forbidden = $this->getPermissions($model, false);

        // If the user is forbidden from viewing all records of this model,
        // return an empty result set.
        if ($forbidden->contains(fn (Ability $ability) => $ability->entity_id === null)) {
            $builder->where($model->getKeyName(), null);

            return;
        }

        // Get the permissions that the authenticated user is allowed to view.
        $allowed = $this->getPermissions($model);

        // If the user is allowed to view all records of this model, return without applying the scope.
        if ($allowed->contains(fn (Ability $ability) => $ability->entity_id === null)) {
            return;
        }

        // Apply the scope to the query builder.
        $builder->whereIn("{$model->getTable()}.{$model->getKeyName()}", $allowed->pluck('entity_id')->toArray());
        $builder->whereNotIn("{$model->getTable()}.{$model->getKeyName()}", $forbidden->pluck('entity_id')->toArray());
    }

    /**
     * Extend the query builder with the scope.
     *
     * @param  Builder  $builder The query builder instance.
     */
    public function extend(Builder $builder): void
    {
        $builder->macro('ignorePermissions', function (Builder $builder): Builder {
            return $builder->withoutGlobalScope($this);
        });
    }

    /**
     * Get the permissions for the given model and allowed flag.
     *
     * @param  Model  $model The model instance for which to get the permissions.
     * @param  bool  $allowed The flag indicating whether to get allowed or forbidden permissions.
     * @return Collection The collection of permissions.
     */
    private function getPermissions(Model $model, bool $allowed = true): Collection
    {
        return Cache::driver('array')->sear(
            $this->getCacheKey($model, $allowed),
            function () use ($model, $allowed) {
                return auth()->user()?->{$allowed ? 'getAbilities' : 'getForbiddenAbilities'}()
                    ->whereIn('name', ['*', 'view'])
                    ->whereIn('entity_type', ['*', $model::class]);
            }
        );
    }

    /**
     * Get the cache key for the given model and allowed flag.
     *
     * @param  Model  $model The model instance for which to get the cache key.
     * @param  bool  $allowed The flag indicating whether to include the allowed or forbidden permissions in the cache key.
     * @return string The cache key for the model and allowed flag.
     */
    private function getCacheKey(Model $model, bool $allowed = true): string
    {
        return implode(':', ['permissions', $model->getMorphClass(), $allowed ? 'a' : 'f']);
    }

and trait:

trait IsPermissionConstrained
{
   public static bool $checkPermissions = true;

   public static function bootIsPermissionConstrained(): void
   {
       self::addGlobalScope(new PermissionConstrained);
   }

   public static function withoutPermissions(Closure $closure): void
   {
       self::$checkPermissions = false;
       $closure();
       self::$checkPermissions = true;
   }
}

I have 2 ways to disable permissions. First is ignorePermissions() method - this will disable the scope only for current query. The other is $checkPermissions static variable. The idea is that I have this trait connected to base model that all other models extend from, and when I set Model::checkPermissions = false all queries (including relations) will ignore permissions. There is also helper withoutPermissions method to execute queries closure ignoring permissions.

Scope caches all permissions in array driver - so only for current request (might not work with Octane). This speeds up abilities retrieval greatly with many permissions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants