Hackernoon logoWriting CASL React Abilities in a JSON File With a Laravel Artisan Command by@Bassaganas

Writing CASL React Abilities in a JSON File With a Laravel Artisan Command

Author profile picture

@BassaganasJordi Bassagañas

Hi there! How are you today? I blog about technology, the Internet, SEO, programming tips, and more.

When writing a single page application or SPA you might inadvertently end up duplicating code because at the early stage of any project it is quite common to ask yourself so many questions.
Is this or that piece of code to be written here or there?
We've all been there, however it is important to clearly define the responsibilities of both the backend and the frontend.
Don't repeat yourself (DRY) is a principle of software development aimed at reducing repetition of software patterns.
Let me show you how access control rules are defined only once in a PHP backend to be also consumed in a React application by the
@casl/react
component.
CASL (pronounced /ˈkæsəl/, like castle) allows to integrate
@casl/ability
into your React application in order to show or hide UI elements based on the user's ability to see them.
The wrong thing to do in this case, if not a nightmare, would be to deal with two different set of rules: CASL abilities in the frontend and ACL rules in the backend.
The good news are that setting up everything at once is easy peasy with the help of the following Laravel Artisan command.
php artisan acl:setup


app/Console/Commands/AclSetup.php
:
<?php

namespace App\Console\Commands;

use App\Acl;
use Illuminate\Console\Command;

class AclSetup extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'acl:setup';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'ACL setup.';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $this->storeInDatabase();

        file_put_contents(storage_path().'/ability-rules.json', $this->caslAbilityRules());
    }

    private function storeInDatabase()
    {
        foreach (Acl::CHOICE_PERMISSIONS as $role => $resources) {
            foreach ($resources as $resource) {
                $restaurant = Acl::create([
                    'resource' => $resource,
                    'role' => $role,
                ]);
            }
        }
    }

    /**
     * CASL abilities.
     *
     * @link https://stalniy.github.io/casl/abilities/2017/07/20/define-abilities.html
     */
    private function caslAbilityRules()
    {
        $rules = [];
        foreach (Acl::CHOICE_PERMISSIONS as $role => $resources) {
            foreach ($resources as $resource) {
                $item = str_replace('Controller', '', $resource);
                $exploded = explode('@', $item);
                $rules[$role][] = [
                    'action' => $exploded[1],
                    'subject' => $exploded[0],
                ];
            }
        }

        return json_encode($rules, JSON_PRETTY_PRINT);
    }
}
The important thing to be noted is that the exact same access rules are both loaded into the app's database and written into the
storage/ability-rules.json
file according to
@casl/ability
syntax.
{
    "ROLE_BASIC": [
        {
            "action": "index",
            "subject": "Restaurant"
        },
        {
            "action": "store",
            "subject": "Review"
        }
    ],
    "ROLE_EDITOR": [
        {
            "action": "index",
            "subject": "Restaurant"
        },
        {
            "action": "show",
            "subject": "Restaurant"
        },
        {
            "action": "update",
            "subject": "Restaurant"
        },
        {
            "action": "delete",
            "subject": "Restaurant"
        },
        {
            "action": "delete",
            "subject": "Review"
        },
        {
            "action": "index",
            "subject": "User"
        }
    ],
    "ROLE_ADMIN": [
        {
            "action": "index",
            "subject": "Restaurant"
        },
        {
            "action": "show",
            "subject": "Restaurant"
        },
        {
            "action": "store",
            "subject": "Restaurant"
        },
        {
            "action": "update",
            "subject": "Restaurant"
        },
        {
            "action": "delete",
            "subject": "Restaurant"
        },
        {
            "action": "delete",
            "subject": "Review"
        },
        {
            "action": "index",
            "subject": "User"
        },
        {
            "action": "show",
            "subject": "User"
        },
        {
            "action": "store",
            "subject": "User"
        },
        {
            "action": "update",
            "subject": "User"
        },
        {
            "action": "delete",
            "subject": "User"
        },
        {
            "action": "delete",
            "subject": "User"
        }
    ]
}
Finally, this is the
app/Acl.php
model where permissions are actually granted to user roles.
<?php

namespace App;

use App\User;
use Illuminate\Database\Eloquent\Model;

class Acl extends Model
{
    protected $fillable = ['resource', 'role'];

    const CHOICE_PERMISSIONS = [
        User::CHOICE_ROLE_BASIC => [
            'RestaurantController@index',
            'ReviewController@store',
        ],
        User::CHOICE_ROLE_EDITOR => [
            'RestaurantController@index',
            'RestaurantController@show',
            'RestaurantController@update',
            'RestaurantController@delete',
            'ReviewController@delete',
            'UserController@index',
        ],
        User::CHOICE_ROLE_ADMIN => [
            'RestaurantController@index',
            'RestaurantController@show',
            'RestaurantController@store',
            'RestaurantController@update',
            'RestaurantController@delete',
            'ReviewController@delete',
            'UserController@index',
            'UserController@show',
            'UserController@store',
            'UserController@update',
            'UserController@delete',
            'UserController@delete',
        ],
    ];

    public static function getChoices()
    {
        return [
            'permissions' => self::CHOICE_PERMISSIONS,
        ];
    }

    public static function grantedRoles(string $resource)
    {
        $roles = [];
        foreach (self::CHOICE_PERMISSIONS as $key => $val) {
            in_array($resource, $val) ? $roles[] = $key : false;
        }

        return $roles;
    }

    public static function isAuthorized(string $role, string $resource)
    {
        return in_array($role, self::grantedRoles($resource));
    }
}
By loading the
storage/ability-rules.json
file in the frontend, we'll be in a position to benefit from the declarative syntax of the CASL React component at the same time that everything is perfectly in sync.
import Can from '../Can';
import { Col, Row } from 'reactstrap';
import { LoremIpsum } from '../common/LoremIpsum';
import RestaurantCreate from './restaurant/Create';
import RestaurantIndex from './restaurant/Index';
import React from 'react';

class Restaurants extends React.Component {
  render() {
    return (
      <Row className="m-3">
        <Col md={9}>
          <RestaurantIndex />
        </Col>
        <Col md={3}>
          <Can I="store" a="Restaurant">
            <RestaurantCreate />
          </Can>
          <Can not I="store" a="Restaurant">
            <LoremIpsum />
          </Can>
        </Col>
      </Row>
    );
  }
}

export { Restaurants };
For further details on this implementation please visit programarivm/warthog which is a real-worldish React app where I am playing around with Redux and Thunk. programarivm/meerkat is another real-worldish app using Flux instead of Redux.

You May Also Be Interested in...

Author profile picture

@BassaganasJordi Bassagañas

Read my stories

Hi there! How are you today? I blog about technology, the Internet, SEO, programming tips, and more.

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!