ZuloOne
English
Sign in
Menu

Auth and API gates

Updated

What a caller may do, and what the gates refuse. The office keeps the JWT in HttpOnly cookies; mobile and agents still send Authorization: Bearer from the same JSON body.

← Developer wiki

Production posture#

ZuloOne:CoreDevelopmentMode=false turns on the fallback policy: every endpoint needs an authenticated user unless it is [AllowAnonymous] (login, setup, health, branding, agent enroll, workspace zip with a ticket).

RequirePermission and RequireDeveloperOnMutation skip while CoreDevMode started enabled, so the test rig stays open.

Closed#

  • /api/data/{table} refuses EF-owned tables (MetaUsers, MetaSessions, …) with 404. Same for export. A name that is not a dictionary still has no dictionary matrix (documents, registers) — that is not a platform table.
  • Agent / job admin. AgentsController and JobsController require Admin. An agent or run JWT may only call /api/agent/* and /hubs/agent.
  • Metadata writes. Designer controllers take RequireDeveloperOnMutation (GET stays open for the office). Compile requires Developer.
  • Script Enforce. Type.GetType, Activator.CreateInstance, MethodInfo.Invoke, and System.Reflection are flagged the same way as Process / File.
  • Agent invoke. AgentRuntimeInvoker is an allowlist (platform data services including IMetadataService, plus ZuloOne.Services.Contracts.*). Not permissions, schema, SQL, or wipe. Same ScriptServices.EnsureAllowed deny-list as a task script. The run JWT is bound to the enroll session of that agent: assemblies / invoke / complete require X-ZuloOne-Agent-Session. A stolen run token without that machine's session is 401.
  • Sessions. Refresh is SHA-256 in MetaSessions.SessionToken (current[:previous]). Replay of the previous hash revokes the user's whole family. POST /api/auth/logout ends the presented refresh; password change / admin lock / deactivate do the same.
  • Office cookies. Login / refresh / first-run setup set zuloone.access (path /) and zuloone.refresh (path /api/auth), both HttpOnly, SameSite=Lax. The SPA sends credentials: include and X-ZuloOne-Client: office. A cookie-authenticated POST/PUT/PATCH/DELETE without that header is 403 (a cross-site form can send Lax cookies, not a custom header). Authorization: Bearer still wins and skips the CSRF gate — mobile, agents, and the test suite keep the JSON token. The office never writes the JWT to localStorage.
  • First-run setup. POST /api/auth/setup runs in a Serializable transaction; a concurrent second admin is DbUpdateException → already completed.
  • Passwords. VerifyPassword is BCrypt only. A leftover Base64 hash does not authenticate.
  • Attachments. List / upload / download / delete resolve the owner row through IDataService.GetByIdAsync (RLS). Platform tables 404.
  • Designer writes (second pass). Print forms, EDTs, resources, analytics, layers, total drivers, virtual totals, widget assignments/placements, report views, guides, localization, metadata registries — mutations need Developer. Custom report run stays a business POST; create/update/delete do not. Personal form layouts stay the caller's.
  • Bulk data. /api/data-transfer and /api/data-packages need Developer (apply/recreate wipe and restock tables). /api/email/queue needs Admin (bodies and addresses).
  • Agent SQL. ISqlService is not on the invoke allowlist. In-process scripts still have it; a run token cannot ExecuteAsync arbitrary SQL.
  • Print RLS. BuildForFormAsync checks the owner row before a data script runs. Hidden record → 404, not a rendered page.
  • Zip ticket. Payload is rid|exp|jti. The first full GET spends jti. A Range resume (bytes=N-, N > 0) works for 2 minutes after that spend. A second full GET is 401.
  • OData catalog. /odata and /$metadata list only entities the caller may Read. Collection GET already had ACL + RLS.
  • Hubs. Client SendMessage / SendNotificationToUser do not broadcast. Record/table subscribe require a visible business row. Studio and metadata groups need Developer.
  • Agent enroll. POST /api/agent/session uses the same lockout counters as login (agent:{name}), without captcha — agents cannot solve Turnstile.

Left open on purpose#

  • Empty object-permission matrix is still public for business objects (PermissionPolicy.AllowOnEmptyMatrix). Changing that locks unfinished tenants.
  • /health still reports developerTools / workspaceAgent — the SPA reads them on the login screen.

Trap#

localhost and 127.0.0.1 are different cookie hosts. The Vite origin in Cors:AllowedOrigins must be the same host name the browser uses, or the office cookie is set and never sent.

Where next#

Was this page helpful?