Obexal Docs

Docs/Administration/Policy-as-code

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:

FieldContent
accessPolicyConditional access rules (networks, time windows, countries) and the default action
riskPolicyRisk-based access: enabled flag, MFA and deny thresholds
passwordPolicyThe tenant password policy
groupPasswordPoliciesPer-group password policy overrides, keyed by groupId
rolesCustom 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.json

Plan: 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: delete changes 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.json
Note

Use 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.