Tenant Administration
Tenant Administration
Section titled “Tenant Administration”Day-to-day management of tenants in Mantis.
Overview
Section titled “Overview”Tenant administration covers ongoing management tasks:
Viewing Tenants
Section titled “Viewing Tenants”List All Tenants
Section titled “List All Tenants”curl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/tenants"Response:
{ "data": [ { "id": "019b937d-4862-84ae-9c2a-6ac230cc4c7e", "name": "Acme Corp", "description": "Enterprise customer", "contact_email": "ops@acme.com", "created_at": "2024-01-15T10:30:00Z" }, { "id": "019b937d-4862-84ae-9c2a-6ac230cc4c80", "name": "Globex Inc", "description": "SMB customer", "contact_email": "admin@globex.com", "created_at": "2024-01-20T14:00:00Z" } ], "meta": { "total": 2, "page": 1, "per_page": 20 }}Get Tenant Details
Section titled “Get Tenant Details”curl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID"Response includes relationships:
{ "id": "019b937d-4862-84ae-9c2a-6ac230cc4c7e", "name": "Acme Corp", "description": "Enterprise customer", "contact_email": "ops@acme.com", "logo_url": "https://acme.com/logo.png", "tags": [ { "id": "019b937d-bad5-8cd6-a67d-db03dd598065", "key": "region", "value": "us-east" }, { "id": "019b937d-b403-85af-a687-afc4320b48cb", "key": "tier", "value": "enterprise" } ], "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-02-01T09:15:00Z"}Updating Tenants
Section titled “Updating Tenants”Update Tenant Details
Section titled “Update Tenant Details”curl -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Corporation", "description": "Updated enterprise customer description", "contact_email": "newops@acme.com" }' \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID"Update Fields
Section titled “Update Fields”| Field | Update Behavior |
|---|---|
name | Must remain unique |
description | Set to null to clear |
contact_email | Set to null to clear |
logo_url | Set to null to clear |
Via Lens UI
Section titled “Via Lens UI”- Navigate to Tenancy → Tenants
- Click on the tenant name
- Click Edit
- Update fields
- Click Save Changes
Managing Entitlements
Section titled “Managing Entitlements”View Current Entitlements
Section titled “View Current Entitlements”# List solutionscurl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID/solutions"
# List targetscurl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID/targets"
# List environmentscurl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID/environments"Add Solution Entitlement
Section titled “Add Solution Entitlement”curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "solution_id": "019b937d-33f5-8f92-ae77-b58f3bea15c4", "environment_id": "019b937d-739d-8cd8-a8af-636d98857494" }' \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID/solutions"Remove Solution Entitlement
Section titled “Remove Solution Entitlement”curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID/solutions/$SOLUTION_ID/environments/$ENV_ID"Entitlement Matrix
Section titled “Entitlement Matrix”Document tenant entitlements:
| Tenant | Solution | Production | Staging | Development |
|---|---|---|---|---|
| Acme Corp | WebApp | ✓ | ✓ | ✗ |
| Acme Corp | API | ✓ | ✓ | ✗ |
| Globex Inc | WebApp | ✓ | ✗ | ✗ |
Managing Variables
Section titled “Managing Variables”Tenant variables live in variable sets. A variable takes its tenant from the set it
is created in, recorded as owner_tenant_id; scope_tenant_id is a separate condition
scope that no request field sets. There is no /tenants/{id}/variables endpoint.
# List variables in a setcurl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/variable-sets/$VARIABLE_SET_ID/variables"
# Update onecurl -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"value": "https://api-v2.acme.com"}' \ "https://api.mantis.local/api/v1/variable-sets/$VARIABLE_SET_ID/variables/$VARIABLE_ID"
# Delete onecurl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/variable-sets/$VARIABLE_SET_ID/variables/$VARIABLE_ID"Rotating a secret is an update with variable_type: "sensitive" — there is no
is_encrypted field on the request. Reads of a sensitive variable return
"********" in value, so the plaintext never leaves the database.
Viewing Deployments
Section titled “Viewing Deployments”List Tenant Deployments
Section titled “List Tenant Deployments”curl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID/deployments"Response:
{ "data": [ { "id": "019b937d-4862-8de0-1111-222233334444", "source_type": "solution", "solution_name": "Solution 019b937d-4862-8de0-1111-222233334444", "state": "success", "progress": 1.0, "tenant_name": "Acme Corp", "environment_name": "Production", "created_at": "2024-02-01T10:00:00Z", "completed_at": "2024-02-01T10:05:32Z", "duration_ms": 332000 } ], "meta": { "total": 150, "page": 1, "per_page": 20 }}Paginate Deployments
Section titled “Paginate Deployments”# Paginationcurl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID/deployments?page=2&per_page=50"State-based filtering is not currently supported on this endpoint. Use the SQL monitoring queries below for status-filtered analysis.
Deployment Statistics
Section titled “Deployment Statistics”Track tenant health with deployment metrics:
SELECT DATE_TRUNC('day', created_at) AS day, COUNT(*) AS total_deployments, SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS successful, SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed, AVG(EXTRACT(EPOCH FROM (completed_at - started_at))) AS avg_duration_secsFROM deployment_historyWHERE tenant_id = $tenant_idAND created_at >= NOW() - INTERVAL '30 days'GROUP BY DATE_TRUNC('day', created_at)ORDER BY day DESC;User Management
Section titled “User Management”List Tenant Users
Section titled “List Tenant Users”curl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/admin/users"System admins see all users; a tenant-scoped caller sees users in their tenant plus tenant-less (global) users (tenant_id IS NULL), resolved by effective_tenant_scope. There is an admin-only tenant_id query parameter to narrow the listing; it is ignored for tenant-scoped callers.
Add User to Tenant
Section titled “Add User to Tenant”The body is deny_unknown_fields and declares only email, username,
display_name, role_ids and tenant_id. There is no password: the account is
created in an invited state and Mandible emails an activation link. role_ids are
role UUIDs.
curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "username": "bob", "email": "bob@acme.com", "tenant_id": "019b937d-4862-84ae-9c2a-6ac230cc4c7e", "role_ids": ["019b93a2-1f04-8c7e-b6d1-2a5f0c83e714"] }' \ "https://api.mantis.local/api/v1/admin/users"To set a password directly instead of using the emailed invitation, post to
/api/v1/admin/users/{id}/password with {"new_password": "..."} — see
Creating a Tenant.
Move User Between Tenants
Section titled “Move User Between Tenants”curl -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tenant_id": "019b937d-1f00-88a6-ab60-42b775a77452" }' \ "https://api.mantis.local/api/v1/admin/users/$USER_ID"Remove Tenant User
Section titled “Remove Tenant User”Disable rather than delete for audit retention:
curl -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "inactive"}' \ "https://api.mantis.local/api/v1/admin/users/$USER_ID"Audit Review
Section titled “Audit Review”View Tenant Audit Logs
Section titled “View Tenant Audit Logs”Audit logs are read from /api/v1/audit/logs. A tenant-scoped caller sees their
tenant’s entries and none of the platform’s (tenant_id IS NULL); an admin
with the admin role and no tenant sees everything. An admin-only tenant_id
query parameter narrows the listing (it is ignored for tenant-scoped callers).
A tenant’s entries are the ones about what it owns, whoever acted: a platform administrator’s change to one of its resources, what a Thorax instance reports about its deployments, and a sign-in refused for one of its accounts, API keys or identity providers. A sign-in attempt that names no known account is the platform’s.
curl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/audit/logs"Filter by Category
Section titled “Filter by Category”# Authentication eventscurl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/audit/logs?category=authentication"
# Deployment eventscurl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/audit/logs?category=deployment"
# Security eventscurl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/audit/logs?severity=security"Export Audit Logs
Section titled “Export Audit Logs”audit_log_entries enforces row-level security, so the scope must be declared
or the export silently produces an empty file. Use \copy
from psql — server-side COPY TO <file> needs superuser or pg_write_server_files.
SET mantis.audit_tenant_id = '<tenant-uuid>';
\copy ( SELECT occurred_at, event_type, event_category, severity, actor_id, action, outcome, metadata FROM audit_log_entries WHERE tenant_id = '<tenant-uuid>' AND occurred_at >= '2024-01-01' AND occurred_at < '2024-02-01' ORDER BY occurred_at) TO 'audit_export.csv' WITH CSV HEADERTenant Lifecycle
Section titled “Tenant Lifecycle”Onboarding Checklist
Section titled “Onboarding Checklist”□ Create tenant□ Configure description and contact□ Attach required solutions to environments□ Assign targets□ Set up variables (including encrypted secrets)□ Add tags for organization□ Create initial users□ Configure SSO (if applicable)□ Document tenant configuration□ Notify tenant of accessDeactivation
Section titled “Deactivation”When a tenant needs to be temporarily disabled:
# Remove all solution entitlementsfor SOL_ENV in $(get_tenant_solutions); do curl -X DELETE "$API_URL/tenants/$TENANT_ID/solutions/$SOL_ENV"done
# Disable all tenant users (valid statuses: active, inactive, locked,# pending_verification, pending_approval)for USER_ID in $(get_tenant_users); do curl -X PUT -d '{"status": "inactive"}' \ "https://api.mantis.local/api/v1/admin/users/$USER_ID"doneReactivation
Section titled “Reactivation”# Re-enable usersfor USER_ID in $(get_tenant_users); do curl -X PUT -d '{"status": "active"}' \ "https://api.mantis.local/api/v1/admin/users/$USER_ID"done
# Restore entitlementscurl -X POST -d '{"solution_id": "019b937d-0299-8bb4-aafc-7415e8e3e979", "environment_id": "019b937d-7c74-8ad6-a9cc-215477a41a2f"}' \ "$API_URL/tenants/$TENANT_ID/solutions"Moving Definitions Between Tenants
Section titled “Moving Definitions Between Tenants”A solution, sequence, action or environment a tenant owns can be handed to another tenant, or made global.
curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"kind": "sequence", "definition_id": "'"$SEQUENCE_ID"'", "destination_tenant_id": "'"$DEST_TENANT_ID"'"}' \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID/definitions/move"kind is solution, sequence, action or environment. A destination_tenant_id of
null, or none at all, makes the definition global: readable by every tenant.
What Moves With It
Section titled “What Moves With It”A move carries what the definition is built from. Only definitions the source tenant owns are carried; a global action a sequence attaches stays global.
| Moved | Carries |
|---|---|
| Solution | The sequences its versions publish, and their actions |
| Sequence | The actions its versions attach |
| Action | Nothing |
| Environment | Nothing |
The response lists every definition whose owner changed, deepest first:
{ "moved": [ { "kind": "action", "id": "01a03…", "name": "Restart web" }, { "kind": "sequence", "id": "01a03…", "name": "Deploy web" } ]}Entitlements
Section titled “Entitlements”An entitlement names a solution and an environment. The source tenant’s entitlements for a moved solution or environment go to the destination, so the new owner can deploy what it received, except that one is dropped:
- when its other half belongs to a tenant other than the destination. That is the source tenant when the other half stayed behind: moving an environment drops the source tenant’s entitlements pairing it with the source’s own solutions, and moving a solution drops those pairing it with the source’s own environments. It is a third tenant when the entitlement paired the moved definition with that tenant’s solution or environment, which the destination was never given. A global other half is carried.
- when the destination already holds the same pair. The destination keeps its own.
Entitlements other tenants hold are left alone. A move to global changes no entitlements: a tenant deploys a global definition by holding an entitlement for it, so the source tenant keeps the ones it has.
Refusals
Section titled “Refusals”The move answers 409 and changes nothing when:
-
A carried definition is also used by something staying behind: a sequence that a solution outside the move publishes, or an action that a sequence outside the move attaches. The message names the definition and what uses it.
-
A carried definition is managed by a config repository. The message names the repository; move the definition in Git instead.
-
Live configuration the source tenant holds still points at what is moving: an enabled deployment schedule naming a moved solution, sequence, action or environment; an enabled deployment freeze naming a moved solution or environment (counted while it is enabled, in its window or not); or a promotion lifecycle that governs a moved solution. Only the source tenant’s rows refuse — one another tenant holds keeps resolving the definition after the move — and none is asked about for a move to global. Disable or remove it, then retry.
-
Work is in flight on what is moving, whoever is running it:
- a promotion lifecycle with a rule promoting from or to a moved environment;
- a pending or approved promotion request for a moved solution or into a moved environment;
- a deployment still running a moved solution, sequence or action (its failure handler included) or into a moved environment, or a dispatch queued for a moved environment;
- a command that has not finished with a moved action.
Finish or cancel it, then retry.
-
For a move to another tenant, configuration held by a tenant other than the destination still names what is moving: a variable scoped to a moved solution, environment or action version; a variable set linked to a moved solution; an environment or tenant target assignment, or an active registration token, naming a moved environment; or a test gate on a carried definition or run by a carried action. Platform-wide configuration does not refuse, and none of this is asked for a move to global, which every tenant can still reach. Remove the configuration, move, and recreate what the destination needs.
-
A carried definition left the source tenant, or a new version began using one the move did not carry, while the move was being checked. Retry.
It answers 404 for a definition the source tenant does not own or a source tenant that does not exist, and 400 for a destination that is the source tenant itself or does not exist. The destination is checked before the definition and before any refusal.
Permissions
Section titled “Permissions”The caller needs tenants:manage_solutions in the source tenant and in the destination
tenant. Holding it in one does not imply the other: a move writes to the source and grants
reach in the destination. Making a definition global needs the permission platform-wide.
Each move is recorded on the audit chain of the source tenant and of the destination, listing everything that moved; for a move to global the destination chain is the platform’s.
Via Lens UI
Section titled “Via Lens UI”The detail page of a tenant-owned solution, sequence, action or environment has a Move
to tenant button. It opens a dialog that says what moves with the definition and offers
every other tenant the user can see as the destination, plus Global for a platform
administrator. After the move it lists every definition whose owner changed; a refusal is
shown in the dialog and nothing moves. The button is absent for a global or Git-managed
definition, for a user without tenants:manage_solutions, and when there is nowhere to
move it: a tenant-scoped user sees only its own tenant, so moving between tenants is a
platform administrator’s task.
Deleting Tenants
Section titled “Deleting Tenants”Prerequisites
Section titled “Prerequisites”Before deletion:
- Export data - Audit logs, deployment history
- Notify stakeholders - Confirm deletion is authorized
- Disable users first - Prevent new activity
- Document decision - For compliance
Deletion Impact
Section titled “Deletion Impact”| Resource | On Tenant Delete |
|---|---|
| Solutions, sequences, actions, environments | Cascade deleted |
| Solution-Environment links | Cascade deleted |
| Target assignments | Cascade deleted |
| Tenant tags | Cascade deleted |
| Audit logs | Preserved |
| Deployment history | Preserved (tenant set to NULL) |
By default the definitions the tenant owns are destroyed with it. A solution,
sequence, action or environment whose tenant_id is this tenant goes with it; globally
shared definitions (tenant_id NULL) and definitions owned by other tenants are
untouched.
The request body can move definitions out first instead:
| Body | Effect |
|---|---|
None, or {} | Every definition the tenant owns is destroyed |
{"move_all_to": "<tenant id>"} | Every definition and every variable the tenant owns goes to that tenant |
{"move_definitions": [...]} | The definitions named go where each entry says; the rest are destroyed |
move_all_to and move_definitions are alternatives; sending both is a 400. Each
move_definitions entry is {"kind", "definition_id", "destination_tenant_id"}, and a
named definition carries what it is built from, as in a
move. move_definitions does not carry variables, so a tenant that
owns any is refused until they are moved or deleted. Neither form can make a definition
global, and the tenant being deleted cannot be a destination.
A move during a delete follows the rules of the move endpoint:
the same refusals, the same entitlement handling, and tenants:manage_solutions in the
tenant being deleted and in every destination, on top of tenants:delete. A definition
staying in the tenant being deleted does not count as staying behind, and the
outside-tenant configuration checks do not count what it holds — those rows go with the
tenant or stop its deletion themselves. Live state it holds still refuses: a schedule,
freeze or lifecycle in its name has to go before the tenant can be deleted in any case.
A refused move fails the whole delete, a named definition the tenant does not own answers
404, and a destination that does not exist answers 400, whether or not the tenant owns
anything to send there.
The response lists how many of each kind were destroyed and every definition and variable that moved; the same are recorded on the audit entry:
{ "id": "01a03…", "name": "acme", "deleted_definitions": [ { "kind": "solution", "count": 12 }, { "kind": "action", "count": 47 } ], "moved_definitions": [ { "kind": "environment", "id": "01a03…", "name": "production" } ]}Deletion is refused (409) while the tenant still owns any user, agent registration
or agent session. Those columns are ON DELETE SET NULL, so a surviving row would
silently become a global one; the API names what is left rather than letting that happen.
Registration tokens are not among them: registration_tokens.tenant_id is
ON DELETE CASCADE, so they are destroyed with the tenant and stop enrolling agents at
once. The delete response counts them.
Deletion is also blocked (409) while a child row still holds the tenant under
ON DELETE RESTRICT or NO ACTION. Those are: variable_sets, variables (by either
owner_tenant_id or scope_tenant_id), deployment_schedules, deployment_freezes,
promotion_lifecycles, promotion_requests, api_keys, environment_states,
environment_state_history, storages_local, storages_git, storages_git_auth, and
storages_s3. Remove or reassign these before deleting the tenant.
A variable the tenant owns answers with its own message: delete it, or pass move_all_to
to re-own the tenant’s variables to another tenant as part of the delete.
notification_events and deployment_history do not block. Both are records rather
than owned resources, so their tenant_id is ON DELETE SET NULL.
Delete Tenant
Section titled “Delete Tenant”curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID"
# Move everything the tenant owns to another tenant, then delete itcurl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"move_all_to": "'"$DEST_TENANT_ID"'"}' \ "https://api.mantis.local/api/v1/tenants/$TENANT_ID"Via Lens UI
Section titled “Via Lens UI”Delete on the tenants list and on a tenant’s page opens a dialog that asks what becomes
of the tenant’s definitions: Delete them with the tenant, or Move them to another
tenant, which sends everything to the tenant chosen there (move_all_to) and is offered
to a user holding tenants:manage_solutions. Naming definitions one by one is available
through the API only. A refused delete is shown in
the dialog and nothing is deleted.
Post-Deletion Cleanup
Section titled “Post-Deletion Cleanup”There are no orphaned users to clean up, because the API refuses to delete a tenant that still has any. Reassign or remove its users, agent registrations and agent sessions first; the 409 names what is left.
The users.tenant_id foreign key is ON DELETE SET NULL, so a tenant row deleted
directly in SQL would clear it and silently turn those users into tenant-less
(platform-scope) accounts. That is what the API check exists to prevent — do not
work around it by deleting the row by hand.
Monitoring and Alerts
Section titled “Monitoring and Alerts”Tenant Health Metrics
Section titled “Tenant Health Metrics”Monitor key metrics per tenant:
| Metric | Alert Threshold | Action |
|---|---|---|
| Failed deployments | >3 in 1 hour | Investigate failures |
| Inactive users | >30 days | Review user access |
| Variable changes | Unexpected | Review audit logs |
| Target disconnections | >1 target | Check target health |
SQL Monitoring Queries
Section titled “SQL Monitoring Queries”Failed Deployments by Tenant:
SELECT t.name AS tenant, COUNT(*) AS failed_countFROM deployment_history dhJOIN tenants t ON t.id = dh.tenant_idWHERE dh.status = 'failed'AND dh.created_at >= NOW() - INTERVAL '24 hours'GROUP BY t.nameORDER BY failed_count DESC;Active Users per Tenant:
SELECT t.name AS tenant, COUNT(*) AS active_users, MAX(u.last_login_at) AS latest_loginFROM users uJOIN tenants t ON t.id = u.tenant_idWHERE u.status = 'active'GROUP BY t.name;Bulk Operations
Section titled “Bulk Operations”Batch Update Variables
Section titled “Batch Update Variables”#!/bin/bash# Update a variable across all tenants
VARIABLE_NAME="api_version"NEW_VALUE="v2"
# The variable set carries the tenant, so iterate the per-tenant sets rather than# passing a tenant on the body: the request is `deny_unknown_fields` and declares# no `key`, `scope_tenant_id` or `is_encrypted`.## Updating means PUT on the variable's own id. POST to the collection CREATES, and a# set that already holds `api_version` answers 409 — a loop of POSTs updates nothing and# reports "Updated" for every one of them. Look the id up first; create it only where# the set does not have it yet.for VARIABLE_SET_ID in "${TENANT_VARIABLE_SETS[@]}"; do VARIABLE_ID=$(curl -sf \ -H "Authorization: Bearer $TOKEN" \ "$API_URL/variable-sets/$VARIABLE_SET_ID/variables" \ | jq -r --arg n "$VARIABLE_NAME" '.data[] | select(.name == $n) | .id')
if [ -n "$VARIABLE_ID" ]; then curl -sf -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"value\": \"$NEW_VALUE\"}" \ "$API_URL/variable-sets/$VARIABLE_SET_ID/variables/$VARIABLE_ID" \ && echo "Updated $VARIABLE_SET_ID" else curl -sf -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"$VARIABLE_NAME\", \"value\": \"$NEW_VALUE\", \"variable_type\": \"string\" }" \ "$API_URL/variable-sets/$VARIABLE_SET_ID/variables" \ && echo "Created in $VARIABLE_SET_ID" fidoneBatch Grant Entitlements
Section titled “Batch Grant Entitlements”#!/bin/bash# Grant new solution to all enterprise tenants
SOLUTION_ID="sol_new_feature"ENVIRONMENT_ID="env_production"
# Get tenants with the enterprise tag. NOTE: the LIST response rows carry no# `tags` field (tags appear only on the per-tenant detail response), so filter# tags client-side by fetching each tenant's detail, or filter on a field the# list actually returns.ENTERPRISE_TENANTS=$(curl -s "$API_URL/tenants" | jq -r '.data[].id' | while read id; do curl -s "$API_URL/tenants/$id" | jq -r 'select(.tags[]?.value == "enterprise") | .id'done)
for TENANT_ID in $ENTERPRISE_TENANTS; do curl -s -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{ \"solution_id\": \"$SOLUTION_ID\", \"environment_id\": \"$ENVIRONMENT_ID\" }" \ "$API_URL/tenants/$TENANT_ID/solutions" echo "Granted to $TENANT_ID"doneTroubleshooting
Section titled “Troubleshooting”Tenant Cannot Deploy
Section titled “Tenant Cannot Deploy”-
Check solution entitlement:
Terminal window curl "$API_URL/tenants/$TENANT_ID/solutions" -
Check target assignment:
Terminal window curl "$API_URL/tenants/$TENANT_ID/targets" -
Verify variables: there is no tenant-variables endpoint (see Managing Variables). Read the tenant’s variable sets and the variables in each:
Terminal window curl "$API_URL/variable-sets?tenant_id=$TENANT_ID"curl "$API_URL/variable-sets/$VARIABLE_SET_ID/variables"
User Cannot Access Tenant
Section titled “User Cannot Access Tenant”-
Verify user’s tenant_id:
SELECT tenant_id FROM users WHERE id = $user_id; -
Check user status:
SELECT status FROM users WHERE id = $user_id; -
Verify tenant exists:
SELECT id, name FROM tenants WHERE id = $tenant_id;
Next Steps
Section titled “Next Steps”- Overview - Multi-tenancy concepts
- Data Isolation - Security model
- Users - User management
- Audit Logs - Audit configuration
