BetterIAM
Server API

sod

Separation-of-duties rules name roles that nobody may hold together, such as creating suppliers and approving payments to them.

Separation-of-duties rules name roles that nobody may hold together, such as creating suppliers and approving payments to them. Each role is fine on its own; the combination makes fraud or an unnoticed mistake possible. Because roles reach people through many paths over time (direct bindings, groups, access requests, packages, configuration), nobody sees such a combination forming. A rule makes Better IAM check for it on every operation that can grant a role, and report the conflicts that already exist. The guide is separation of duties.

How rules are checked

A person holds a rule's role when they have a direct binding to it, a binding through a group they are a live member of, an eligible (just-in-time) binding even before activating it, or a future-dated binding. Expired bindings, lapsed memberships, and deleted identities do not count. Role inheritance is not expanded: name the roles you actually bind.

A rule has one of two modes:

  • prevent (the default): every operation that can grant a role (bindings.create and bindings.update, group membership changes, identities.createMany, access-request approval, access-package assignment and approved package requests, config.apply, and member-invitation acceptance) compares the tenant's conflicts before and after it runs. If it would create a new one, it fails with SOD_CONFLICT (409) and its transaction rolls back.
  • detect: nothing is refused; conflicts are only reported by violations and by the access analysis, as high-severity separation-of-duties findings.

Conflicts that already existed never block unrelated work, so you can add a rule to a tenant that is not clean yet and fix violations at your own pace. SCIM role mappings are not blocked; their conflicts appear in the reports.

Rule management is authorized on iam/sod/* (create, list, violations) and iam/sod/{ruleId} (update, delete), so a compliance team can manage rules without other administrative rights.

Methods5
Serveriam.api.sod
Clientclient.sod
HTTPPOST /api/iam/sod/*
MethodWhat it doesAccess
createDeclares 2 to 20 roles that nobody may hold together, and reports how many people already hold two of them.Credential
deleteRemoves a rule, so the combination is no longer checked or reported.Credential
listLists the tenant's rules, newest first.Credential
updateChanges a rule's name, description, roles, or mode.Credential
violationsLists everyone who currently holds two or more roles of a rule, with names for review screens.Credential

create

Declares 2 to 20 roles that nobody may hold together, and reports how many people already hold two of them.

POST/api/iam/sod/create
client.sod.create()Credential

Used inSeparation of duties

  • Permission: iam:sod:manage on iam/sod/*.
  • Audited as: iam:sod:manage.
  • Errors: INVALID_INPUT when name or roleIds is missing, fewer than 2 or more than 20 distinct roles are named, one of them is a protected Owner role, the mode is not prevent or detect, or the name (200 characters) or description (1000) is too long; NOT_FOUND when a role is not in this tenant.

The result is the stored rule plus existingViolations, the number of conflicts that already exist. Creating a rule never fails because of them. To measure a rule's impact before enforcing it, create it with mode: 'detect' and switch to prevent with update later.

const rule = await iam.api.sod.create(credential, {
  tenantId,
  name: 'Supplier creation vs payment approval',
  roleIds: [supplierAdmin.id, paymentApprover.id],
  description: 'Finance controls policy, section 4.2',
});
// rule.existingViolations: people who already hold both roles
Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/sod/create" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "name": "<name>",
  "roleIds": [
    "<roleId>"
  ]
}'
Signature
iam.api.sod.create(
  credential: CredentialInput,
  input: {
    tenantId: string;
    name: string;
    roleIds: string[];
    mode?: SodRule['mode'];
    description?: string;
  },
): Promise<{
  existingViolations: number;
  name: string;
  description?: string;
  roleIds: string[];
  mode: 'prevent' | 'detect';
  createdAt: number;
  createdBy: string;
  id: string;
  tenantId: string;
  uniqueKey?: string;
}>

delete

Removes a rule, so the combination is no longer checked or reported.

POST/api/iam/sod/delete
client.sod.delete()Credential
  • Permission: iam:sod:manage on iam/sod/{ruleId}.
  • Audited as: iam:sod:manage.
  • Errors: NOT_FOUND when the rule is not in this tenant.
Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/sod/delete" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "ruleId": "<ruleId>"
}'
Signature
iam.api.sod.delete(
  credential: CredentialInput,
  input: { tenantId: string; ruleId: string },
): Promise<{ deleted: boolean }>

list

Lists the tenant's rules, newest first.

POST/api/iam/sod/list
client.sod.list()Credential
  • Permission: iam:sod:read on iam/sod/*.
  • Audited as: iam:sod:read.
Input

Prop

Type

Returns

An array of SodRule.

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/sod/list" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>"
}'
Signature
iam.api.sod.list(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<SodRule[]>

update

Changes a rule's name, description, roles, or mode.

POST/api/iam/sod/update
client.sod.update()Credential
  • Permission: iam:sod:manage on iam/sod/{ruleId}.
  • Audited as: iam:sod:manage.
  • Errors: NOT_FOUND when the rule is not in this tenant; INVALID_INPUT under the same rules as create.

Only the fields you pass change; roleIds replaces the whole list. Switching to prevent takes effect for the next granting operation, and conflicts that exist at that moment still do not block unrelated work.

Input

Prop

Type

Returns

A SodRule object:

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/sod/update" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "ruleId": "<ruleId>"
}'
Signature
iam.api.sod.update(
  credential: CredentialInput,
  input: {
    tenantId: string;
    ruleId: string;
    name?: string;
    description?: string;
    roleIds?: string[];
    mode?: SodRule['mode'];
  },
): Promise<SodRule>

violations

Lists everyone who currently holds two or more roles of a rule, with names for review screens.

POST/api/iam/sod/violations
client.sod.violations()Credential

Used inSeparation of duties

  • Permission: iam:sod:read on iam/sod/*.
  • Audited as: iam:sod:read.

Both prevent and detect rules are covered; pass ruleId to check one rule (an unknown id returns an empty list). Each entry names the rule (ruleId, ruleName, mode), the person (identityId, and identityName, their email or else their name), and the conflicting roles (roleIds, roleNames). Disabled identities are included, because they can be re-enabled. To fix a violation, remove one of the conflicting grants: delete a binding with bindings.delete, remove the person from the group that carries the role, or revoke the access package that granted it.

const violations = await iam.api.sod.violations(credential, { tenantId, ruleId: rule.id });
Input

Prop

Type

Returns

An array of object.

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/sod/violations" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>"
}'
Signature
iam.api.sod.violations(
  credential: CredentialInput,
  input: { tenantId: string; ruleId?: string },
): Promise<{
  identityName: string;
  roleNames: string[];
  ruleId: string;
  ruleName: string;
  mode: SodRule['mode'];
  identityId: string;
  roleIds: string[];
}[]>

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page