Roles and Permissions
The mental model is identical to Laravel Spatie: users have roles, roles have permissions, and users can also have permissions directly. Every assignment is polymorphic, so any model that mixes in Authorizable can act as the subject.
The Graph
User --- model_has_roles ---> Role --- role_has_permissions ---> Permission
\ /
----------- model_has_permissions -------------------------->
Both direct and role-inherited permissions are merged when you ask has questions on the user (user.hasPermissionTo('posts.publish'), user.getAllPermissions()).
Creating Roles And Permissions
import { Role, Permission } from '@admicaa/netpress-permissions';
const editor = await Role.findOrCreate('editor');
const publish = await Permission.findOrCreate('posts.publish', {
group: 'posts',
description: 'Publish any post to the public feed',
});
findOrCreate is idempotent and safe to call from a seeder.
Role → Permission
await editor.givePermissionTo(publish);
await editor.givePermissionTo('posts.archive', 'posts.edit'); // resolved by name
await editor.revokePermissionTo('posts.edit');
await editor.syncPermissions('posts.publish', 'posts.archive');
You can pass:
- A
Permissionmodel instance - A numeric id
- A string name — the package will resolve it via
Permission.findByName(name)
User → Role
import { BaseModel } from '@admicaa/netpress';
import { Authorizable } from '@admicaa/netpress-permissions';
class User extends Authorizable(BaseModel) {
static table = 'users';
}
const user = await User.create({ name: 'Amina' });
await user.assignRole(editor);
await user.assignRole('reviewer');
await user.syncRoles('editor', 'reviewer');
await user.removeRole('reviewer');
User → Permission (Direct)
Sometimes a user needs a single permission without earning a whole role:
await user.givePermissionTo('billing.view');
await user.revokePermissionTo('billing.view');
await user.syncPermissions('billing.view');
Asking Questions
await user.hasRole('editor'); // true/false
await user.hasAnyRole('editor', 'reviewer'); // true/false
await user.hasAllRoles('editor', 'reviewer'); // true/false
await user.hasPermissionTo('posts.publish'); // direct OR via roles
await user.hasDirectPermission('billing.view'); // only direct
await user.hasAnyPermission('a', 'b'); // any of the list
await user.hasAllPermissions('a', 'b'); // all of the list
await user.can('posts.publish'); // policy-aware helper
can runs through NetPress core authorization: it checks registered policies first, then falls back to the user's roles and direct permissions through hasPermissionTo().
Listing
const roles = await user.getRoles(); // Role models
const roleNames = await user.getRoleNames(); // ['editor']
const direct = await user.getDirectPermissions();
const viaRoles = await user.getPermissionsViaRoles();
const all = await user.getAllPermissions();
Inverse Lookups
Because the pivot is polymorphic, you can walk in the other direction:
import { User, Admin } from '../app/models/index.js';
const audit = await Permission.findOrCreate('audit.view');
const userHolders = await audit.holders(User).get(); // Users with the permission
const adminHolders = await audit.holders(Admin).get(); // Admins with the permission
const manager = await Role.findOrCreate('manager');
const managerUsers = await manager.assignees(User).get();
Use holders(ModelClass) on a Permission and assignees(ModelClass) on a Role. The class you pass must be registered in the morph map (the Authorizable trait doesn't do that for you — keep it explicit in one bootstrap file).