Daravelv1.13.0
ENID GitHub

Roles and permissions

daravel_permission keeps roles and permissions in the database, on the five tables of spatie/laravel-permission v6, so a Laravel application using Spatie and a Daravel application can share them: a role given by either is seen by the other.

dart pub add daravel_permission
// bootstrap/app.dart
import 'package:daravel_permission/daravel_permission.dart';

Application createApp() => Application.configure(basePath: basePath)
    .withPermissions()
    .withMigrations([...migrations, const CreatePermissionTables()])
    // ...
    .create();

CreatePermissionTables creates roles, permissions, model_has_roles, model_has_permissions and role_has_permissions with Spatie's columns, keys and index names, and does nothing when permissions exists already, so a database that ran Spatie's migration keeps it.

The model#

final class User extends Model with AuthenticatableModel, HasRoles {
  // ...
}

Assignments are stored under the model's morph class (App\Models\User by default), as Laravel stores them.

final writer = await Role.create('writer');
await Permission.create('posts.edit');
await writer.givePermissionTo('posts.edit');

await user.assignRole('writer');            // a name, a Role, an id or a list
await user.givePermissionTo('posts.publish'); // directly, without a role

await user.hasRole('writer');               // also 'admin|writer'
await user.hasAnyRole(['admin', 'writer']);
await user.hasAllRoles(['admin', 'writer']);
await user.hasPermissionTo('posts.edit');   // through a role or directly
await user.getRoleNames();                  // ['writer']
await user.getAllPermissions();             // ['posts.edit', 'posts.publish']

await user.syncRoles(['editor']);           // replaces the roles
await user.removeRole('editor');
await user.revokePermissionTo('posts.publish');

As in Spatie, hasPermissionTo throws PermissionDoesNotExist for a permission nobody could have (a typo), and checkPermissionTo answers false instead. Role.create refuses a duplicate (RoleAlreadyExists), and a role of another guard cannot be given (GuardDoesNotMatch); the messages are Spatie's.

Guards#

Roles and permissions belong to a guard (guard_name). It is web unless PERMISSION_GUARD says otherwise, since that is the name a Laravel application uses, so the rows match. A model of another guard overrides it:

final class Admin extends Model with AuthenticatableModel, HasRoles {
  @override
  String get guardName => 'admin';
}

await Role.create('admin', guard: 'admin');

Gates and middleware#

withPermissions() adds a gate hook, as Spatie's Gate::before: a permission the user has makes Gate.allows('posts.edit'), the can:posts.edit middleware and authorize() pass, and any other ability goes on to your gate definitions and policies.

router.get('/admin', admin.index).middleware(['auth', 'role:admin|editor']);
router.post('/posts', posts.store).middleware(['auth', 'permission:posts.create']);
router.put('/posts/{id}', posts.update).middleware([
  'auth',
  'role_or_permission:admin|posts.edit',
]);

The names are separated by | (any of them); after a comma comes an auth guard (role:admin,token). A refusal is a 403 with Spatie's messages:

{"message": "User does not have the right roles."}

PERMISSION_DISPLAY_ROLE_IN_EXCEPTION and PERMISSION_DISPLAY_PERMISSION_IN_EXCEPTION add the names to the message.

PERMISSION_SUPER_ADMIN_ROLE=root lets a role pass every gate check, the pattern Spatie documents; it is off by default.

Wildcards#

With PERMISSION_WILDCARD=true (Spatie's enable_wildcard_permission), permissions match by parts:

Granted Grants
posts.* or posts posts.edit, posts.edit.own
posts.edit,delete posts.edit, posts.delete
posts.*.own posts.edit.own, not posts.edit.all

The cache#

A model's roles and permissions are kept in memory after the first check, so a check is a set lookup instead of three queries. A change made through Daravel (assignRole, givePermissionTo, the commands, ...) applies at once in its isolate and within a second on every other isolate and process of the machine; with a cache store shared between servers (CACHE_STORE=database or redis), on every server. A change made by a Laravel application shows within PERMISSION_CACHE_TTL (5 minutes).

Commands#

Add them to bootstrap/commands.dart, from a library of their own so the server does not compile the CLI:

import 'package:daravel_permission/commands.dart';

final List<CommandFactory> commands = [...permissionCommands];
Command What it does
permission:create-role <name> [--guard] [--permissions=a,b] Creates a role, and gives it permissions
permission:create-permission <name> [--guard] Creates a permission
permission:assign <id> <role> [--model] [--guard], permission:revoke ... Gives or takes a role (--model is the stored type, App\Models\User by default)
permission:show [--guard] Roles against permissions
permission:cache-reset Forgets the cached roles and permissions

With Laravel#

bench/interop/permission.sh runs Laravel 12 with spatie/laravel-permission 6 and Daravel on one database: Laravel migrates and assigns, Daravel reads it, Daravel assigns, Laravel reads it (hasRole, can). Teams (permission.teams) and UUID model keys are not supported yet.