SCIM group mapping
Use IdP groups as FGA user groups and optional authorization grants.
SCIM group sync feeds the same authorization model your application already uses. The identity provider is the SCIM client: it writes Group resources and membership to SqlOS. SqlOS mirrors those groups into FGA and can optionally map selected groups to roles on existing resources inside the organization's own part of the resource tree.
Every provisioned Group becomes an FGA user group:
| SCIM input | FGA result |
|---|---|
externalId | stable, connection-scoped source link |
displayName | mirrored group name |
members[].value | membership for the linked SqlOS user subject |
| Group rename | update the existing group rather than create a duplicate |
| Group deletion | remove directory-owned memberships and mapped grants; mark the external link inactive |
Mirroring does not grant a role by itself. It makes the directory group available to FGA grants, application access assignments, the dashboard, and the access tester.
Manage membership through Group.members; SqlOS does not accept User.groups as a writable attribute. Manual edits to a mirrored SCIM group's membership may be replaced by the next IdP PUT, PATCH, deactivation, or resync. Memberships in unrelated, non-SCIM FGA groups are not touched.
Group collections support only the exact reconciliation filters used by enterprise IdPs:
id eq "grp_123"
displayName eq "Acme-Admins"
externalId eq "00g123"The operator is eq; compound filters and operators such as co, sw, or pr return invalidFilter.
Group PUT and PATCH support:
displayNamemembers collectionmembers[value eq "{user-id}"]displayName or membersAdd a member:
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [
{ "value": "usr_123" }
]
}
]
}Remove the same member:
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "remove",
"path": "members[value eq \"usr_123\"]"
}
]
}Membership values may be the SqlOS SCIM User id or the user's external ID on the same connection. References from another connection or organization are not resolved. Adding an existing member or removing an absent member succeeds without creating duplicate state.
Most enterprise directory groups describe departments, mailing lists, or assignments—not application permissions. Automatically granting every synced group would create accidental access. SqlOS keeps the safe sequence explicit:
Unmatched groups remain available for deliberate manual grants and application assignments.
The customer's identity provider chooses group display names, and a pattern mapping turns part of that name into a resource ID. FGA resources have no organization of their own, so without a limit a tenant could name a group after another tenant's resource and receive a role on it.
Every SCIM connection therefore declares a grant boundary: the FGA resource that is the organization's root in your resource tree. A mapped grant may target only the boundary resource or one of its descendants.
options.AuthServer.SeedScimConnection("acme", scim =>
{
scim.OrganizationSlug = "acme";
scim.TokenSecretName = "ACME_SCIM_BEARER_TOKEN";
scim.GrantBoundaryResourceId = "org::acme";
});ParentId chain in the FGA tree. The resource is inside the boundary only if that walk reaches the boundary resource. A resource ID that merely starts with the boundary's ID, such as org::acme::store::9001 placed under another organization, is outside it.Fga.MaxResourceHierarchyDepth guards as access checks. A cycle, an over-deep chain, or a boundary resource that no longer exists fails closed.Set the boundary to the organization's root resource, the node where you would grant an organization administrator. The boundary resource must exist before you save it. Do not use the global FGA root in a multi-tenant application; that would let every tenant's directory grant on every tenant's resources.
Use an exact mapping when the IdP group has a stable display name:
scim.MapGroup("Acme-Admins", mapping =>
{
mapping.RoleKey = "org_admin";
mapping.ResourceId = "org::acme";
mapping.Description = "Directory-managed organization administrators";
});A fixed ResourceId is checked against the boundary when the mapping is saved. The seed, the admin API, and the dashboard all reject a fixed resource outside the boundary with the resource_outside_grant_boundary error, so a copied or mistyped configuration cannot grant on another tenant's resource. A resource that does not exist yet is accepted and checked when it appears.
Use MapGroupExternalId instead when the provider's group ID is stable but administrators may rename its display name.
The resulting FGA grant uses the mirrored group subject, not one grant per user. FGA expands membership during access checks, so an IdP membership change updates authorization without rewriting each user's grants.
Use pattern mappings for a controlled family of resources:
scim.GrantBoundaryResourceId = "org::acme";
scim.MapGroupPattern("^Store-(?<storeId>[^-]+)-Managers$", m =>
{
m.RoleKey = "store_manager";
m.ResourceIdTemplate = "org::acme::store::{storeId}";
});For Store-123-Managers, the mapping resolves storeId to 123 and targets org::acme::store::123. SqlOS creates the grant only if org::acme::store::123 is org::acme or one of its descendants in the FGA tree.
The org::acme:: prefix keeps resolved IDs readable, but it is not the security control; the resource tree is. A group name that resolves to a resource under another organization is refused with a scim.grant.outside_boundary sync event, even if that resource's ID happens to start with org::acme. Because a template's result depends on the group name, it is checked every time a group is pushed rather than when the mapping is saved.
Keep patterns anchored and narrow. SqlOS bounds regular-expression evaluation, but a broad pattern can still map an unintended group inside the boundary. The target role and resource must already exist; a mapping does not invent new authorization roots.
Managed-grant metadata records the connection, mapping, external group ID, FGA group, role, resource, and grant. SqlOS uses that ownership to revoke only the access it created when:
User membership changes do not delete the group grant. They add or remove the user's subject from the group, and FGA access changes through the existing group grant.
Changing or disabling a mapping immediately revokes the grants owned by that mapping. Disabling a connection immediately rejects its bearer token and revokes every managed grant owned by the connection. Mirrored groups, memberships, history, manual grants, non-SCIM groups, and unrelated FGA state remain intact.
Re-enabling a connection or mapping allows future provisioning but does not recreate revoked grants by itself. Push or resynchronize the affected groups after re-enabling so SqlOS can evaluate the current mapping and recreate the grants that should exist.
Manual grants and non-SCIM groups remain intact. Disabling or changing a mapping must not remove a grant that was not created and tracked by that mapping.
When the IdP sends active: false or deletes a User, SqlOS removes that user's memberships from the connection's mirrored groups and deactivates the user's organization membership. The person immediately loses group-derived access in that organization.
The same SqlOS user can still be active in another organization. SCIM does not remove that other tenant's membership, sessions, FGA groups, or grants.
Tenant isolation covers both what a connection can see and what it can grant:
Manual grants and application assignments are outside SCIM and are not bounded by the connection. Review them separately.
SeedScimConnection in source control.The mapping editor shows the match mode, source group value, target role/resource, source (seeded or dashboard), enabled state, active managed-grant count, and a boundary check: whether a fixed target is inside the boundary, or that a template is checked on every push. Persisted sync events report missing roles, missing resources, scim.grant.outside_boundary, and scim.grant.boundary_missing, with the resolved resource and the boundary. Patterns that exceed the bounded evaluation time are returned to the SCIM client as request errors.
Connections created before 7.2.1 have no grant boundary, so they fail closed. Their existing managed grants stay until each group's next push, when SqlOS revokes them and records scim.grant.boundary_missing. The dashboard flags these connections.
GrantBoundaryResourceId to the organization's root FGA resource: in SeedScimConnection for code-owned connections, or with the dashboard or UpdateScimConnectionAsync for the rest. Setting it immediately revokes any managed grant outside it, including cross-tenant grants created before the upgrade.scim.grant.mapped audit history from before the upgrade for grants on resources outside each organization's subtree.A mirrored FGA group can also be assigned to a client application in selected_users_groups_roles mode:
{
"principalType": "group",
"principalId": "grp_acme_admins",
"access": "allowed"
}Application access and FGA permissions are separate decisions. The application assignment controls whether a user can use the client. The FGA grant controls what the user can do inside the application.
Query a group without changing it:
export SCIM_BASE_URL="https://app.example.com/sqlos/scim/v2"
export SCIM_TOKEN="paste-the-token-shown-by-sqlos"
curl --fail-with-body --silent --show-error --get \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Accept: application/scim+json" \
--data-urlencode 'filter=displayName eq "Acme-Admins"' \
"$SCIM_BASE_URL/Groups"For repository validation, run the focused SCIM suite and docs gate:
dotnet test tests/SqlOS.Tests/SqlOS.Tests.csproj \
--filter 'FullyQualifiedName~SqlOSScim'
dotnet test tests/SqlOS.IntegrationTests/SqlOS.IntegrationTests.csproj \
--filter 'FullyQualifiedName~ScimProtocolIntegrationTests'
./scripts/docs-check.sh| Symptom | Check |
|---|---|
| IdP cannot find a group | Use one supported exact eq filter and confirm the token belongs to the group's organization |
Member add returns invalidValue | Provision the user first and send that connection's User id or externalId |
| Filtered remove fails | Use members[value eq "..."] with a User identifier, not User.groups |
| Group mirrors but no grant appears | Confirm the mapping is enabled, its role and target resource already exist, and the connection has a grant boundary |
Sync event scim.grant.boundary_missing | Set the connection's grant boundary, or recreate the boundary resource if it was deleted, then resend the group |
Sync event scim.grant.outside_boundary | The resolved resource is not the boundary or a descendant in the FGA tree, or its ancestor chain has a cycle or is too deep. Move the resource under the organization's root or correct the template |
Admin API returns grant_boundary_required | Set the connection's grant boundary before enabling a mapping |
Admin API returns resource_outside_grant_boundary | Choose a resource inside the organization's subtree |
| Rename creates unexpected authorization | Prefer an external-ID mapping when provider group names are mutable |
| Deprovisioned user still has access | Inspect non-SCIM groups, direct grants, application assignments, and memberships in other organizations |
| Mapping failure appears in sync events | Correct the role, resource, or template capture, then resend the group |
| Group request fails during pattern matching | Correct or simplify the pattern from the SCIM error response, then resend the group |
eq lookup works for ID, display name, and external IDinvalidValuescim.grant.outside_boundary