---
title: "Provision accounts from a directory"
description: "Create and update accounts by immutable provider identity, using a dedicated provisioner certificate."
canonical_url: "https://docs.oz0.ai/admin/sign-in/provisioning"
---
# Provision accounts from a directory

> Create and update accounts by immutable provider identity, using a dedicated provisioner certificate.

For: cluster and tenant administrators.

Create and update accounts by immutable provider identity, using a dedicated provisioner certificate.

## Accounts from a directory

A provisioner, such as a service that syncs your directory or a script of your own that reads it, can make accounts before
people first sign in, keep their groups current, and end their access, through the admin API. It keys each account by
its external ID: the provider's ID in the cluster (as `oz0 oidc list` shows it) and the provider's immutable ID for
the person, the `sub` of its ID tokens. Every call is idempotent, so a provisioner can repeat any of them.

```bash [Terminal]
oz0 account upsert --provider okta --subject 00u1a2b3c4 --name kim --email kim@acme.se
oz0 account groups --provider okta --subject 00u1a2b3c4 --group eng --group oncall --version 7
oz0 account get    --provider okta --subject 00u1a2b3c4
oz0 account list   --provider okta
oz0 account disable --provider okta --subject 00u1a2b3c4   # and enable
oz0 account remove  --provider okta --subject 00u1a2b3c4
```

- `account upsert` makes the account without a password, linked to the external ID, so the person's first sign-in
through the provider finds it by that link and never by name. Run again, it returns the same account and leaves its
name alone. A name another account already holds is refused, never merged. A new account is a cluster `member`
(or `--role reader`); a cluster admin only comes from a group rule.
- `account groups` replaces the person's groups at the provider and applies the provider's
[group rules](https://docs.oz0.ai/admin/sign-in/identity-providers#roles-from-groups) right away, the same rules a sign-in applies, so the provisioner and a sign-in
agree. Roles an owner set by hand stay, and the command lists them. `--version`, a number that grows with each change
at the provider, makes a retry or a late update change nothing.<br />

By default a sign-in still applies the ID token's groups, and the latest of the two wins. A provider whose tokens
carry no groups takes them from the directory instead: `oz0 oidc set okta --groups-source directory`,
`groups_source: directory` in the file, or *Groups come from* under *Scopes and claims* in the provider's dialog.
- `account memberships` sets roles in tenants directly (`--member acme:operator`), for a provisioner whose
provider has no group rules. It replaces only the roles it gave before, and gives a role only in a tenant that
[allowed the provisioner](https://docs.oz0.ai/admin/sign-in/provisioning#which-tenants-a-provisioner-gives-roles-in), up to the role allowed there.
- `account disable` ends every session of the account at once and refuses its sign-ins; `account enable` lets it
back. The account remembers who disabled it. A provisioner may enable only an account its own provider's directory
disabled: when a cluster admin disabled it, say during an incident, the directory's next pass leaves it disabled
(the call succeeds with `kept`), and only a cluster admin enables it again.
- `account remove` ends the person's access everywhere: their sessions, their links to identity providers, the
chat identities linked to the account, their roles in tenants and their teams, their own quotas and budget requests,
the MCP clients they approved, and their unsaved drafts. It prints what it ended.<br />

What their jobs spent stays in the tenant's cost history. Run again, it changes nothing. `oz0 user remove NAME` and *Remove account* under *Access* take the
same steps, so ending someone's access never needs a provisioner. Operators' certificates are not accounts: when
the person also had one, the command names it, and `oz0 server operator disable NAME` refuses it.

The tenant API keys the person made belong to the tenant, which decides what removing the person does to them: they
are revoked, the default, or kept working and listed for the tenant's admins until one of them revokes each key. See
[API keys](https://docs.oz0.ai/build/apps/runtime-api/authentication#authentication).

### A provisioner's credentials

A provisioner can use an operator's certificate, but it needs far less. Give it a certificate of its own, on a
server's machine:

```bash [Terminal]
oz0 server provisioner issue directory-sync --provider okta   # OZ0_DB set; --dev --data-dir DIR for a dev server
```

The certificate works only for these `account` calls, for the people of the providers it names (`--provider` again
replaces them), and for renewing itself (`oz0 operator renew` in its context). Every other call of the admin API,
the runtime API, Temporal's ports and the edge's routes refuse it: it cannot read jobs, secrets, keys or settings,
change providers, or sign anyone in. It lasts 90 days. `oz0 server provisioner disable directory-sync` refuses it at once, if
it is lost.

A provisioner can write its own entries to the audit log, under the `provisioning.` actions only, such as an IdP
request it could not apply; the entry names the provisioner as its actor. It may put an entry in a tenant's log only
when its providers reach that tenant (a group rule names it, the provisioner gave someone a role there, or the tenant
allowed it), and at most 20 entries a second, with bursts of up to 1,000; past that it is asked to retry.

### Which tenants a provisioner gives roles in

A provisioner gives roles directly (`account memberships`) only in the tenants that allowed it, each up to a highest
role. Without an allowance it gives no role in a tenant, not even `reader`. Whoever manages a tenant's members (its
owners) allows it, within what they hold there, so only an owner lets a provisioner make owners; cluster admins may
too:

```bash [Terminal]
oz0 tenant provisioner allow directory-sync --tenant acme --up-to operator             # roles up to operator, no content
oz0 tenant provisioner allow directory-sync --tenant acme --up-to admin --content      # up to admin, the content permission too
oz0 tenant provisioner list --tenant acme
oz0 tenant provisioner remove directory-sync --tenant acme
```

- `--up-to` takes `reader`, `operator`, `admin` or `owner` (default `operator`). A role of a [role set](https://docs.oz0.ai/admin/tenants#roles-and-permissions) counts by its permissions: it may be given only when the highest role holds every one of them.
- Without `--content`, the provisioner gives no content permission there: a role whose default includes it is given without it, and a request that asks for it is refused.
- A request that asks for a role the tenant did not allow is refused whole, naming the tenant and the command that allows it, and changes nothing. Removing a role it gave is never refused, so a sync can always end access.
- Taking an allowance back, or lowering it, leaves the roles already given; the tenant's owners see them under *Access* (made by `directory:<provider>`) and change them by hand, and the provisioner's next request that names the tenant is refused until it fits.
- Each change is in the tenant's audit log, as `provisioner.tenant.allow` and `provisioner.tenant.remove`.
- [Group rules](https://docs.oz0.ai/admin/sign-in/identity-providers#roles-from-groups) are a cluster admin's, and apply to a provisioner's `account groups` as they do at every sign-in: a rule that gives `owner` gives it to whoever the directory puts in its group. Allowances do not change them.

A removed account leaves a record of its name, when it was made and removed, and who removed it, so the audit log
still says who held a name when it is given to someone else. The record keeps a SHA-256 hash of each external ID the
account had, not the IDs themselves. The hashes have no key, so they are pseudonymised, not anonymous: anyone who can
read the database or a backup can check a guessed ID against them.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
