Policy as code (GitOps)
Export your tenant governance as a declarative JSON bundle, preview changes like a plan, and apply them from CI.
The governance of a tenant (conditional access, risk policy, password rules, custom roles) can be exported as a single declarative JSON document, diffed against a candidate version, and applied. Combined with API tokens, this gives you a full GitOps loop: the configuration lives in Git, CI shows the plan on every pull request, and merge applies it. All three endpoints require the tenant:manage permission.
The configuration bundle
The bundle is versioned (version: 1) and contains exactly five resources:
| Field | Content |
|---|---|
accessPolicy | Conditional access rules (networks, time windows, countries) and the default action |
riskPolicy | Risk-based access: enabled flag, MFA and deny thresholds |
passwordPolicy | The tenant password policy |
groupPasswordPolicies | Per-group password policy overrides, keyed by groupId |
roles | Custom roles (key, name, permissions) |
{
"version": 1,
"accessPolicy": {
"rules": [
{"name": "Office network", "networks": ["203.0.113.0/24"], "action": "allow", "enabled": true}
],
"defaultAction": "mfa"
},
"riskPolicy": {"enabled": true, "mfaThreshold": 40, "denyThreshold": 80},
"passwordPolicy": {"minLength": 12, "rejectBreached": true, "rejectContextual": true,
"requireLower": false, "requireUpper": false, "requireDigit": false, "requireSymbol": false,
"historyCount": 5, "maxAgeDays": 0},
"groupPasswordPolicies": [],
"roles": [
{"key": "support", "name": "Support", "permissions": ["users:view", "audit:view"]}
]
}Export the current configuration
GET /v1/admin/config serializes the live configuration. This is the file you commit:
curl -s https://accounts.obexal.com/v1/admin/config \
-H "Authorization: Bearer $OBX_TOKEN" > tenant-config.jsonPlan: preview the diff
POST /v1/admin/config/plan compares the current configuration with your candidate bundle and returns the changes, in the spirit of terraform plan. Nothing is modified:
curl -s -X POST https://accounts.obexal.com/v1/admin/config/plan \
-H "Authorization: Bearer $OBX_TOKEN" \
-H "Content-Type: application/json" \
--data @tenant-config.json{
"changes": [
{"resource": "accessPolicy", "action": "update"},
{"resource": "role:support", "action": "create"},
{"resource": "role:legacy-ops", "action": "delete"}
],
"hasChanges": true
}Resources are named accessPolicy, riskPolicy, passwordPolicy, groupPasswordPolicy:<groupId> and role:<key>; actions are create, update and delete.
Apply, with the same guardrails
POST /v1/admin/config/apply applies the bundle and returns the executed plan. It reuses the validations of the interactive endpoints, so automation cannot do anything the console forbids:
- The access policy goes through the anti-lockout validation.
- Roles go through permission validation and anti-escalation: a bundle role granting a permission you do not hold yourself is refused with
403. - By default apply is non-destructive:
deletechanges are reported in the plan but skipped. - With
?prune=true, roles and group overrides absent from the bundle are actually deleted (destructive reconciliation).
Every applied change is written to the audit log.
A GitOps loop in three calls
# 1) Export production and version it
curl -s https://accounts.obexal.com/v1/admin/config \
-H "Authorization: Bearer $OBX_TOKEN" > tenant-config.json
# git add tenant-config.json && git commit
# 2) In CI, on every pull request: show the plan
curl -s -X POST https://accounts.obexal.com/v1/admin/config/plan \
-H "Authorization: Bearer $OBX_TOKEN" -H "Content-Type: application/json" \
--data @tenant-config.json
# {"changes":[{"resource":"role:support","action":"create"}],"hasChanges":true}
# 3) On merge: apply (prune deletes what the file no longer declares)
curl -s -X POST "https://accounts.obexal.com/v1/admin/config/apply?prune=true" \
-H "Authorization: Bearer $OBX_TOKEN" -H "Content-Type: application/json" \
--data @tenant-config.jsonUse a dedicated API token scoped tenant:manage for this pipeline, with an expiration date, and store it in your CI secret store.
What the bundle does not cover
The bundle is policy governance only. It deliberately excludes:
- Applications (OIDC and SAML clients) and their secrets.
- The directory: users, groups, memberships, invitations. Group password overrides reference existing groups by
groupId; the bundle never creates groups. - Custom domains: ownership rests on a DNS handshake, which is not declarative. See Custom domains.
- Branding and logo, managed through their own endpoints. See Branding.
- Webhooks, SAML, LDAP and social connections, SCIM and admin API tokens: these carry secrets, which are never exported.
If a resource is not in the five fields above, apply will not touch it.