Skip to content

Security

theGreenGuy edited this page Jun 14, 2026 · 6 revisions

Security

Edge authentication + per-endpoint authorization, toggleable so the stack runs locally without a realm (build.md §4.8, §12).

Authentication — gateway JWT

With openwcs.security.enabled=true, the gateway is an OAuth2 resource server: it validates the JWT against the Keycloak realm, requires auth on /api/**, and forwards the identity downstream as X-Auth-User / X-Auth-Roles. It always strips client-supplied versions of those headers (anti-spoofing) — the gateway is the trust boundary. Off by default, all traffic is permitted so the stack runs without tokens.

This path is exercised end-to-end in CI (GatewayAuthEndToEndTest) against a live Keycloak Testcontainer that imports the openwcs realm: no token → 401, a realm JWT → 200 with the identity propagated, and a client-supplied X-Auth-* header is stripped.

Authorization — per-endpoint RBAC

  • libs/common carries a code-defined Permission catalog and a RoleCatalog (ADMIN/SUPERVISOR/OPERATOR/VIEWER) mirroring the IAM seed.
  • Each service enforces a coded Permission against the forwarded X-Auth-Roles, gated by openwcs.security.enabled (no-op when off; 403 problem+json on a missing permission when on). order-management uses an AccessGuard; other services use an RbacFilter.
  • Inter-service identity propagation: services forward X-Auth-* on outbound calls (e.g. allocation → inventory), so downstream RBAC authorizes against the original user.

Screen access (off / read / write)

Separate from the coded Permission RBAC above, each screen in the SPA has a configurable access level set on the Access control screen (Administration). The level is one of:

  • Off — the screen is hidden (not in the nav, route blocked).
  • Read — the screen opens view-only; create/edit/delete/save controls are hidden or disabled.
  • Write — full access.

It is configured per role (a 3-way Off/Read/Write control) and per individual user (a Read/Write level on the allow-list). A user's effective level is the strongest of their per-user entry and any of their roles; ADMIN always has write and can never lock itself out. The UI catalog (ui/src/auth/screens.ts) holds each screen's built-in default level per role — VIEWER and the read-only Reporting section default to read, everything else to write — which applies when a screen has no override.

Storage + API: iam persists only the overrides (screen_access + screen_access_role / screen_access_user, each with an access_level of READ/WRITE; absence = OFF). GET/PUT /api/iam/screen-access read/replace the full map ({ screenKey: { roles: {role: level}, users: {user: level} } }); GET /api/iam/screen-access/me returns the caller's effective levels.

Gateway enforcement. READ is not just a UI nicety: the gateway rejects a non-admin write (POST/PUT/PATCH/DELETE) to a screen-owned API path with 403 unless the user's effective level is WRITE. ScreenWriteCatalog maps the cleanly-owned write surfaces (master-data CRUD, counting, slotting, /api/flow/*/topology, warehouse-access, screen-access) and mirrors each screen's default levels; ScreenAccessResolver fetches the override map from iam's network-only /internal/screen-access (cached ~30 s). Reads always pass, writes to unmapped paths pass (fail-open), an IAM blip skips the check (fail-open), and admins bypass. Orders (shared /api/orders), settings (writes fan across many services) and Keycloak-admin user management are intentionally unmapped and rely on the UI's write-gating; gateway coverage extends incrementally.

The database console (/api/master-data/admin/db/**) is an access-required route: it is reachable on any method only if the caller has at least READ on the admin-database screen (the console is SELECT-only, so READ is enough). The console's controller no longer hard-requires ADMIN — an admin can grant the screen to another role or user via Access control and they can then run read-only queries, while the SELECT-only validator + read-only transaction still bound what runs.

IAM service

iam (port 8087) owns the authorization model: users → roles → coded permissions, with effective-permission resolution (union across roles). Keycloak does authentication; IAM layers RBAC on top.

Self-service password change at login

A user can set a new password from the login screen without an admin, and crucially rescue an account that "is not fully set up". A temporary or forced password adds Keycloak's UPDATE_PASSWORD required action, so the password grant returns invalid_grant ("Account is not fully set up") and the user can never obtain a token to change the password in the app. The login screen therefore offers a Change password form, and switches to it automatically when a sign-in reports that state, prefilling the entered password as the current one.

  • Endpoint: POST /api/iam/change-password — the one unauthenticated /api/** route (explicitly permitted on the gateway). Identity is proven by the current password: KeycloakClient.verifyPassword does a direct grant and treats both a 2xx response and the "not fully set up" error as proof the credential is correct.
  • Setting the new password is done via the Keycloak admin API using a dedicated confidential service-account client openwcs-iam (least privilege: manage-users/view-users/query-users, secret in OPENWCS_IAM_CLIENT_SECRET). The new password is set permanent and the required actions are cleared.
  • Rules: new password ≥ 8 characters, must differ from the current one, username non-blank. A wrong current password or a missing user both return a generic 401 (no account enumeration).
  • Footgun fix: the admin Set password dialog now defaults Temporary off (still available), so admins do not create the locked state by accident.

Warehouse access (per-user scope)

Each user is mapped to the warehouses they may work in and one default (iam.user_warehouse; a partial-unique index enforces at-most-one default per user). Endpoints under /api/iam/warehouse-access:

  • GET /me — the signed-in user's allowed warehouses + default (the UI's top-bar switcher auto-selects the default on login and scopes every warehouse-related screen; users never type a UUID).
  • GET / PUT /{username} — list/replace a user's mapping. ADMIN-only, enforced server-side on X-Auth-Roles (not just in the UI). The default must be one of the allowed warehouses.

A network-only /internal/warehouse-access/{username} (mapped off /api/**, so unreachable through nginx or the gateway's public routes) lets the gateway resolve a user's warehouses.

Gateway enforcement. For non-admins the gateway resolves the allowed set from IAM (short-TTL cache; fails open if IAM is unavailable), forwards it downstream as X-Auth-Warehouses, and rejects with 403 any request that names a warehouseId (query parameter, or the /warehouses/{id} path) outside that set. Admins are never warehouse-scoped. Writes that carry the warehouse only in a JSON body are guarded per-endpoint downstream via the forwarded header (follow-up).

Keycloak realm

Docker Compose imports platform/keycloak/openwcs-realm.json — realm openwcs with roles ADMIN/SUPERVISOR/OPERATOR/VIEWER, the public openwcs-web client (direct-access grants on for dev), the confidential openwcs-iam service-account client (manage-users, used for self-service password change), and demo users (dev-only passwords). Reached at http://localhost:8180.

Enabling it

Set OPENWCS_SECURITY_ENABLED=true and point the gateway at the realm, e.g. SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://localhost:8180/realms/openwcs.

Not yet: mTLS between services (internal trust currently rides on forwarded headers behind the edge); custom (non-seed) IAM roles would need a runtime IAM lookup.

Clone this wiki locally