Authorization & Permissions
This document describes the role-based access control (RBAC) system in Tenanto.
Overview
Tenanto uses Spatie Laravel-Permission with multi-tenant team support:
- Tenant Roles: Scoped to individual tenants (owner, admin, member)
- System Roles: Global roles for system administrators
- Permissions: Granular access control within tenants
- Policies: Laravel policies for model-level authorization
Authentication
Is the user logged in?
Tenant context
Does the user belong to the current tenant?
Role and permission
Does the role grant the requested action?
Policy
Is this exact resource allowed for this user?
Tenant Roles
Role Definitions
| Role | Description | Use Case |
|---|---|---|
| Owner | Full access including billing | Account creator, paying customer |
| Admin | Manage users, teams, and resources | Team managers |
| Member | Access assigned resources only | Regular team members |
Role Hierarchy
| Role | Summary |
|---|---|
| Owner | Full tenant access, billing, destructive tenant actions |
| Admin | User, team, and resource management without owner-only billing actions |
| Member | Day-to-day work on assigned resources with limited admin access |
Permission Matrix
| Permission | Owner | Admin | Member |
|---|---|---|---|
| View Users | ✓ | ✓ | - |
| Create Users | ✓ | ✓ | - |
| Edit Users | ✓ | ✓ | - |
| Delete Users | ✓ | ✓ | - |
| View Teams | ✓ | ✓ | ✓ |
| Create Teams | ✓ | ✓ | - |
| Edit Teams | ✓ | ✓ | - |
| Delete Teams | ✓ | ✓ | - |
| View Projects | ✓ | ✓ | ✓ |
| Create Projects | ✓ | ✓ | - |
| Edit Projects | ✓ | ✓ | - |
| Delete Projects | ✓ | ✓ | - |
| View Tasks | ✓ | ✓ | ✓ |
| Create Tasks | ✓ | ✓ | ✓ |
| Edit Tasks | ✓ | ✓ | ✓ |
| Delete Tasks | ✓ | ✓ | - |
| View Settings | ✓ | ✓ | - |
| Edit Settings | ✓ | - | - |
| View Billing | ✓ | - | - |
| Manage Billing | ✓ | - | - |
Implementation
Permission Enum
Located at app/Domain/Authorization/Enums/TenantPermission.php:
enum TenantPermission: string
{
// User management
case VIEW_USERS = 'view users';
case CREATE_USERS = 'create users';
case EDIT_USERS = 'edit users';
case DELETE_USERS = 'delete users';
// Team management
case VIEW_TEAMS = 'view teams';
case CREATE_TEAMS = 'create teams';
case EDIT_TEAMS = 'edit teams';
case DELETE_TEAMS = 'delete teams';
// Project management
case VIEW_PROJECTS = 'view projects';
case CREATE_PROJECTS = 'create projects';
case EDIT_PROJECTS = 'edit projects';
case DELETE_PROJECTS = 'delete projects';
// Task management
case VIEW_TASKS = 'view tasks';
case CREATE_TASKS = 'create tasks';
case EDIT_TASKS = 'edit tasks';
case DELETE_TASKS = 'delete tasks';
// Settings
case VIEW_SETTINGS = 'view settings';
case EDIT_SETTINGS = 'edit settings';
// Billing
case VIEW_BILLING = 'view billing';
case MANAGE_BILLING = 'manage billing';
}
Role Enum
Located at app/Domain/Authorization/Enums/TenantRole.php:
enum TenantRole: string
{
case OWNER = 'owner';
case ADMIN = 'admin';
case MEMBER = 'member';
public function permissions(): array
{
return match ($this) {
self::OWNER => TenantPermission::cases(),
self::ADMIN => [
TenantPermission::VIEW_USERS,
TenantPermission::CREATE_USERS,
// ... admin permissions
],
self::MEMBER => [
TenantPermission::VIEW_TEAMS,
TenantPermission::VIEW_PROJECTS,
TenantPermission::VIEW_TASKS,
TenantPermission::CREATE_TASKS,
TenantPermission::EDIT_TASKS,
],
};
}
}
System Roles
System administrators have tenant_id = NULL and use different roles:
System Role Enum
Located at app/Domain/Authorization/Enums/SystemRole.php:
enum SystemRole: string
{
case SUPER_ADMIN = 'super-admin';
}
Creating System Admin
// Via artisan tinker
$admin = User::create([
'name' => 'System Admin',
'email' => '[email protected]',
'password' => bcrypt('secure-password'),
'tenant_id' => null, // System admin has no tenant
]);
$admin->assignRole(SystemRole::SUPER_ADMIN->value);
System Admin Access
System admins:
- Can access the Filament admin panel at
/admin - Can view and manage all tenants
- Cannot use tenant-scoped API endpoints
- Cannot be assigned tenant roles
Policies
Policy Structure
Policies provide defense-in-depth authorization:
// app/Policies/ProjectPolicy.php
final class ProjectPolicy
{
public function view(User $user, Project $project): bool
{
// Check 1: User has permission
// Check 2: Resource belongs to user's tenant
return $user->can(TenantPermission::VIEW_PROJECTS->value)
&& $this->belongsToUserTenant($user, $project);
}
public function update(User $user, Project $project): bool
{
return $user->can(TenantPermission::EDIT_PROJECTS->value)
&& $this->belongsToUserTenant($user, $project);
}
public function delete(User $user, Project $project): bool
{
return $user->can(TenantPermission::DELETE_PROJECTS->value)
&& $this->belongsToUserTenant($user, $project);
}
private function belongsToUserTenant(User $user, Project $project): bool
{
return $user->tenant_id === $project->tenant_id;
}
}
Registering Policies
In app/Providers/AuthServiceProvider.php:
protected $policies = [
Project::class => ProjectPolicy::class,
Task::class => TaskPolicy::class,
Tenant::class => TenantPolicy::class,
User::class => UserPolicy::class,
];
Usage Examples
In Controllers
class ProjectController extends Controller
{
public function index()
{
$this->authorize('viewAny', Project::class);
return Project::all();
}
public function show(Project $project)
{
$this->authorize('view', $project);
return $project;
}
public function store(StoreProjectRequest $request)
{
$this->authorize('create', Project::class);
return Project::create($request->validated());
}
public function update(UpdateProjectRequest $request, Project $project)
{
$this->authorize('update', $project);
$project->update($request->validated());
return $project;
}
public function destroy(Project $project)
{
$this->authorize('delete', $project);
$project->delete();
return response()->noContent();
}
}
In Blade Templates
@can('create', App\Domain\ExampleApp\Models\Project::class)
<a href="{{ route('projects.create') }}">Create Project</a>
@endcan
@can('update', $project)
<a href="{{ route('projects.edit', $project) }}">Edit</a>
@endcan
@can('delete', $project)
<form action="{{ route('projects.destroy', $project) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit">Delete</button>
</form>
@endcan
Permission Checks
// Using can() helper
if ($user->can('view projects')) {
// ...
}
// Using enum
if ($user->can(TenantPermission::VIEW_PROJECTS->value)) {
// ...
}
// Using hasPermissionTo()
if ($user->hasPermissionTo('edit projects')) {
// ...
}
// Check role
if ($user->hasRole(TenantRole::OWNER->value)) {
// ...
}
Multi-Tenant Team Scoping
How It Works
Spatie Permission's team feature is used for tenant isolation:
// In TenantMiddleware.php
app(PermissionRegistrar::class)->setPermissionsTeamId($tenant->id);
This ensures:
- Roles assigned in Tenant A are not visible in Tenant B
- Permission checks are scoped to current tenant
- Users can have different roles in different tenants
Configuration
In config/permission.php:
'teams' => true,
'team_foreign_key' => 'tenant_id',
Assigning Roles
// Assign role to user within tenant context
$user->assignRole(TenantRole::ADMIN->value);
// The role is automatically scoped to current tenant
// (via PermissionRegistrar::setPermissionsTeamId)
Adding New Permissions
Step 1: Add to Enum
// In TenantPermission.php
case VIEW_REPORTS = 'view reports';
case CREATE_REPORTS = 'create reports';
case EXPORT_REPORTS = 'export reports';
Step 2: Update Role Permissions
// In TenantRole.php
self::ADMIN => [
// existing permissions...
TenantPermission::VIEW_REPORTS,
TenantPermission::CREATE_REPORTS,
],
Step 3: Run Permission Seeder
php artisan db:seed --class=RoleAndPermissionSeeder
Step 4: Create Policy (if new resource)
// app/Policies/ReportPolicy.php
final class ReportPolicy
{
public function viewAny(User $user): bool
{
return $user->can(TenantPermission::VIEW_REPORTS->value);
}
public function create(User $user): bool
{
return $user->can(TenantPermission::CREATE_REPORTS->value);
}
public function export(User $user, Report $report): bool
{
return $user->can(TenantPermission::EXPORT_REPORTS->value)
&& $user->tenant_id === $report->tenant_id;
}
}
Filament Integration
Resource Authorization
Filament resources use policies automatically:
class ProjectResource extends Resource
{
// Policy methods are called automatically:
// - viewAny() for listing
// - create() for create button/page
// - update() for edit button/page
// - delete() for delete button
}
Custom Authorization
For custom actions:
Tables\Actions\Action::make('archive')
->action(fn (Project $record) => $record->archive())
->visible(fn (Project $record): bool =>
auth()->user()->can('archive', $record)
),
Navigation Visibility
public static function canViewNavigation(): bool
{
return auth()->user()->can('viewAny', Project::class);
}
Best Practices
1. Always Use Policies
// CORRECT: Use policy
$this->authorize('update', $project);
// WRONG: Manual check
if ($user->tenant_id !== $project->tenant_id) {
abort(403);
}
2. Use Enum Values
// CORRECT: Type-safe
$user->can(TenantPermission::VIEW_PROJECTS->value);
// AVOID: String literal
$user->can('view projects');
3. Defense in Depth
Always check both permission AND tenant ownership:
public function update(User $user, Project $project): bool
{
return $user->can(TenantPermission::EDIT_PROJECTS->value)
&& $user->tenant_id === $project->tenant_id; // Double check!
}
4. Fail Closed
When in doubt, deny access:
public function viewAny(User $user): bool
{
// Only allow if permission is explicitly granted
return $user->can(TenantPermission::VIEW_PROJECTS->value);
}
Testing Authorization
Testing Policies
public function test_user_cannot_edit_other_tenant_project(): void
{
$tenantA = Tenant::factory()->create();
$tenantB = Tenant::factory()->create();
$userA = User::factory()->for($tenantA)->create();
$projectB = Project::factory()->for($tenantB)->create();
$userA->assignRole(TenantRole::OWNER->value);
$this->assertFalse($userA->can('update', $projectB));
}
public function test_member_cannot_delete_projects(): void
{
$tenant = Tenant::factory()->create();
$user = User::factory()->for($tenant)->create();
$project = Project::factory()->for($tenant)->create();
$user->assignRole(TenantRole::MEMBER->value);
$this->assertFalse($user->can('delete', $project));
}
Testing Permissions
public function test_admin_has_expected_permissions(): void
{
$tenant = Tenant::factory()->create();
$user = User::factory()->for($tenant)->create();
$user->assignRole(TenantRole::ADMIN->value);
$this->assertTrue($user->can('view users'));
$this->assertTrue($user->can('create projects'));
$this->assertFalse($user->can('manage billing'));
}
Troubleshooting
Permission Not Working
-
Clear permission cache:
php artisan permission:cache-reset -
Verify role assignment:
dd($user->getRoleNames()); -
Check team context is set:
dd(app(PermissionRegistrar::class)->getPermissionsTeamId());
User Has Wrong Permissions
-
Check which role(s) are assigned:
$user->roles->pluck('name'); -
Check role's permissions:
$user->getAllPermissions()->pluck('name');