# Effective permissions

Source: https://docs.qiick.io/externa/permissions

Externa authorizes with **session auth (Fortify)**, **Spatie roles/permissions**, and a custom **effective permission** layer that unions direct roles with roles inherited from user groups. Super-admins bypass ability checks via `Gate::before`.

> **Two permission layers**
> | Layer                                             | What it gates                                                                        | Storage                                                                                                                            |
> | ------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
> | **Spatie / `PermissionEnum`**                     | Admin UI capability (who can open collections, files, roles, AI, …)                  | Spatie `roles` / `permissions` + `EffectivePermissionResolver`                                                                     |
> | **`collection_permissions` / `file_permissions`** | Headless `/api/v1` (and field ACL / `item_filter` / file visibility on that surface) | Role × collection × action (+ optional `rules`), and role × file action (`create` / `read` / `read_private` / `update` / `delete`) |
> 
> Spatie does **not** replace Collection access. A user with `can-show-collections` still needs the role’s **Collection access** matrix (and optional field/`item_filter` rules) for Public CMS API and for enforcer-aware surfaces. **`read_private`** unlocks effective-private files on `/api/v1/files*` — see [Public CMS API](https://docs.qiick.io/externa/public-cms-api#public-vs-private-files) and [Roles & permissions](https://docs.qiick.io/externa/roles-and-permissions#files-access-public-cms-api).

## Session authentication (Fortify)

| Item        | Detail                                                                                                                           |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Package     | `laravel/fortify`                                                                                                                |
| Guard       | `web` (session) → `App\Models\User`                                                                                              |
| Provider    | `App\Providers\FortifyServiceProvider`                                                                                           |
| Username    | `email` (`lowercase_usernames`)                                                                                                  |
| Home        | `/dashboard`                                                                                                                     |
| Features    | Registration, password reset, email verification, two-factor auth (confirm + confirm password), **passkeys** (`confirmPassword`) |
| Actions     | `CreateNewUser`, `ResetUserPassword`                                                                                             |
| Views       | Inertia pages under `resources/js/pages/auth/*`                                                                                  |
| Rate limits | `login`, `two-factor`, `passkeys`                                                                                                |

User model traits include Spatie `HasRoles`, Fortify `TwoFactorAuthenticatable` + `PasskeyAuthenticatable` (`PasskeyUser`), and soft deletes. Passkeys guide: [Passkeys](https://docs.qiick.io/externa/passkeys).

## Spatie Permission

- Package: `spatie/laravel-permission`
- Config: `config/permission.php`
- Canonical permission **names** live in `App\Enums\PermissionEnum`
- Sync command: `php artisan permissions:sync` (`SyncPermissionsCommand`)
  - Upserts DB permissions from the enum
  - Regenerates `resources/js/enums/permission-enum.ts`
- Seeders: `PermissionSeeder` runs sync; `RoleSeeder` creates built-in roles and `syncPermissions`

Admin FormRequests typically call `AuthorizesWithPermission::authorizePermission()`, which delegates to `EffectivePermissionResolver::hasPermission()`.

## PermissionEnum

All values (kebab-case strings):

| Area                | Permissions                                                                                                                                                                                                                                               |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Dashboard           | `can-show-dashboard`                                                                                                                                                                                                                                      |
| Users               | `can-show-users`, `can-create-users`, `can-edit-users`, `can-delete-users`, `can-restore-users`, `can-force-delete-users`                                                                                                                                 |
| Groups              | `can-show-groups`, `can-create-groups`, `can-edit-groups`, `can-delete-groups`, `can-restore-groups`, `can-force-delete-groups`                                                                                                                           |
| Roles               | `can-show-roles`, `can-create-roles`, `can-edit-roles`, `can-delete-roles`                                                                                                                                                                                |
| Permissions         | `can-show-permissions`, `can-create-permissions`, `can-edit-permissions`, `can-delete-permissions`                                                                                                                                                        |
| Files               | `can-show-files`, `can-create-files`, `can-edit-files`, `can-delete-files`, `can-restore-files`, `can-force-delete-files`, `can-download-files`, `can-favorite-files`, `can-copy-files`, `can-replace-files`, `can-tag-files`, `can-update-file-metadata` |
| Collections         | `can-show-collections`, `can-create-collections`, `can-edit-collections`, `can-delete-collections`, `can-restore-collections`, `can-force-delete-collections`                                                                                             |
| Activity            | `can-show-activity-logs`                                                                                                                                                                                                                                  |
| Chat hub            | `can-show-chat` (item drawer still uses collection item read ACL)                                                                                                                                                                                         |
| Jobs                | `can-show-jobs`, `can-manage-jobs`                                                                                                                                                                                                                        |
| AI                  | `can-use-ai`                                                                                                                                                                                                                                              |
| Project             | `can-manage-project-settings`                                                                                                                                                                                                                             |
| API keys (admin UI) | `can-show-api-keys`, `can-manage-api-keys`                                                                                                                                                                                                                |

Source: `app/Enums/PermissionEnum.php`. Helper: `PermissionEnum::values()`.

> **Public CMS API is separate**
> Spatie permissions above gate the **admin UI**. Headless `/api/v1` access uses `collection_permissions` (role × collection × create/read/update/delete, optional field ACL / `item_filter`) and `file_permissions` (including `read_private`). See [Public CMS API](https://docs.qiick.io/externa/public-cms-api) and the callout at the top of this page.

## RoleEnum

Built-in roles (`app/Enums/RoleEnum.php`), seeded in `RoleSeeder`:

| Role        | Value         | Seeded permissions                                                     |
| ----------- | ------------- | ---------------------------------------------------------------------- |
| Super admin | `super-admin` | All permissions in the DB for the guard                                |
| Admin       | `admin`       | All permissions (same set as super-admin at seed time)                 |
| Reader      | `reader`      | Only permissions whose name starts with `can-show-`                    |
| Public      | `public`      | **None** (system, not assignable) — anonymous actor for `/api/v1` only |

Reader show set: every permission whose name starts with `can-show-` (dashboard, users, groups, roles, permissions, files, collections, activity logs, api keys, **jobs**, …). Reader does **not** get `can-use-ai`, `can-manage-project-settings`, or mutating file/collection permissions.

The **`public`** role cannot be assigned to users/groups, renamed, or deleted. Configure its **Collection access** matrix for anonymous API traffic.

> **Super-admin vs admin**
> Both roles may hold the same permission rows after seeding. Runtime difference: **super-admin** also bypasses Laravel Gates via `Gate::before` and is treated as all-powerful in `EffectivePermissionResolver` / `useCan`. **Admin** must actually possess each permission through roles/groups.

## EffectivePermissionResolver

Class: `App\Services\Authorization\EffectivePermissionResolver` (singleton in `AppServiceProvider`).

### API

| Method                                          | Returns                                        |
| ----------------------------------------------- | ---------------------------------------------- |
| `permissionsFor(User $user)`                    | Effective permission name list                 |
| `hasPermission(User $user, string $permission)` | Boolean                                        |
| `roleNamesFor(User $user)`                      | Effective role names                           |
| `effectiveRoleIds(User $user)`                  | Effective role ids (direct ∪ via groups)       |
| `isSuperAdmin(User $user)`                      | Whether `super-admin` is among effective roles |
| `forget(?User $user = null)`                    | Clear per-request cache                        |

### Resolution algorithm

1. **Role names / ids** = unique union of:
   - Direct: `$user->roles`
   - Via groups: roles attached to each of the user’s groups
2. If `super-admin` ∈ role names → permissions = full `PermissionEnum::values()`
3. Else permissions = unique union of:
   - Spatie `$user->getAllPermissions()` (direct + via assigned roles)
   - Permissions from roles attached through the user’s groups (group → role → permission)
4. Results are cached for the request keyed by user id

`hasPermission` returns `true` immediately for super-admins; otherwise checks membership in `permissionsFor`.

> **Groups scale roles, not a separate ACL matrix**
> Attach Spatie roles to a group → members inherit **admin Spatie permissions** and any **`collection_permissions` / `file_permissions`** configured on those roles. There is no group-level collection ACL table. Admin field/`item_filter` rules (`CollectionPermissionGuard::rulesForUser`) use `effectiveRoleIds()`, the same set as Spatie effective roles. Headless `/api/v1` still authenticates as a single role (public or API key).

`HomePath` (post-login redirect) also uses `EffectivePermissionResolver::hasPermission`, so group-inherited `can-show-*` permissions affect where users land after login.

## Gate::before

Configured in `AppServiceProvider::configureAuthorization()`:

```php
Gate::before(function (?User $user): ?bool {
    if ($user === null) {
        return null;
    }

    if (app(EffectivePermissionResolver::class)->isSuperAdmin($user)) {
        return true;
    }

    return null;
});
```

Super-admins therefore pass any `Gate` / `authorize()` ability check, including abilities that are not registered. Covered by `tests/Feature/Authorization/SuperAdminGateTest.php`.

## Middleware

Aliases in `bootstrap/app.php`:

| Alias              | Class                     | Behavior                                                                                     |
| ------------------ | ------------------------- | -------------------------------------------------------------------------------------------- |
| `permission`       | `EnsureUserHasPermission` | `permission:{name}` — 403 unless `EffectivePermissionResolver::hasPermission`                |
| `can.manage.files` | `EnsureCanManageFiles`    | Maps route name / HTTP method / bulk `action` to a file `PermissionEnum`, then same resolver |

These are **custom** aliases (not Spatie’s default `role` / `permission` middleware package aliases).

### Where they are applied

**Admin** (`routes/admin.php`): individual resources use `->middleware('permission:…')` with the matching `PermissionEnum` value. All `/files/*` routes sit inside `Route::middleware('can.manage.files')`.

**AI** (`routes/ai.php`): authenticated AI group uses `auth` + `verified` + `permission:can-use-ai` (some import status routes add further checks). Per-tool Spatie checks via `ChecksAiPermissions`; item tools also apply `CollectionPermissionEnforcer` (field ACL / `item_filter`).

**Team chat** (`routes/chat.php`): `auth` + `verified` + `permission:can-show-chat`. Item-form aliases stay on collection item read ACL — [Chat (hub & items)](https://docs.qiick.io/externa/chat).

**Collections** (`routes/collections.php`): `auth` + `verified` plus Spatie `permission:` per action (same pattern as admin). See [Collections API](https://docs.qiick.io/externa/collections-api).

## Frontend: `useCan` and `auth.permissions`

Shared by `HandleInertiaRequests`:

```php
'auth' => [
    'user' => $user,
    'permissions' => $user ? $permissionResolver->permissionsFor($user) : [],
    'roleNames' => $user ? $permissionResolver->roleNamesFor($user) : [],
    'isSuperAdmin' => $user ? $permissionResolver->isSuperAdmin($user) : false,
],
```

Hook: `resources/js/hooks/use-can.ts`

```ts
const { can, auth } = useCan()
if (can(PermissionEnum.CanEditCollections)) {
  // show edit UI
}
```

Helper `userHasPermission` (`resources/js/lib/permissions.ts`):

1. No user → `false`
2. `auth.isSuperAdmin` → `true`
3. Else `auth.permissions.includes(permission)`

Used by sidebar nav filtering, collections destructive actions, AI FAB visibility, admin screens, and similar UI gates.

## Enforcement matrix

| Surface                    | Server middleware                              | Additional enforcement                                                      |
| -------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------- |
| Admin CRUD                 | `permission:` per route                        | FormRequests + Gates                                                        |
| Files                      | `can.manage.files`                             | Action → permission mapping                                                 |
| AI chat / tools            | `permission:can-use-ai`                        | Per-tool `requirePermission` + `CollectionPermissionEnforcer` on item tools |
| Team chat hub (`/chat/*`)  | `permission:can-show-chat`                     | Item aliases use collection item read ACL — [Chat](https://docs.qiick.io/externa/chat)           |
| Collections HTTP (session) | `auth` + `verified` + `permission:` per action | FormRequests; items also use `CollectionPermissionEnforcer`                 |
| Public CMS API `/api/v1`   | `ResolveApiAccess`                             | `collection_permissions` / `file_permissions` via guards + enforcer         |
| Settings / dashboard       | `auth` (+ verified where applied)              | Feature-specific                                                            |
| Project / appearance       | `permission:can-manage-project-settings`       | [Project settings](https://docs.qiick.io/externa/project-settings)                               |
| Jobs monitor               | `permission:can-show-jobs` / `can-manage-jobs` | [Operations](https://docs.qiick.io/externa/operations#jobs-monitor-admin-ui)                     |

## Related pages

- [Public CMS API](https://docs.qiick.io/externa/public-cms-api) — headless `/api/v1`, public role, API keys
- [Roles & permissions](https://docs.qiick.io/externa/roles-and-permissions) — product feature guide
- [Architecture](https://docs.qiick.io/externa/architecture) — shared props and middleware stack
- [AI assistant model](https://docs.qiick.io/externa/ai-assistant) — tool-level permission gates
- [Admin & files API](https://docs.qiick.io/externa/admin-api) — route-level permission strings
