Skip to content

Tenant scoping and tenant switching

The logiccloud orchestrate API is multi-tenant. This page explains how a request selects its tenant, when a tenant switch is required, and how to read the audit trail. It applies to the web UI, the CLI, the SDKs and direct API calls. The server applies the same rules to all of them.

In the examples, $API is the base URL of your logiccloud orchestrate API, $TOKEN is a bearer token and <organization-id> is the UUID of an organization. Set $API once before you run them:

Terminal window
API=https://api.example.com
  • Every endpoint acts in exactly one tenant (organization), except the global operations listed below.
  • You select the tenant with the x-organization-id header.
  • A member of a tenant acts in it without a switch.
  • A superadmin or a user with the SUPPORT system role needs an open tenant switch to act in a tenant they are not a member of.
  • An API key is fixed to its own organization and never uses a switch.
  1. The API is tenant-scoped. Send the x-organization-id header with the organization UUID. The header selects the tenant. It grants nothing: the server still checks that you may act there. A malformed value returns 400. Without the header, the server uses your open switch, or else your primary organization. A route that names an organization in its path or body uses that organization, and a conflicting header returns 403. The tenant slug is a web UI concept. The API accepts only the organization id. The x-organization-source header grants nothing; a forged value never skips a check.
  2. Home tenants need no switch. A home tenant is any tenant you are a member of. This holds for superadmins and for regular users. POST /api/v1/tenant/switch to a home tenant creates no switch. It ends any open switch and returns 204.
  3. A foreign tenant needs an open switch. A superadmin, or a user with the SUPPORT system role, who acts in a tenant they are not a member of needs an open switch. Open it with POST /api/v1/tenant/switch and a reason of 3 to 500 characters. The response is 201 and the server writes an audit entry. Without a switch, the API returns 403 TENANT_SWITCH_REQUIRED. This applies to the web UI and to direct API calls in the same way.
  4. A switch has a limited lifetime. The default is 8 hours. You can set an expiresAt up to 72 hours from now when you open it. You can extend the switch with PATCH /api/v1/tenant/switch and a reason. The new expiry is at most 72 hours from now. Each extension writes an audit entry. A switch ends at its expiry, with DELETE /api/v1/tenant/switch, or when the user logs out of the web UI. A sweep runs every 5 minutes. It closes expired switches and writes the audit entry. An expired switch stops granting access at its expiry time.
  5. Login lands in the switch tenant. After login in the web UI, an open switch decides which tenant you land in. Without a switch, or with an expired one, you land in your primary tenant.
  6. Navigation by slug updates the switch. When you open a tenant by its slug in the web UI, the web UI updates your switch record. Every change appears in the audit log with an action and a trigger. Actions are switch, return and extend. The trigger of a return is user, home, replaced, logout, expired or revoked (the system role was revoked).
  7. API keys are fixed to one organization. A key never uses a switch, not even the switch of the user who created it. A header that names another organization returns 403. Every /api/v1/tenant/switch* endpoint returns 403 for an API key.

A few endpoints are not scoped to one tenant and need no x-organization-id:

  • the /api/v1/tenant/* context and switch endpoints,
  • the platform-level organization endpoints under /api/v1/organizations (superadmin),
  • the platform-wide activity and reporting endpoints under /api/v1/activity,
  • authentication event ingestion.

All other endpoints are tenant-scoped. Global operations never return TENANT_SWITCH_REQUIRED.

A superadmin or SUPPORT user who targets a tenant they are not a member of, without an open switch, receives this response:

{
"statusCode": 403,
"error": "Forbidden",
"code": "TENANT_SWITCH_REQUIRED",
"message": "A tenant switch is required to act in this organization",
"organizationId": "<organization-id>"
}

Open a switch, then repeat the request. Other tenant rejections use the standard error body with a message:

  • Not a member of this organization: a user without a system role targets a tenant they are not a member of.
  • Organization not found or Organization is inactive.
  • The x-organization-id header and the organization in the path differ.
  • The caller lacks the permission in the tenant.

Use case 1: a regular user with one tenant

Section titled “Use case 1: a regular user with one tenant”

A user without a system role has one tenant. The user never holds a switch. A switch to the own tenant returns 204 and creates no record. A switch into any tenant the user is not a member of returns 403, and no record is created.

A user with one tenant acts in it without a switch record single_tenant_user user User with one tenant api API call x-organization-id: own tenant user->api uses ok 200 no switch record api->ok member: allowed
Terminal window
curl -H "Authorization: Bearer $TOKEN" \
-H "x-organization-id: <organization-id>" \
"$API/api/v1/devices"

Use case 2: a user moves between their own tenants

Section titled “Use case 2: a user moves between their own tenants”

A user who is a member of tenants A and B moves between them with a home switch. The API answers 204 and creates no switch record. If a switch was open, the API ends it with the trigger home.

A switch to a home tenant returns 204, creates no record and ends an open switch home_switch user Member of tenant A and B post POST /api/v1/tenant/switch organizationId = B user->post acts in A res 204 no switch record post->res B is a home tenant open Open switch (if any) ends, trigger home post->open side effect
Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"organizationId": "<organization-id>"}' \
"$API/api/v1/tenant/switch"

Use case 3: a superadmin enters a customer tenant

Section titled “Use case 3: a superadmin enters a customer tenant”

A superadmin opens a switch with a reason. The API answers 201 and writes an audit entry. The superadmin then calls the API with the customer tenant in x-organization-id. The switch ends with DELETE /api/v1/tenant/switch, at logout from the web UI, or at expiry. The superadmin can extend the switch before it ends.

A superadmin opens a switch, works in the tenant, optionally extends it, and the switch ends superadmin_enters admin Superadmin not a member of tenant C open POST /api/v1/tenant/switch reason 3-500 characters admin->open sw 201 switch open, audited open->sw use API calls with x-organization-id: C sw->use allowed in C ext PATCH /api/v1/tenant/switch extend, audited use->ext optional end Switch ends audited use->end DELETE, logout (web UI) or expiry ext->use

Open a switch with the default lifetime of 8 hours:

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"organizationId": "<organization-id>", "reason": "Ticket 1234: investigate failed deployment"}' \
"$API/api/v1/tenant/switch"

Open a switch with a chosen expiry. expiresAt is at least 1 minute and at most 72 hours from now. Do not send duration together with expiresAt.

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"organizationId": "<organization-id>", "reason": "Planned maintenance", "expiresAt": "<ISO-8601-time>"}' \
"$API/api/v1/tenant/switch"

Work inside the tenant:

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
-H "x-organization-id: <organization-id>" \
"$API/api/v1/devices"

Extend the open switch. The API returns 404 when no switch is open and 409 when the switch has already expired:

Terminal window
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"reason": "Work takes longer than planned", "expiresAt": "<ISO-8601-time>"}' \
"$API/api/v1/tenant/switch"

End the switch. The API returns 400 when no switch is open:

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" "$API/api/v1/tenant/switch"

A switch applies to the requests of its own user only. A new switch into another tenant replaces the open one, so a user has at most one open switch.

Use case 4: a superadmin calls a tenant without a switch

Section titled “Use case 4: a superadmin calls a tenant without a switch”

A superadmin sends a request for a tenant they are not a member of, and no switch is open. The API rejects the request with the 403 body shown above. This is the same for the web UI and for direct API calls. A hidden button is not access control: the server decides.

A request without a switch returns 403 TENANT_SWITCH_REQUIRED, and a switch fixes it switch_required admin Superadmin no open switch call GET /api/v1/devices x-organization-id: C admin->call rej 403 TENANT_SWITCH_REQUIRED with organizationId call->rej not a member fix Open a switch then repeat the call rej->fix fix
Terminal window
curl -i -H "Authorization: Bearer $TOKEN" \
-H "x-organization-id: <organization-id>" \
"$API/api/v1/devices"

The same rule holds for a user with the SUPPORT system role.

Use case 5: login with an open or an expired switch

Section titled “Use case 5: login with an open or an expired switch”

After login, the web UI asks the API for the tenant context. An open switch decides the tenant you land in. Without a switch, you land in your primary tenant. A switch that has expired is closed with an audit entry (trigger expired), and you land in your primary tenant.

After login an open switch decides the landing tenant, otherwise the primary tenant login_landing login User logs in to the web UI q Open switch? login->q sw Lands in the switch tenant q->sw yes, not expired pm Lands in the primary tenant q->pm no ex Expired switch: closed, audited expired q->ex yes, expired ex->pm

An API key is fixed to the organization it was issued for. The key works with no x-organization-id header or with its own organization id. A header that names another organization returns 403. The key never uses a switch, not even the switch of its creator. All /api/v1/tenant/switch* endpoints return 403 for a key. The same holds for the other /api/v1/tenant context endpoints.

An API key stays in its organization, another organization or a switch endpoint returns 403 api_key_scope key CLI or SDK with X-API-Key own Key organization 200 key->own no header or own id other Header names another organization key->other sw /api/v1/tenant/switch* key->sw r403a 403 other->r403a r403b 403 sw->r403b
Terminal window
curl -H "X-API-Key: $API_KEY" "$API/api/v1/devices"
curl -i -H "X-API-Key: $API_KEY" \
-H "x-organization-id: <other-organization-id>" \
"$API/api/v1/devices"

The second call returns 403.

Use case 7: read the audit trail for tenant switches

Section titled “Use case 7: read the audit trail for tenant switches”

Every switch event writes one audit entry with the entity type tenant-switch. The metadata of an entry holds the action (switch, return or extend), the reason, the tenant names and, for a return, the trigger. The entry for a closed switch is filed under the tenant the switch entered. The audit endpoints are tenant-scoped, so a superadmin needs an open switch to read the audit log of a tenant they are not a member of. The caller needs the audit read permission.

Switch events become audit entries that you list, read per switch or export audit_trail event Switch event switch, return, extend entry Audit entry entityType tenant-switch event->entry written once search GET /api/v1/audit ?entityType=tenant-switch entry->search list one GET /api/v1/audit/entity/ tenant-switch/{id} entry->one one switch csv GET /api/v1/audit/export ?entityType=tenant-switch entry->csv CSV

List all tenant switch entries of a tenant:

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
-H "x-organization-id: <organization-id>" \
"$API/api/v1/audit?entityType=tenant-switch"

Read the entries of one switch. <switch-id> is the id returned when you opened the switch:

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
-H "x-organization-id: <organization-id>" \
"$API/api/v1/audit/entity/tenant-switch/<switch-id>"

Export the entries as CSV. The last two columns are Reason and Metadata:

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
-H "x-organization-id: <organization-id>" \
"$API/api/v1/audit/export?entityType=tenant-switch"

Two more endpoints help with switches. GET /api/v1/tenant/switch/history lists your own switches. GET /api/v1/tenant/switches/active lists all open switches (superadmin and SUPPORT only). See Audit Logs for the web UI.

  • Logout ends a switch only through the web UI. The web UI calls DELETE /api/v1/tenant/switch?trigger=logout when you sign out. A CLI or SDK client does not sign out through the web UI. For these clients the expiry is the backstop. End the switch yourself with DELETE /api/v1/tenant/switch when your work is done.
  • A WebSocket stays with the tenant it was opened for. A realtime connection that you opened before a switch ended keeps streaming events of its tenant until it reconnects. Reconnect after you end or change a switch.
  • The organization_id claim of the access token can be stale. The token keeps the organization list it had at sign-in, and a token refresh does not reorder it. The web UI uses the claim only for a call without a tenant slug. The server checks your membership or your open switch on every call, so a stale claim never grants access to a tenant.
  • A custom expiry and an extension allow at most 72 hours from now. A later time returns 400. Extend again before the switch ends.
  • A reason has 3 to 500 characters after trimming. A shorter or longer reason returns 400.