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
┌─────────────────────────────────────────────────┐
│ Authorization Layers │
├─────────────────────────────────────────────────┤
│ Layer 1: Authentication │
│ └── Is user logged in? │
│ │
│ Layer 2: Tenant Context │
│ └── Does user belong to this tenant? │
│ │
│ Layer 3: Role & Permission │
│ └── Does user have required permission? │
│ │
│ Layer 4: Policy │
│ └── Is user allowed to act on this resource? │
└─────────────────────────────────────────────────┘
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
Owner
├── All permissions
├── Billing management
└── Delete tenant
Admin
├── User management
├── Team management
├── Resource management
└── View settings
Member
├── View resources
├── Create/edit tasks
└── View teams
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');