Skip to main content

v26.3.14

v26.3.14

Directory Sync applies SCIM replace and clear operations on group members

SCIM PATCH operations that replace a group's full membership (op: replace with path members, or a pathless replace whose value carries a members list) and operations that clear the membership (op: remove on members without a value) are now applied. Previously these operations returned 200 but were ignored, so group membership in Directory Sync could silently drift from the identity provider. Membership changes applied this way emit the usual group.user_added and group.user_removed webhook events, only for members that actually change. The current membership is processed page by page, so replacing the membership of a group larger than DSYNC_MAX_INLINE_GROUP_MEMBERS works even though inline reads of it do not. On the Redis store, indexed reads are now ordered by creation time so that paging through them is stable.

Attribute paths in group PATCH operations (members, displayName, and the members[value eq "..."] filter form) are matched case-insensitively, as RFC 7643 requires.

A PATCH remove operation without a path is now rejected with 400 noTarget, as required by RFC 7644, instead of being misapplied as an attribute update. A members operation whose value is not a list of member references is rejected with 400 instead of being silently ignored.

SCIM discovery endpoints for Directory Sync

Each directory's SCIM endpoint now serves the SCIM 2.0 discovery endpoints defined in RFC 7644: ServiceProviderConfig, Schemas, and ResourceTypes. Identity providers and SCIM clients can use them to validate the connection and discover which protocol features are supported. The endpoints require the directory's bearer token, the same credential used for the Users and Groups endpoints. No action is required for existing directories.

SCIM error responses now include the status and scimType fields

Error responses from the Polis SCIM endpoints now carry the status attribute required by RFC 7644 and, where the specification defines a keyword for the condition, a scimType attribute. A duplicate user or group returns "scimType": "uniqueness" on its 409.

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"status": "409",
"scimType": "uniqueness",
"detail": "User already exists"
}

Identity provider integrations that read these machine-readable fields can now tell a permanent uniqueness conflict apart from other failures, instead of matching on the free-text detail. The change is additive: HTTP status codes and successful responses are unchanged, so existing integrations keep working without any action.

Send the SSO connection client secret as a header when deleting a connection

DELETE /api/v1/sso now accepts the connection's client secret in the x-polis-client-secret header. Use it instead of the clientSecret query parameter, which is deprecated but still works.

A secret in a query string ends up in browser history, proxy access logs, and tracing spans. Sending it as a header keeps it out of the URL and reduces that exposure.

To migrate, move the value from the query string to the header:

DELETE /api/v1/sso?clientID=<client-id>
x-polis-client-secret: <client-secret>

If both are sent, the header takes precedence. Deleting by tenant and product is unchanged.

The setup link endpoint DELETE /api/setup/{token}/sso-connection no longer needs a clientSecret query parameter at all — pass only clientID. A secret sent by an older client is ignored. The endpoint now returns 404 instead of 500 when the clientID does not exist.

The admin portal endpoint DELETE /api/admin/connections takes only clientID for the same reason, and now answers 404 for a clientID that does not exist, where it previously reported success.

Store SCIM PATCH values addressed by path inside their schema extension

A SCIM PATCH can address an attribute inside a schema extension in two ways. It can name the extension and pass an object of attributes:

{
"op": "replace",
"path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
"value": { "manager": { "value": "61bb1e4c" } }
}

Or it can put the attribute in the path and pass its value directly:

{
"op": "replace",
"path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager",
"value": { "value": "61bb1e4c" }
}

Directory sync told the two apart by the type of the value, so the second form was mistaken for the first whenever its value was an object or an array. The attribute was stored as a literal top-level attribute named urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager, beside the extension rather than inside it, where nothing could read it: absent from SCIM responses, unusable in webhook payloads, and untouched by a later remove operation on the same path.

The two forms are now told apart by whether the attribute arrived in the operation's path, so a value is stored inside its extension whether it is a string, an object or an array, and for every schema rather than only the ones this service defines. A SCIM filter in a path, such as roles[value eq "admin"].display, updates the matching entry inside the extension instead of being stored as a literal attribute name.

Every write now also folds attributes left beside an extension by an earlier release back into it, so affected users are repaired as their identity provider next updates them. An attribute already present in the extension is kept in preference to the stray one.