Authorization
The lab is the reference deployment for NeOps authorization: the engine enforces, every caller carries a token, and the whole model is declared in one committed file.
The three modes
The engine reads NEOPS_AUTHZ_MODE, and rejects anything outside these three values at startup:
| Mode | What the engine does |
|---|---|
enforce |
Verifies the bearer token on every gated route; answers 401 without one and 403 without the route’s permission. |
permissive |
Requires a valid Neops JWT; a token reporting authz_enforced: false is allowed through, with a warning. Against a CMS running the permissions plugin it denies like enforce. |
disabled |
Treats every caller as anonymous and gates nothing. |
docker-compose.yml sets enforce. A lab that gates nothing would prove nothing about the deployments it stands in for, and the failure modes that matter — a missing grant, an expired token, a CORS origin nobody enumerated — only appear under enforcement.
Two more variables travel with it on the workflow_engine service:
NEOPS_JWT_PUBLIC_KEY_PATH— the SPKI PEM the engine verifies RS256 signatures against.make lab-jwtmintscms/jwt/{private,public}.pemfor the CMS; the public half is mounted into the engine at/etc/neops/jwt/public.pem.NEOPS_CORS_ORIGINS— the exact browser origins allowed to call the engine: the web client on:8080and the monitor app on:3031.
Two identities
The automation identity is neops, holding the role lab-admin. Every host script and the lab_bootstrap container acts as it, and so does the engine: make local-env-init mints a CMS API key for neops into cms_api_key.env, which the engine reads as NEOPS_CMS_TOKEN.
lab-admin holds the admin profile for two reasons
The host scripts publish definitions and start executions, and the CMS’s
unlockResources mutation verifies that the NEOPS_CMS_TOKEN caller holds
workflow-execution:write. That second coupling is tracked as
neops-workflow-engine#234.
The three personas exist to make the model visible in the UI. Each holds one role, and each password equals the username:
| Login | Role | Profile | Can |
|---|---|---|---|
author / author |
workflow-author |
author |
read and write workflow definitions |
operator / operator |
workflow-operator |
operator |
read definitions, run and abort executions |
admin / admin |
workflow-admin |
admin |
everything above, plus delete definitions and roll executions back |
All four roles carry the same CMS visibility (default_permission 7 and a RoleScope on Global), so the only difference between the personas is their workflow authority. Each profile’s contents come from the CMS: apply_cms_config prints the exact flags per element as grant_workflow_permissions applies them.
author and operator are partial profiles, so those two personas meet a 403 in the monitor app. The monitor answers one at the request that met it: the panel or the affordance reports the permission it wanted, and the rest of the page keeps working. Signing a persona out takes a request with no valid token, so even the narrowest profile reaches a usable app.
Where the grants are declared
cms/permissions.json:
{
"roles": {
"lab-admin": "admin",
"workflow-author": "author",
"workflow-operator": "operator",
"workflow-admin": "admin"
},
"users": {
"neops": ["lab-admin"],
"author": ["workflow-author"],
"operator": ["workflow-operator"],
"admin": ["workflow-admin"]
}
}
apply_cms_config reads it, creates each role and user, and then runs the CMS’s own manage.py grant_workflow_permissions --role <role> --profile <profile> --yes once per role, printing the diff it applies. That command is the single mechanism: the script writes no grant row itself.
Default profiles
Each profile is a set of <element>:<flag> grants:
| Profile | Grants |
|---|---|
author |
workflow:read, workflow:write |
operator |
workflow:read, workflow-execution:read, workflow-execution:write, workflow-execution-abort:write, worker:read |
admin |
author and operator together, plus workflow-delete:write, workflow-execution-rollback:write, worker:write |
Profiles are additive, and with NEOPS_PERMISSIONS_SIMPLE_IMPLICIT_PERMISSION_LEVEL on, which is the permissions plugin’s default and what this lab runs, the CMS widens each element as it stores it: a write grant is kept as read, execute and write, so an execute flag stands beside every write in the diff grant_workflow_permissions prints.
The table’s home is the CMS: manage.py grant_workflow_permissions declares it, and neops-core’s docs/runbooks/workflow-authorization-rollout.md documents it for a deployment that never runs this lab. It is repeated here so the persona table above can be read in one place.
To change the model, edit the file and re-apply:
make local-lab-up and make local-lab-discover apply it too, before they mint their token, so an edit reaches the engine on the next lab target.
For a one-off grant outside the file:
Grants only widen
grant_workflow_permissions ORs a profile’s flags into whatever the role
already holds. Moving a role from admin to operator in the file and
re-applying reports “Nothing to change” and leaves the admin flags in
place. To take a flag away, clear the checkbox in the web client’s rights
management screen or delete the RolePermissionElement row in the CMS
admin; the lab has no command for it.
The published CMS image has no grant_workflow_permissions
apply_cms_config checks for the command and prints a one-line message when
it is absent, so make local-env-init completes against
quay.io/zebbra/neops-cms-free:develop. The lab then runs with roles, users
and scopes but no workflow grants — see Running branch images.
How the monitor app gets a token
The monitor app at http://localhost:3031 holds no session of its own. The web client embeds it in an iframe (FRONTEND_WORKFLOW_MANAGER_URL) and relays its access token over postMessage; the monitor keeps it in memory only.
The relay’s trust anchor is window.__NEOPS_CONFIG__.webclientOrigin, which the lab supplies through monitor/config.js, bind-mounted over rest/monitor-app/static/config.js. Setting WEBCLIENT_ORIGIN on the service has no effect here: that variable is read by an nginx entrypoint, and this service runs the engine image with npm run dev. See invariant 16.
Opening http://localhost:3031 directly gives an unauthenticated monitor, because no web client is there to relay from. Reach it through the web client’s workflow-monitor page.
Tokens expire, and logins are rate-limited
Access tokens last 15 minutes (NEOPS_JWT_ACCESS_LIFETIME_MINUTES, a CMS default this lab keeps). A token carries the grants its account held at the moment it was issued, and the engine reads them from the token alone, so a grant added or withdrawn afterwards reaches the engine at the next login and no sooner. make local-lab-discover polls to a 900-second ceiling, so a discovery minted at the start and still running at the end outlives its token; run_workflow reports that and stops polling. The execution itself continues — watch it in the monitor.
The CMS rate-limits local logins to 5 per minute per client IP (NEOPS_LOCAL_LOGIN_RATE_LIMIT), and every lab call leaves the host from the same address. A make target mints a token and passes it to every caller in it. local-lab-up mints a second one after containerlab deploy, whose minutes can outlast the first, so a full local-lab-up plus local-lab-discover cycle is three logins. Re-running a target several times inside a minute is what reaches the limit; ./lab_token names the setting when it does.
Mint a token by hand with:
export NEOPS_ENGINE_TOKEN=$(./lab_token) # as neops
export NEOPS_ENGINE_TOKEN=$(./lab_token --username operator)
curl -H "Authorization: Bearer $NEOPS_ENGINE_TOKEN" http://localhost:3030/workflow-execution
Running branch images
Enforcement needs a CMS carrying grant_workflow_permissions, an engine carrying the auth module, and a web client carrying the token broker. Until those merge and CI republishes, build each from its authorization branch and point the lab at it:
export NEOPS_CMS_IMAGE=neops-cms-free:latest
export NEOPS_WORKFLOW_ENGINE_IMAGE=neops-workflow-engine:latest
export NEOPS_WEB_CLIENT_IMAGE=neops-web-client:latest
make local-env-prune && make local-env-init
Against the published images the lab still comes up: the engine ignores the three authorization variables, the public-key mount goes unread, a bearer header on an ungated route is harmless, and apply_cms_config skips the grant step. See Images.
