Skip to content

Tenant Isolation

Configure multi-tenant access control and data isolation in Mantis.

Mantis supports multi-tenancy where resources can be scoped to specific tenants, providing logical separation between different teams, customers, or environments.

FieldTypeDescription
idUUIDUnique identifier
nameStringTenant name (unique)
descriptionStringOptional description
logo_urlStringOptional logo URL
contact_emailStringOptional contact email
created_atDateTimeCreation timestamp
updated_atDateTimeLast update timestamp

Each user can optionally have a tenant_id:

Tenant scope is embedded in JWT claims:

{
"sub": "019b937d-4862-8e07-ac3a-1b8589d1b807",
"email": "alice@example.com",
"username": "alice",
"roles": ["operator"],
"tenant_id": "019b937d-4862-84ae-9c2a-6ac230cc4c7e",
"exp": 1704067200
}

All database queries automatically filter by tenant:

-- For tenant-scoped user (tenant_id = '019b937d-4862-84ae-9c2a-6ac230cc4c7e')
SELECT * FROM targets
WHERE tenant_id = '019b937d-4862-84ae-9c2a-6ac230cc4c7e';
-- For global user (tenant_id = null)
SELECT * FROM targets;
-- No tenant filter applied
ResourceScopedNotes
TargetsAssigned via tenants_targets
DeploymentsInherit from target
TagsAssigned via tenant_tags
VariablesTenant-specific variables

Optionally Tenant-Scoped & Global Resources

Section titled “Optionally Tenant-Scoped & Global Resources”
ResourceScopedNotes
ActionsPartialNullable tenant_id — global when NULL, tenant-private when set
SequencesPartialNullable tenant_id — global when NULL, tenant-private when set
SolutionsPartialNullable tenant_id — global when NULL, tenant-private when set
EnvironmentsPartialNullable tenant_id — global when NULL, tenant-private when set
UsersPartialHave tenant_id but can be global
RolesShared role definitions
PermissionsSystem-wide definitions
Terminal window
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "acme-corp",
"description": "Primary production tenant",
"contact_email": "ops@acme-corp.example"
}' \
"https://api.mantis.local/api/v1/tenants"
Terminal window
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "019b937d-4862-84ae-9c2a-6ac230cc4c7e"
}' \
"https://api.mantis.local/api/v1/admin/users/$USER_ID"
Terminal window
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_id": "019b937d-4862-8aaa-bccd-1122334455ff",
"environment_id": null
}' \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID/targets"

Assign one target per request (environment_id is optional).

Terminal window
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"solution_id": "019b937d-4862-8ccc-ddee-3344556677bb",
"environment_id": "019b937d-4862-8eee-ff00-5566778899cc"
}' \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID/solutions"

Assign one solution per request; both solution_id and environment_id are required.

Delegate full tenant management:

Configuration:

  1. Create tenant_admin role user for each tenant
  2. Assign user to tenant: tenant_id = X
  3. Assign tenant_admin role
  4. User can manage their tenant’s resources

Allow tenants to use shared actions/sequences:

Actions, sequences, and solutions all support optional tenant scoping — they are global when tenant_id is NULL and tenant-private when set:

  • Global (unscoped) definitions can be referenced by any tenant
  • Tenant-scoped definitions are visible only to their owning tenant
  • Deployments are scoped to tenant targets

Global users can view across tenants:

User TypeCan ViewCan Modify
Global AdminAll tenantsAll tenants
Global ViewerAll tenantsNone
Tenant AdminOwn tenantOwn tenant
Tenant UserOwn tenantBased on role

Cross-tenant access returns 404 (not 403):

Terminal window
# Tenant A user trying to access Tenant B resource
curl -X GET \
-H "Authorization: Bearer $TENANT_A_TOKEN" \
"https://api.mantis.local/api/v1/targets/$TENANT_B_TARGET"
# Response: 404 Not Found (not 403 Forbidden)

This prevents enumeration attacks.

A cross-tenant access attempt does not match the caller’s tenant scope, so the resource is simply not returned — the API responds with 404 Not Found rather than revealing that the resource exists under a different tenant.

Each tenant can have specific variables:

Terminal window
# Set tenant variable
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"template_id_or_key": "DATABASE_URL",
"value": "postgres://tenant-a-db:5432/app",
"is_encrypted": true,
"variable_type": "common"
}' \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID/variables"

Variables are injected during deployments:

CREATE TABLE tenants (
id UUID PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
logo_url TEXT,
contact_email TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
-- Tenant solutions (assignment is scoped to an environment)
CREATE TABLE tenants_solutions_environments (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
solution_id UUID NOT NULL REFERENCES solutions(id),
environment_id UUID NOT NULL REFERENCES environments(id),
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (tenant_id, solution_id, environment_id)
);
-- Tenant targets
CREATE TABLE tenants_targets (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
target_id UUID NOT NULL REFERENCES targets(id),
environment_id UUID REFERENCES environments(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (tenant_id, target_id, environment_id)
);
-- Tenant tags
CREATE TABLE tenant_tags (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
tag_id UUID NOT NULL REFERENCES tags(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (tenant_id, tag_id)
);
-- Tenant variables (template-based, environment-scoped)
CREATE TABLE tenant_variables (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
template_id UUID NOT NULL REFERENCES tenant_variable_templates(id),
environment_id UUID REFERENCES environments(id),
value TEXT NOT NULL,
value_encrypted TEXT,
is_encrypted BOOLEAN NOT NULL DEFAULT false,
encryption_version INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE (tenant_id, template_id, environment_id)
);
-- Users have optional tenant_id
ALTER TABLE users ADD COLUMN tenant_id UUID REFERENCES tenants(id);
-- Index for query performance
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
  1. One tenant per customer/team - Clear organizational boundary
  2. Use meaningful names - acme-corp, team-platform, prod-west
  3. Document tenant purpose - Clear descriptions for each tenant
  1. Minimize global users - Most users should be tenant-scoped
  2. Use tenant_admin role - Delegate management within tenants
  3. Regular audits - Review tenant user assignments
  1. Assign resources explicitly - Don’t rely on defaults
  2. Document ownership - Track which tenant owns what
  3. Clean up unused - Remove resources from inactive tenants
  1. Check user’s tenant assignment:

    Terminal window
    curl -s -H "Authorization: Bearer $TOKEN" \
    https://api.mantis.local/api/v1/admin/users/$USER_ID | jq '.tenant_id'
  2. Check resource’s tenant assignment:

    Terminal window
    curl -s -H "Authorization: Bearer $TOKEN" \
    https://api.mantis.local/api/v1/tenants/$TENANT_ID/targets
  3. Verify tenant matches

  1. Verify tenant_id is null in user record
  2. Check role permissions - Global users still need appropriate roles
  1. Check assignment tables - tenants_targets, tenants_solutions_environments
  2. Verify the environment - solution/target assignments are environment-scoped, so check the environment_id
  1. Create default tenant:

    INSERT INTO tenants (name, description)
    VALUES ('default', 'Default Tenant');
  2. Assign existing resources:

    -- IDs are UUIDs; substitute the real tenant UUID below
    INSERT INTO tenants_targets (id, tenant_id, target_id)
    SELECT gen_random_uuid(), '019b937d-4862-84ae-9c2a-6ac230cc4c7e', id FROM targets;
  3. Update existing users:

    UPDATE users SET tenant_id = '019b937d-4862-84ae-9c2a-6ac230cc4c7e'
    WHERE tenant_id IS NULL;
  4. Keep admin users global:

    UPDATE users SET tenant_id = NULL
    WHERE id IN (
    SELECT ur.user_id FROM users_roles ur
    JOIN roles r ON r.id = ur.role_id
    WHERE r.name = 'admin'
    );