Role-Based Permissions
In this section, we will explore how to define and enforce role-based permissions using Asgardian. Role-based access control (RBAC) allows you to assign permissions to users based on their roles, making it easier to manage access control in your application.
Role-Based Permission Patterns
There are several patterns you can use to implement role-based permissions with Asgardian:
Pattern 1: Separate Ability Instances
Create distinct ability instances for different roles:
import { createAbility } from '@nordic-ui/asgardian'
// Admin abilities - full access
const adminAbility = createAbility().can('manage', 'all')
// Editor abilities - can manage content but not delete
const editorAbility = createAbility()
.can(['create', 'read', 'update'], 'Post')
.can(['create', 'read', 'update'], 'Comment')
// Author abilities - can manage own content
const authorAbility = createAbility()
.can(['read', 'create'], 'Post')
.can(['update', 'delete'], 'Post', { authorId: 123 })
.can(['read', 'create'], 'Comment')
.can(['update', 'delete'], 'Comment', { authorId: 123 })
// Visitor abilities - read-only access
const visitorAbility = createAbility()
.can('read', 'Post', { published: true })
.can('read', 'Comment')
Pattern 2: Role-Based Ability Factory
Create a factory function that returns the appropriate ability instance:
type Role = 'admin' | 'editor' | 'author' | 'visitor'
type Resources = 'Post' | 'Comment'
function createRoleAbility(role: Role, userId?: number) {
const ability = createAbility()
switch (role) {
case 'admin':
ability.can('manage', 'all')
break
case 'editor':
ability
.can(['create', 'read', 'update'], 'Post')
.can(['create', 'read', 'update'], 'Comment')
break
case 'author':
ability
.can(['read', 'create'], 'Post')
.can(['update', 'delete'], 'Post', { authorId: 123 })
.can(['read', 'create'], 'Comment')
.can(['update', 'delete'], 'Comment', { authorId: 123 })
break
case 'visitor':
ability
.can('read', 'Post', { published: true })
.can('read', 'Comment')
break
}
return ability
}
Pattern 3: Composable Ability Modules
Break down permissions into smaller, focused modules that can be composed together:
// Define specific permission modules
function createContentPermissions(userId: number) {
return createAbility()
.can('read', 'Post', { published: true })
.can(['create', 'update'], 'Post', { authorId: userId })
.can('read', 'Comment')
}
function createModeratorPermissions() {
return createAbility()
.can('delete', 'Post')
.can('delete', 'Comment')
.can('update', 'Post', { reported: true })
}
function createAdminPermissions() {
return createAbility().can('manage', 'all')
}
type User = { id: number, roles: ('admin', 'moderator', 'user')[] }
// Compose permissions based on user roles
function createUserAbility(user: User) {
const abilities = [
// Base permissions everyone gets
createContentPermissions(user.id)
]
if (user.roles.includes('moderator')) {
abilities.push(createModeratorPermissions())
}
if (user.roles.includes('admin')) {
abilities.push(createAdminPermissions())
}
// Combine all permissions
const finalAbility = createAbility()
for (const ability of abilities) {
ability.rules.forEach(rule => {
if (rule.inverted) {
finalAbility.cannot(rule.action, rule.resource, rule.conditions)
} else {
finalAbility.can(rule.action, rule.resource, rule.conditions)
}
})
}
return finalAbility
}
Using Role-Based Abilities
Here’s how to use the role-based abilities in your application:
// Create ability instance based on user's role
const user = {
id: 123,
role: 'author'
}
const ability = createRoleAbility(user.role, user.id)
// Check permissions
ability.isAllowed('read', 'Post', { published: true }) // true
ability.isAllowed('update', 'Post', { authorId: 123 }) // true
ability.isAllowed('update', 'Post', { authorId: 456 }) // false
ability.isAllowed('delete', 'Post', { authorId: 456 }) // false
Multiple Roles
To handle users with multiple roles, you can combine permissions from different roles:
function createMultiRoleAbility(roles: Role[], userId?: number) {
const ability = createAbility<never, Resources>()
if (roles.includes('admin')) {
ability.can('manage', 'all')
return ability // Admin has all permissions, no need to check other roles
}
if (roles.includes('editor')) {
ability
.can(['create', 'read', 'update'], 'Post')
.can(['create', 'read', 'update'], 'Comment')
}
if (roles.includes('author')) {
ability
.can('read', 'Post')
.can(['create', 'update'], 'Post', { authorId: userId })
.can(['read', 'create'], 'Comment')
}
// Visitor permissions are implicit for all users
ability
.can('read', 'Post', { published: true })
.can('read', 'Comment')
return ability
}
// Usage with multiple roles
const userWithMultipleRoles = {
id: 123,
roles: ['editor', 'author'] as Role[]
}
const ability = createMultiRoleAbility(userWithMultipleRoles.roles, userWithMultipleRoles.id)
Best Practices
- Start restrictive
// ✅ Good: Start with limited permissions const ability = createAbility() .can('read', 'Post', { published: true }) .can('update', 'Post', { authorId: userId }) // ❌ Bad: Starting with broad permissions const ability = createAbility() .can('manage', 'all') .cannot('delete', 'Post')
- Be explicit
// ✅ Good: Specific resource access const editorAbility = createAbility() .can('manage', 'Post') // Explicitly grant all actions on posts .can('read', 'Comment') // Explicitly grant only read access on comments // ❌ Bad: Over-permissive resource access const editorAbility = createAbility() .can('manage', 'all') // Too broad, grants access to all current and future resources
- Use type safety
// ✅ Good: Type-safe roles and resources type Role = 'admin' | 'editor' | 'author' | 'visitor' type Resources = 'Post' | 'Comment' const ability = createAbility<never, Resources>() // ❌ Bad: No type safety const ability = createAbility<any, any>()
Summary
In this section, we learned how to define and enforce role-based permissions using Asgardian. Role-based access control allows you to manage user permissions efficiently by grouping them into roles and defining rules that apply to these roles.