T
Tenanto
Documentation / Authorization

Authorization

Updated Jan 25, 2026

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:

┌─────────────────────────────────────────────────┐
│              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:


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:

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

  1. Clear permission cache:

    php artisan permission:cache-reset
    
  2. Verify role assignment:

    dd($user->getRoleNames());
    
  3. Check team context is set:

    dd(app(PermissionRegistrar::class)->getPermissionsTeamId());
    

User Has Wrong Permissions

  1. Check which role(s) are assigned:

    $user->roles->pluck('name');
    
  2. Check role's permissions:

    $user->getAllPermissions()->pluck('name');
    

Related Documentation