Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel user capabilities

Tags:

Within Laravel you can easily define abilities and then hook into them later on a user request regarding to do different actions:

$gate->define('update-post', function ($user, $post) {
    return $user->id === $post->user_id;
});

But almost all my defined abilities has this part $user->id === $model->user_id in it. I don't like it as it's a kind of repeating a condition over and over which I think could be more abstract.

Most of my defined abilities are according to updating/deleting records, so it would be better if I could make a global condition applied to all of them or if there could be a group ability defining which is like to what we do in routing.

Is there any workaround for it? I really like it DRY.

like image 213
revo Avatar asked Dec 19 '15 20:12

revo


2 Answers

I checked your question quite a bit, but I've found no "easy" way to do it.

Instead, what I would probably do is this:

<?php


namespace App\Policies;

 use App\User;
 use App\Post;

trait CheckOwnership {
    protected function checkOwnership($user, $model) {
        $owned = $user->id === $model->user_id;
        if ($owned === false)
             throw new NotOwnedException;
    }    
 }

 class PostPolicy
 {

     use CheckOwnership;

    public function update(User $user, Post $post)
    {
         try {
             $this->checkOwnership($user, $post);
             //continue other checks
         } catch (NotOwnedException $ex) {
             return false;
         } 
    }
 }
like image 43
Tzook Bar Noy Avatar answered Sep 27 '22 20:09

Tzook Bar Noy


Everything in Laravel is extendable, that's the power of its service providers.

You can extend the Gate object to a MyCustomGate object and do whatever you want in that object. Here's an example:

MyCustomGate.php

class MyCustomGate extends \Illuminate\Auth\Access\Gate
{
    protected $hasOwnershipVerification = [];

    /**
     * Define a new ability.
     *
     * @param  string  $ability
     * @param  callable|string  $callback
     * @return $this
     *
     * @throws \InvalidArgumentException
     */
    public function defineWithOwnership($ability, $callback, $foreignUserIdKey = "user_id")
    {
        // We will add this 
        $this->hasOwnershipVerification[$ability] = $foreignUserIdKey;

        return $this->define($ability, $callback);
    }

    /**
     * Resolve and call the appropriate authorization callback.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  string  $ability
     * @param  array  $arguments
     * @return bool
     */
    protected function callAuthCallback($user, $ability, array $arguments)
    {
        $callback = $this->resolveAuthCallback(
            $user, $ability, $arguments
        );

        // We will assume that the model is ALWAYS the first key
        $model = is_array($arguments) ? $arguments[0] : $arguments;

        return $this->checkDirectOwnership($ability, $user, $model) && call_user_func_array(
            $callback, array_merge([$user], $arguments)
        );
    }

    /**
     * Check if the user owns a model.
     *
     * @param  string  $ability
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return bool
     */
    protected function checkDirectOwnership($ability, $user, $model)
    {
        if(!isset($this->hasOwnershipVerification[$ability])) {
            return true
        }

        $userIdKey = $this->hasOwnershipVerification[$ability];

        // getAuthIdentifier() is just ->id, but it's better in case the pk of a user is different that id
        return $user->getAuthIdentifier() == $model->{$userIdKey};
    }
}

Then, you will have to tell Laravel to use your gate instead of the default one. You ca do that in your AuthServiceProvider (assuming that it's extending Illuminate\Auth\AuthServiceProvider, just add the following method.

AuthServiceProvider

/**
 * Register the access gate service.
 *
 * @return void
 */
protected function registerAccessGate()
{
    $this->app->singleton(\Illuminate\Contracts\Auth\Access\Gate::class, function ($app) {
        return new MyCustomGate($app, function () use ($app) {
            return $app['auth']->user();
        });
    });
}

And this way, you can define abilities using defineWithOwnership() method instead of define(). You can still use define() for abilities that don't require ownership verification. There's a third parameter defineWithOwnership() accepts which is $foreignUserIdKey; that's used for the case when a model has a different field for the user id.

Note: I wrote the code on the fly and did not try it, it may have errors, but you get the idea.

like image 158
Blue Genie Avatar answered Sep 27 '22 22:09

Blue Genie