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)

LayerWhat it gatesWhere configured
Session + Spatie / PermissionEnumAdmin UI and authenticated /ai/* HTTP (who can open screens / call AI routes)Roles UI + permissions:sync
collection_permissionsHeadless /api/v1 and /api/graphql collection CRUD; optional rules.fields / item_filter; admin item form field ACL when a grant existsRole Collection access matrix
file_permissionsHeadless /api/v1/files* visibility (read / read_private / create / update / delete)Role Files access matrix
Machine HMAC / project secretInbound 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 keysSeeded 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

SurfaceEntry / authPermission / ACLPrimary Pest
Admin UI (collections, files, roles, settings, …)Fortify session; permission:* middleware + FormRequestSpatie PermissionEnum via EffectivePermissionResolver; collection field ACL on item forms when grant presentCollectionAuthorizationTest, RolePermissionManagementTest, FileManagerTest, EffectivePermissionResolverTest, SuperAdminGateTest
Public CMS API /api/v1Anonymous → role public; or Authorization: Bearer ek_… → key’s roleResolveApiAccessCollectionPermissionEnforcer + FilePermissionGuardPublicCollectionApiTest, CollectionPermissionRulesTest, PublicFilesApiTest, PublicApiOriginGateTest
GraphQL /api/graphqlSame as REST (no session cookies)Same enforcer + throttle:apiGraphqlCollectionApiTest
AI HTTP /ai/* (except webhook)Session + can-use-aiSpatie can-use-ai; tools still call ChecksAiPermissionsAiPermissionMatrixTest, AiAssistantTest
AI tools (in-process)Authenticated AI userPer-tool Spatie / collection enforcer (ChecksAiPermissions, AiCollectionPermissionEnforcer)AiPermissionMatrixTest, AiCollectionPermissionEnforcerTest, AiToolActionsDbTest
AI inbound webhook POST /ai/webhooks/collection-importNo session; X-AI-Webhook-Signature HMACAI_WEBHOOK_TOKEN over rawBody + "\n" + collection_idAiPhaseThreeToFiveTest
Outbound webhooks (Externa → your URL)N/A (server push)Project webhook URL and signing secret required; empty secret refusedOutboundWebhookTest
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 /storagePrivateDiskFilesTest, 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

ConcernGate
Route middlewareSpatie permission: alias on routes/web.php, routes/collections.php, routes/settings.php, files routes
Form requestsAuthorizesWithPermissionEffectivePermissionResolver::hasPermission()
Super-adminGate::before bypasses Spatie checks
Collection item fieldsWhen the actor has a collection grant with rules.fields, admin form respects read/create/update flags (Collection items)
Jobs / project / appearancecan-show-jobs / can-manage-jobs; can-manage-project-settings

Product guide: Roles & permissions. Enum list: Effective permissions.

Public CMS API

ConcernGate
MiddlewareResolveApiAccess (anonymous → public role; Bearer → API key role)
Collections / itemsRole Collection access create/read/update/delete + optional field ACL / item_filter
CORS / OriginProject public_api_allowed_origins (and related env) — not a substitute for keys
PreviewAdmin 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

PathAuthZ
Authenticated /ai/*Session + Spatie can-use-ai
Tools (manage collections/files/users/…, query activity, …)ChecksAiPermissions::requirePermission() — HTTP gate is not enough
Collection-aware toolsAlso respect collection enforcer where applicable
Inbound collection-import webhookHMAC 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:

KindDirectionAuthZ
OutboundExterna → operator URLProject settings URL + signing secret; dispatcher and job refuse empty secret (#88)
AI inboundIntegrator → POST /ai/webhooks/collection-importHMAC with AI_WEBHOOK_TOKEN (#86)

Guides: Outbound webhooks · AI API — Webhook. Configure outbound under Project settings (can-manage-project-settings).

Files (private path)

SurfaceBehavior
StorageEffective-private files live on the private disk; public /storage/... URLs must not serve private bytes (#84)
Admin File ManagerSpatie 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
UploadsExtension 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)

ConcernLocation
Spatie middleware / FormRequestpermission: alias; AuthorizesWithPermission
Effective SpatieApp\Support\Authorization\EffectivePermissionResolver
API actor resolutionApp\Http\Middleware\ResolveApiAccess
Collection ACLApp\Services\Api\CollectionPermissionEnforcer
File ACLApp\Services\Api\FilePermissionGuard
AI tool gatesApp\Ai\Concerns\ChecksAiPermissions
AI inbound HMACApp\Http\Controllers\Ai\CollectionImportWebhookController
Outbound sign / refuse empty secretoutbound webhook dispatcher + DeliverOutboundWebhookJob

Operators checklist

  • [ ] Walk this matrix against staging before production (with Security checklist)
  • [ ] Confirm public role Collection + Files matrices match intended anonymous exposure
  • [ ] API keys use least-privilege roles (no accidental read_private)
  • [ ] AI_WEBHOOK_TOKEN set 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
Previous
Threat model & hosting