Operations
AuthZ map
One navigable map of how Externa authorizes each surface: admin UI, Public CMS API, GraphQL, AI, webhooks, and private files. Detail guides stay where they are — this page ties routes to gates and primary Pest coverage.
Layers (read this first)
| Layer | What it gates | Where configured |
|---|---|---|
Session + Spatie / PermissionEnum | Admin UI and authenticated /ai/* HTTP (who can open screens / call AI routes) | Roles UI + permissions:sync |
collection_permissions | Headless /api/v1 and /api/graphql collection CRUD; optional rules.fields / item_filter; admin item form field ACL when a grant exists | Role Collection access matrix |
file_permissions | Headless /api/v1/files* visibility (read / read_private / create / update / delete) | Role Files access matrix |
| Machine HMAC / project secret | Inbound AI collection-import webhook; outbound webhook signing (not Spatie) | AI_WEBHOOK_TOKEN; project webhook URL + secret |
Gate::before (super-admin) | Bypasses Spatie ability checks in admin; not available via API keys | Seeded super-admin role |
Spatie alone does not unlock Public CMS API data. A user with can-show-collections still needs Collection access (and Files access for private bytes) on the headless surface. Full resolution: Effective permissions.
Master matrix
| Surface | Entry / auth | Permission / ACL | Primary Pest |
|---|---|---|---|
| Admin UI (collections, files, roles, settings, …) | Fortify session; permission:* middleware + FormRequest | Spatie PermissionEnum via EffectivePermissionResolver; collection field ACL on item forms when grant present | CollectionAuthorizationTest, RolePermissionManagementTest, FileManagerTest, EffectivePermissionResolverTest, SuperAdminGateTest |
Public CMS API /api/v1 | Anonymous → role public; or Authorization: Bearer ek_… → key’s role | ResolveApiAccess → CollectionPermissionEnforcer + FilePermissionGuard | PublicCollectionApiTest, CollectionPermissionRulesTest, PublicFilesApiTest, PublicApiOriginGateTest |
GraphQL /api/graphql | Same as REST (no session cookies) | Same enforcer + throttle:api | GraphqlCollectionApiTest |
AI HTTP /ai/* (except webhook) | Session + can-use-ai | Spatie can-use-ai; tools still call ChecksAiPermissions | AiPermissionMatrixTest, AiAssistantTest |
| AI tools (in-process) | Authenticated AI user | Per-tool Spatie / collection enforcer (ChecksAiPermissions, AiCollectionPermissionEnforcer) | AiPermissionMatrixTest, AiCollectionPermissionEnforcerTest, AiToolActionsDbTest |
AI inbound webhook POST /ai/webhooks/collection-import | No session; X-AI-Webhook-Signature HMAC | AI_WEBHOOK_TOKEN over rawBody + "\n" + collection_id | AiPhaseThreeToFiveTest |
| Outbound webhooks (Externa → your URL) | N/A (server push) | Project webhook URL and signing secret required; empty secret refused | OutboundWebhookTest |
| Private files (disk + Public API) | Admin Spatie for File Manager; API key / public for /api/v1/files* | Admin: can-*-files + private disk; API: read vs read_private; private bytes not on public /storage | PrivateDiskFilesTest, PublicFilesApiTest |
Run a focused suite:
herd php artisan test --filter='CollectionAuthorization|CollectionPermissionRules|Graphql|PublicFiles|PublicCollection|PrivateDisk|OutboundWebhook|AiPermissionMatrix|AiCollectionPermission|AiPhaseThree|EffectivePermission|SuperAdminGate|RolePermission'
Admin UI
| Concern | Gate |
|---|---|
| Route middleware | Spatie permission: alias on routes/web.php, routes/collections.php, routes/settings.php, files routes |
| Form requests | AuthorizesWithPermission → EffectivePermissionResolver::hasPermission() |
| Super-admin | Gate::before bypasses Spatie checks |
| Collection item fields | When the actor has a collection grant with rules.fields, admin form respects read/create/update flags (Collection items) |
| Jobs / project / appearance | can-show-jobs / can-manage-jobs; can-manage-project-settings |
Product guide: Roles & permissions. Enum list: Effective permissions.
Public CMS API
| Concern | Gate |
|---|---|
| Middleware | ResolveApiAccess (anonymous → public role; Bearer → API key role) |
| Collections / items | Role Collection access create/read/update/delete + optional field ACL / item_filter |
| CORS / Origin | Project public_api_allowed_origins (and related env) — not a substitute for keys |
| Preview | Admin Preview as role uses the same enforcer payload shape |
Guides: Public CMS API · Roles — Collection access.
GraphQL
Same auth and collection/file guards as REST. Endpoint /api/graphql only; middleware ResolveApiAccess + throttle:api. No session cookies; no super-admin via API keys.
Guide: GraphQL. Pest: GraphqlCollectionApiTest.
AI
| Path | AuthZ |
|---|---|
Authenticated /ai/* | Session + Spatie can-use-ai |
| Tools (manage collections/files/users/…, query activity, …) | ChecksAiPermissions::requirePermission() — HTTP gate is not enough |
| Collection-aware tools | Also respect collection enforcer where applicable |
| Inbound collection-import webhook | HMAC only (X-AI-Webhook-Signature); not Bearer; not Spatie |
Guides: AI API · AI assistant model. Pest: AiPermissionMatrixTest, AiCollectionPermissionEnforcerTest, AiPhaseThreeToFiveTest.
Webhooks
Two different directions — do not conflate:
| Kind | Direction | AuthZ |
|---|---|---|
| Outbound | Externa → operator URL | Project settings URL + signing secret; dispatcher and job refuse empty secret (#88) |
| AI inbound | Integrator → POST /ai/webhooks/collection-import | HMAC with AI_WEBHOOK_TOKEN (#86) |
Guides: Outbound webhooks · AI API — Webhook. Configure outbound under Project settings (can-manage-project-settings).
Files (private path)
| Surface | Behavior |
|---|---|
| Storage | Effective-private files live on the private disk; public /storage/... URLs must not serve private bytes (#84) |
| Admin File Manager | Spatie can-*-files (show/create/edit/delete/download/…) |
Public CMS API /api/v1/files* | Role Files access: read for public files; read_private unlocks effective-private get/content/transform; without it → 403 / omit / access: denied on expands |
| Uploads | Extension denylist (ForbiddenUploadExtension); SVG/HTML blocked (#85) |
Guides: Public CMS API — public vs private files · Roles — Files access · File manager. Pest: PrivateDiskFilesTest, PublicFilesApiTest.
Source map (code)
| Concern | Location |
|---|---|
| Spatie middleware / FormRequest | permission: alias; AuthorizesWithPermission |
| Effective Spatie | App\Support\Authorization\EffectivePermissionResolver |
| API actor resolution | App\Http\Middleware\ResolveApiAccess |
| Collection ACL | App\Services\Api\CollectionPermissionEnforcer |
| File ACL | App\Services\Api\FilePermissionGuard |
| AI tool gates | App\Ai\Concerns\ChecksAiPermissions |
| AI inbound HMAC | App\Http\Controllers\Ai\CollectionImportWebhookController |
| Outbound sign / refuse empty secret | outbound webhook dispatcher + DeliverOutboundWebhookJob |
Operators checklist
- [ ] Walk this matrix against staging before production (with Security checklist)
- [ ] Confirm
publicrole Collection + Files matrices match intended anonymous exposure - [ ] API keys use least-privilege roles (no accidental
read_private) - [ ]
AI_WEBHOOK_TOKENset only when the inbound webhook is used - [ ] Outbound webhook: both URL and secret, or leave URL empty
- [ ] Private disk: smoke that public URLs cannot fetch private bytes