Access Control (RBAC)¶
Role-based access control with fine-grained permissions.
Overview¶
AuthVital implements a flexible Role-Based Access Control (RBAC) system with:
- Tenant Roles: Built-in roles for tenant management (Owner, Admin, Member)
- Application Roles: Custom roles per application with permissions
- Permissions: Fine-grained access control strings
- App Access: Control which users can access which applications
Role Hierarchy¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ AuthVital RBAC System │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ TENANT LEVEL APPLICATION LEVEL │
│ ───────────── ───────────────── │
│ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ Owner │ ─ Full tenant control │ App Admin │ ─ Full app access │
│ └────┬─────┘ └──────┬───────┘ │
│ │ │ │
│ ┌────▼─────┐ ┌──────▼───────┐ │
│ │ Admin │ ─ Manage members/roles │ Manager │ ─ Create/edit │
│ └────┬─────┘ └──────┬───────┘ │
│ │ │ │
│ ┌────▼─────┐ ┌──────▼───────┐ │
│ │ Member │ ─ Basic access │ Viewer │ ─ Read-only │
│ └──────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Tenant Roles¶
Built-in roles that control tenant-level permissions:
| Role | Permissions |
|---|---|
| Owner | Full access, delete tenant, manage billing, transfer ownership |
| Admin | Manage members, roles, SSO, settings (except delete/billing) |
| Member | Basic access to tenant resources |
Default Tenant Roles¶
These are created automatically for each tenant:
const DEFAULT_TENANT_ROLES = [
{
slug: 'owner',
name: 'Owner',
description: 'Full access to tenant',
isDefault: false,
permissions: ['tenant:*'],
},
{
slug: 'admin',
name: 'Admin',
description: 'Manage members and settings',
isDefault: false,
permissions: [
'tenant:read',
'tenant:members:*',
'tenant:settings:*',
'tenant:sso:*',
],
},
{
slug: 'member',
name: 'Member',
description: 'Basic tenant access',
isDefault: true, // Assigned to new members
permissions: ['tenant:read'],
},
];
Application Roles¶
Custom roles defined per application:
// Example: Project Management App roles
const appRoles = [
{
slug: 'admin',
name: 'Administrator',
description: 'Full application access',
permissions: [
'projects:*',
'users:*',
'settings:*',
'billing:*',
],
},
{
slug: 'manager',
name: 'Project Manager',
description: 'Create and manage projects',
permissions: [
'projects:create',
'projects:read',
'projects:update',
'projects:delete',
'projects:members:*',
'users:read',
],
},
{
slug: 'member',
name: 'Team Member',
description: 'Work on assigned projects',
permissions: [
'projects:read',
'projects:tasks:*',
'users:read',
],
},
{
slug: 'viewer',
name: 'Viewer',
description: 'Read-only access',
permissions: [
'projects:read',
'users:read',
],
},
];
Permissions¶
Format¶
Permissions use a resource:action format:
resource:action
resource:sub-resource:action
resource:* (all actions)
* (superadmin - all permissions)
Examples¶
// Common permission patterns
const permissions = [
// Basic CRUD
'users:read',
'users:create',
'users:update',
'users:delete',
// Nested resources
'projects:tasks:create',
'projects:tasks:delete',
'projects:members:invite',
'projects:members:remove',
// Wildcards
'projects:*', // All project actions
'billing:*', // All billing actions
'settings:*', // All settings actions
// Admin-level
'*', // Superadmin - all permissions
];
Permission Inheritance¶
Wildcards include all sub-permissions:
projects:* includes:
- projects:read
- projects:create
- projects:update
- projects:delete
- projects:tasks:*
- projects:members:*
Data Model¶
erDiagram
Application ||--o{ Role : "defines"
Role ||--o{ RolePermission : "grants"
Permission ||--o{ RolePermission : "granted by"
Membership ||--o{ MembershipRole : "has"
Role ||--o{ MembershipRole : "assigned via"
AppAccess ||--o{ AppAccessRole : "has"
Role ||--o{ AppAccessRole : "assigned via"
Role {
string id PK
string name
string slug
string applicationId FK
string description
boolean isDefault
}
Permission {
string id PK
string key
string name
string description
}
MembershipRole {
string membershipId FK
string roleId FK
}
AppAccess {
string id PK
string userId FK
string tenantId FK
string applicationId FK
} App Access¶
Control which users can access which applications:
Access Modes¶
| Mode | Description |
|---|---|
AUTOMATIC | All tenant members automatically get access |
MANUAL_AUTO_GRANT | Manual control, but new members get access by default |
MANUAL_NO_DEFAULT | Manual control, new members must be explicitly granted |
DISABLED | No new access grants (existing access preserved) |
Managing Access¶
App access is managed through the AuthVital Admin Dashboard or via the Invitations API:
// Grant access via invitation
const { sub, expiresAt } = await authvital.invitations.send(request, {
email: 'user@example.com',
givenName: 'John',
familyName: 'Doe',
roleId: 'role-admin', // Optional: assign app role
});
// List members with their app access
const { memberships } = await authvital.memberships.listForApplication(request, {
status: 'ACTIVE',
});
Admin Dashboard for Direct Access Management
For directly granting, revoking, or updating app access without invitations, use the AuthVital Admin Dashboard under Tenants → [Tenant] → Applications.
SDK Usage¶
Check Permission¶
// Server-side
const { allowed } = await authvital.permissions.check(req, 'projects:delete');
if (!allowed) {
return res.status(403).json({ error: 'Forbidden' });
}
Check Multiple Permissions¶
// Use checkMany to check multiple permissions at once
const { results } = await authvital.permissions.checkMany(req, [
'projects:read',
'projects:update',
]);
// results: { 'projects:read': true, 'projects:update': false }
// Check ALL permissions (must have all)
const hasAll = Object.values(results).every(v => v);
// Check ANY permission (must have at least one)
const hasAny = Object.values(results).some(v => v);
Check Role¶
// From JWT claims
const hasRole = user.app_roles.includes('admin');
// Check any of multiple roles
const isAdminOrManager = ['admin', 'manager'].some(
role => user.app_roles.includes(role)
);
Middleware Examples¶
Express Permission Middleware¶
function requirePermission(...permissions: string[]) {
return async (req, res, next) => {
const { results } = await authvital.permissions.checkMany(req, permissions);
const allowed = Object.values(results).every(v => v);
if (!allowed) {
return res.status(403).json({
error: 'Forbidden',
required: permissions,
message: 'You do not have permission to perform this action',
});
}
next();
};
}
// Usage
app.delete('/api/projects/:id',
requireAuth,
requirePermission('projects:delete'),
deleteProjectHandler
);
NestJS Permission Guard¶
// decorator
export const RequirePermissions = (...permissions: string[]) =>
SetMetadata('permissions', permissions);
// guard
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const required = this.reflector.get<string[]>(
'permissions',
context.getHandler()
);
if (!required?.length) return true;
const { user } = context.switchToHttp().getRequest();
const userPerms = user.app_permissions || [];
return required.every(perm =>
userPerms.includes(perm) ||
userPerms.includes('*') ||
userPerms.includes(perm.split(':')[0] + ':*')
);
}
}
// usage
@Delete(':id')
@RequirePermissions('projects:delete')
async deleteProject(@Param('id') id: string) {
// ...
}
React Integration¶
⚠️ UI Components Are For Display Only
The HasPermission and HasRole components below control UI visibility only.
They do NOT enforce security! An attacker can bypass these by calling your API directly.
You MUST also enforce permissions server-side using the Express/NestJS middleware shown above.
Permission Component¶
function HasPermission({
permission,
children,
fallback = null
}: {
permission: string | string[];
children: React.ReactNode;
fallback?: React.ReactNode;
}) {
const { user } = useAuth();
const permissions = Array.isArray(permission) ? permission : [permission];
const userPerms = user?.app_permissions || [];
const hasPermission = permissions.every(perm =>
userPerms.includes(perm) ||
userPerms.includes('*') ||
userPerms.includes(perm.split(':')[0] + ':*')
);
return hasPermission ? <>{children}</> : <>{fallback}</>;
}
// Usage
<HasPermission
permission="users:delete"
fallback={<span>Not authorized</span>}
>
<DeleteUserButton />
</HasPermission>
Role Component¶
function HasRole({
role,
children,
fallback = null
}: {
role: string | string[];
children: React.ReactNode;
fallback?: React.ReactNode;
}) {
const { user } = useAuth();
const roles = Array.isArray(role) ? role : [role];
const hasRole = roles.some(r => user?.app_roles?.includes(r));
return hasRole ? <>{children}</> : <>{fallback}</>;
}
// Usage
<HasRole role={['admin', 'manager']}>
<AdminPanel />
</HasRole>
usePermissions Hook¶
function usePermissions() {
const { user } = useAuth();
const userPerms = user?.app_permissions || [];
const can = useCallback((permission: string) => {
return userPerms.includes(permission) ||
userPerms.includes('*') ||
userPerms.includes(permission.split(':')[0] + ':*');
}, [userPerms]);
const canAll = useCallback((permissions: string[]) => {
return permissions.every(can);
}, [can]);
const canAny = useCallback((permissions: string[]) => {
return permissions.some(can);
}, [can]);
return { can, canAll, canAny, permissions: userPerms };
}
// Usage
function ProjectActions({ projectId }) {
const { can } = usePermissions();
return (
<div>
{can('projects:update') && <EditButton />}
{can('projects:delete') && <DeleteButton />}
{can('projects:members:invite') && <InviteButton />}
</div>
);
}
Admin: Managing Roles¶
Create Custom Role¶
await authvital.roles.create({
applicationId: 'app-id',
name: 'Quality Assurance',
slug: 'qa',
description: 'Test and verify projects',
permissions: [
'projects:read',
'projects:tasks:read',
'projects:tasks:update',
'reports:read',
],
isDefault: false,
});
Update Role Permissions¶
await authvital.roles.update('role-id', {
permissions: [
'projects:read',
'projects:tasks:*', // Upgraded: full task access
'reports:*', // Added: full reports access
],
});
Assign Role to User¶
// Update a member's tenant role
await authvital.memberships.setMemberRole(
request, // HTTP request for JWT validation
'membership-id', // Membership ID to update
'admin' // New role slug
);
Pre-flight Validation
The SDK performs permission checks before calling the API: - Only owners and admins can change roles - Admins cannot promote anyone to owner - The IDP performs the final authoritative check
JWT Claims¶
Roles and permissions are included in the JWT:
{
"sub": "user-id",
"tenant_id": "tenant-id",
"tenant_role": "admin",
"app_roles": ["manager", "member"],
"app_permissions": [
"projects:create",
"projects:read",
"projects:update",
"projects:members:*",
"users:read"
]
}
Best Practices¶
✅ Do¶
- Use descriptive permission names -
projects:tasks:create>pt:c - Group by resource -
projects:*,users:* - Least privilege - Grant minimum permissions needed
- Audit role changes - Log who changed what
- Test permissions - Include in integration tests
❌ Don't¶
- Don't use
*liberally - Reserved for true superadmins - Don't hardcode roles - Use permissions instead
- Don't skip server validation - UI checks are not enough
- Don't forget default roles - New users need baseline access