Provisioning over HTTP deprecated
Provisioning over HTTP
The provisioning API is available over HTTP and JSON, as an alternative to SOAP. A gateway translates the JSON requests into the gRPC calls the middleware serves internally, so the same operations are reachable without a SOAP client.
SOAP is not deprecated. Both interfaces stay available and act on the same data; a system can use either, or both at once.
Where an identity provider is to drive the users, groups, resources and shared accounts of a context, SCIM provisioning covers that lifecycle with the connector the provider already ships. It builds on the same provisioning services and authenticates with the tokens issued here.
Enabling it
The gateway runs as a sidecar next to the middleware pods that serve provisioning. It is off by default:
provisioningGateway:
enabled: true
That is enough for a first look. Everything below has a working default:
| Value | Default | What it is |
|---|---|---|
role | admin | The pod role carrying the gateway. It reaches the middleware over the pod's loopback interface, so it has to sit next to one that serves provisioning. |
port | 8080 | Port the gateway listens on. |
grpcPort | 8066 | The middleware's gRPC port, i.e. com.openexchange.grpc.server.port. Change it here as well if you change it there. |
timeout | 60s | Upstream timeout. Deliberately well above Envoy's default, because some provisioning calls are slow. |
service.type / service.port | ClusterIP / 80 | The Service in front of the gateway. |
Reaching it
The chart creates a Service named <release>-provisioning-gateway in the release namespace, of type ClusterIP by default. It creates no Ingress — this chart defines none for any of its services, including the middleware itself. Routing traffic to the gateway from outside the cluster is therefore the same step, and uses the same mechanism, as exposing the middleware: an Ingress, an Istio VirtualService or whatever the installation already uses, pointed at that Service.
Do not expose it the way the end-user HTTP API is exposed. This endpoint is administrative and belongs behind whatever restriction the SOAP provisioning endpoint sits behind today.
Securing it
The endpoint carries administrative credentials. Treat it like the SOAP endpoint: do not publish it, and terminate TLS in front of it.
provisioningGateway:
tls:
enabled: true
existingSecret: provisioning-gateway-tls
service:
port: 443
The Secret is an ordinary kubernetes.io/tls Secret with tls.crt and tls.key. The chart does not generate a certificate — rendering fails if existingSecret is empty while TLS is enabled. Without TLS the credentials travel in clear text, which is only defensible on a trusted, non-routable network.
Which operations are exposed
Only the services listed under services are reachable. That list is the endpoint's attack surface, so it is worth a deliberate decision rather than a copy of everything available.
The default is the set SOAP already offers, so a caller can move off SOAP without losing an operation it had: contexts, users, groups, resources, shared and secondary accounts, reseller, deputy permissions, the server/database/filestore registry (SOAP's OXUtilService), job management (OXTaskMgmtService), user copy and session management (OXSessionService). Provisioning tokens (ProvisioningTokenService, see below) have no SOAP counterpart and are exposed as well.
SessiondService carries one operation SOAP does not, ClearSessionStorage, which drops the whole session storage rather than the sessions of one user or context. Envoy filters by service, not by method, so exposing the service exposes that one too; leave it out of services where that is not wanted.
Everything beyond that stays opt-in — in particular the cluster maintenance services (ExtendedUpdateTaskService, DBMigrationService, SchemaService, ContextRestoreService, ConsistencyService). These are not what a provisioning client needs, and they include operations such as starting an update run.
To expose one, add its full gRPC service name:
provisioningGateway:
services:
- com.openexchange.grpc.provisioning.ContextService
- com.openexchange.grpc.provisioning.UserService
# … the rest of the default list …
- com.openexchange.grpc.provisioning.SchemaService
A service name that does not exist is rejected when the gateway starts, so a typo shows up as a failing container rather than as a silently missing endpoint.
Calling it
Paths start with /prov/v1/, and resources are nested the way they belong together:
GET /prov/v1/contexts
POST /prov/v1/contexts
PATCH /prov/v1/contexts/{context_id}
DELETE /prov/v1/contexts/{context_id}
POST /prov/v1/contexts/{context_id}:enable
GET /prov/v1/contexts/{context_id}/users
GET /prov/v1/contexts/{context_id}/groups/{group_id}
POST /prov/v1/contexts/{context_id}/users/{user_id}/deputies:grantMultiple
POST /prov/v1/contexts/{context_id}/tokens
DELETE /prov/v1/contexts/{context_id}/tokens/{token_id}
Operations that are not a plain create/read/update/delete carry a verb suffix after a colon, as in :enable or :grantMultiple.
Two things about these paths are worth knowing before writing a client:
- Path parameters carry plain identifiers and are named after the request field in snake case:
{context_id},{user_id},{group_id}. Where an object may be addressed by name instead, the path offers a second parameter such as{resource_name}. - There is no endpoint for reading a single context. One is read through the collection, by passing its identifier as a filter to
GET /prov/v1/contexts. The same holds for users and resources: the collection endpoint takes identifiers, a pattern and paging as parameters. Groups are the exception and can be read one at a time.
Credentials go into the Authorization header, as HTTP basic — never into the request body. The login is the admin's own; which context it acts on comes from the path, not from the login. This is the point where a SOAP client will trip: SOAP carries them inside the message, and the same habit produces a request the gateway rejects.
curl -u oxadminmaster:secret https://<host>/prov/v1/contexts
curl -u oxadmin:secret \
-H 'Content-Type: application/json' \
-d '{"user": {"name": "jdoe", "displayName": "John Doe"}}' \
https://<host>/prov/v1/contexts/1/users
Contexts are addressed numerically. SOAP accepts a context by name in places where the HTTP path takes the identifier.
An OpenAPI document describing every path, parameter and response is generated from the API definition itself and published alongside the other API documentation:
https://documentation.open-xchange.com/components/middleware/provisioning/<version>/
That page renders the description in a browser. The document itself sits next to it as provisioning.swagger.json and is the file to point a client generator at.
Provisioning tokens
Automated clients that cannot hold an administrator's password, an identity provider driving the SCIM endpoint for instance, authenticate with a provisioning token instead. A token is bound to one context and one scope, so it opens that one interface for that one context and nothing else; it is not accepted on /prov itself.
Tokens are managed over the gateway by the context administrator, a reseller administrator owning the context or, where MASTER_ACCOUNT_OVERRIDE permits it, the master administrator - the same rule every other operation inside a context follows:
# create; the secret is part of this response only
curl -u oxadmin:secret \
-H 'Content-Type: application/json' \
-d '{"label": "Entra ID", "scope": "SCIM", "expires": 0}' \
https://<host>/prov/v1/contexts/1/tokens
# list (metadata only)
curl -u oxadmin:secret https://<host>/prov/v1/contexts/1/tokens
# revoke
curl -u oxadmin:secret -X DELETE https://<host>/prov/v1/contexts/1/tokens/<token_id>
The secret has the form ox_<context-id>_<64 hex characters>. Only its SHA-256 hash is stored, so a lost secret cannot be recovered; revoke the token and create a new one. expires is a point in time in milliseconds since the epoch, 0 means the token does not expire. Every successful use records its time, visible as lastUsed in the listing, which is how a forgotten token can be spotted.
Trying it without a deployment
The gateway can be run against a middleware on a developer machine with ./gradlew :grpc-proto:provisioningGateway, which makes the same paths available at http://localhost:8090/prov/v1/.... Useful for reproducing a report without a cluster; see grpc-api/readme.md in the middleware sources for the details.
Errors
The gateway maps the internal status onto an HTTP status and returns a JSON body with a message:
| Situation | HTTP |
|---|---|
| Invalid credentials | 401 |
| Object does not exist | 404 |
| Malformed or contradictory request data | 400 |
| Database update in progress — retry later | 503 |
| Storage or internal error | 500 |
One wrinkle worth knowing: a missing context is reported as 404 only where the operation acts on the context from the outside — creating, listing, capabilities and the like, which authenticate the master admin and then look it up.
Operations that act inside a context — users, groups, resources, deputies — authenticate against that context, and that step fails before anything is looked up. They answer 401 for a context that does not exist, the same as for a wrong password. A caller cannot tell those two apart by status code, and giving a context id it has no rights on looks identical to giving one that is not there.
Differences from SOAP
The operations are the same, but they are named and grouped differently. SOAP spells out combinations as separate operations; the HTTP API folds them into one call with parameters. Listing contexts is a single endpoint where SOAP has eight operations — pattern, paging, restriction to a database or filestore, and lookup by identifier are all parameters of GET /prov/v1/contexts.
Three behaviors differ deliberately:
- Fetching a group without specifying one returns the context's default group.
- Blocking and unblocking a deputy permission is one endpoint with a boolean, not two operations.
- The reseller data of a context — its custom identifier, its owner and its restrictions — is a resource of its own at
/prov/v1/contexts/{context_id}/reseller, where SOAP carries those fields in the context itself. Sendingrestrictionsreplaces them wholesale, so an empty list removes all of them; leaving the field out keeps the ones the context has. The owner is reported by identifier and name only — read the full record withGET /prov/v1/resellers/{reseller_id}.
Granting several deputy permissions at once rolls back on error: if one grant fails, those already granted in that call are revoked before the error is reported, so a failed call leaves nothing behind.