> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fga.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Workforce via SCIM Groups

> Sync an internal workforce identity provider into FGA using SCIM-provisioned Auth0 groups and group membership.

Use this when your users are your own employees, provisioned from an upstream workforce identity provider (Okta, Entra ID, etc.) via [inbound SCIM](https://auth0.com/docs/authenticate/protocols/scim/configure-inbound-scim), rather than external customers signing up through an Auth0 Organization. Group membership, not organization membership, is what changes as people join, move, and leave, and no organizations are involved in the model below.

A workforce setup typically provisions through a single enterprise connection (one IdP, one tenant), so the model below doesn't include a `connection` type at all. Instead, every rule filters on that connection's ID directly, so only groups and members from your workforce IdP are synced, not from any other connection your tenant might have.

## Model

FGA model · DSL

## Mapping

```yaml theme={null}
version: "1"

rules:
  - name: "Add user member to group"
    when: >-
      input.type == "group.member.added" &&
      input.data.object.member.member_type == "user" &&
      input.data.object.group.connection_id == "con_YOUR_CONNECTION_ID"
    variables:
      group_id: input.data.object?.group?.id
      member_id: input.data.object?.member?.id
    tuples:
      - user: "user:{{ variables.member_id }}"
        relation: "member"
        object: "group:{{ variables.group_id }}"

  - name: "Remove user member from group"
    when: >-
      input.type == "group.member.deleted" &&
      input.data.object.member.member_type == "user" &&
      input.data.object.group.connection_id == "con_YOUR_CONNECTION_ID"
    variables:
      group_id: input.data.object?.group?.id
      member_id: input.data.object?.member?.id
    tuples:
      - action: delete
        user: "user:{{ variables.member_id }}"
        relation: "member"
        object: "group:{{ variables.group_id }}"

  - name: "Add nested group to group"
    when: >-
      input.type == "group.member.added" &&
      input.data.object.member.member_type == "group" &&
      input.data.object.group.connection_id == "con_YOUR_CONNECTION_ID"
    variables:
      group_id: input.data.object?.group?.id
      member_id: input.data.object?.member?.id
    tuples:
      - user: "group:{{ variables.member_id }}#member"
        relation: "member"
        object: "group:{{ variables.group_id }}"

  - name: "Remove nested group from group"
    when: >-
      input.type == "group.member.deleted" &&
      input.data.object.member.member_type == "group" &&
      input.data.object.group.connection_id == "con_YOUR_CONNECTION_ID"
    variables:
      group_id: input.data.object?.group?.id
      member_id: input.data.object?.member?.id
    tuples:
      - action: delete
        user: "group:{{ variables.member_id }}#member"
        relation: "member"
        object: "group:{{ variables.group_id }}"

  - name: "Revoke all group tuples on deletion"
    when: >-
      input.type == "group.deleted" &&
      input.data.object?.id != nil &&
      input.data.object.connection_id == "con_YOUR_CONNECTION_ID"
    variables:
      group_id: input.data.object.id
    tuple_filters:
      - object: "group:{{ variables.group_id }}"
        action: delete
      - object: "group:"
        user: "group:{{ variables.group_id }}#member"
        action: delete

tests:
  - name: "group.member.added adds a user member"
    input:
      type: "group.member.added"
      data:
        object:
          group:
            id: "grp_engineering"
            connection_id: "con_okta_scim"
          member:
            member_type: "user"
            id: "okta|okta-scim|00u17alice"
    expect_tuples:
      - user: "user:okta|okta-scim|00u17alice"
        relation: "member"
        object: "group:grp_engineering"

  - name: "group.member.added from a different connection writes nothing"
    input:
      type: "group.member.added"
      data:
        object:
          group:
            id: "grp_engineering"
            connection_id: "con_some_other_connection"
          member:
            member_type: "user"
            id: "okta|okta-scim|00u17alice"
    expect_tuples: []

  - name: "group.member.deleted removes a user member"
    input:
      type: "group.member.deleted"
      data:
        object:
          group:
            id: "grp_engineering"
            connection_id: "con_okta_scim"
          member:
            member_type: "user"
            id: "okta|okta-scim|00u17alice"
    expect_tuples:
      - user: "user:okta|okta-scim|00u17alice"
        relation: "member"
        object: "group:grp_engineering"
        action: delete

  - name: "group.member.added nests a group inside another group"
    input:
      type: "group.member.added"
      data:
        object:
          group:
            id: "grp_engineering"
            connection_id: "con_okta_scim"
          member:
            member_type: "group"
            id: "grp_platform"
    expect_tuples:
      - user: "group:grp_platform#member"
        relation: "member"
        object: "group:grp_engineering"

  - name: "group.member.deleted removes a nested group"
    input:
      type: "group.member.deleted"
      data:
        object:
          group:
            id: "grp_engineering"
            connection_id: "con_okta_scim"
          member:
            member_type: "group"
            id: "grp_platform"
    expect_tuples:
      - user: "group:grp_platform#member"
        relation: "member"
        object: "group:grp_engineering"
        action: delete

  - name: "group.deleted revokes all group tuples"
    input:
      type: "group.deleted"
      data:
        object:
          id: "grp_engineering"
          connection_id: "con_okta_scim"
    expect_tuples: []
    expect_tuple_filters:
      - object: "group:grp_engineering"
        action: delete
      - object: "group:"
        user: "group:grp_engineering#member"
        action: delete
```

Replace `con_YOUR_CONNECTION_ID` with the real connection ID from your tenant. Every rule's `when` guard checks `connection_id` before writing or deleting anything, so groups and members provisioned through any other connection in your tenant are left untouched.

Group **membership** changes (`group.member.added`/`.deleted`) arrive via a SCIM client whenever your IdP drives them directly. Group deletion is a separate action an admin takes directly in the Auth0 Dashboard, not part of the SCIM sync itself.

**Nested groups** (a group as a member of another group) are handled by the "Add nested group" and "Remove nested group" rules above: they mirror the user-member rules for `member_type == "group"`, writing `group:{member_id}#member` in place of `user:{member_id}`, and keep the same `connection_id` guard. The `group#member` type restriction on `group.member` in the model is what allows this.

**If you provision through more than one connection**, add `type connection` back to the model (`define member: [user, group#member]` on `connection`, and `define connection: [connection]` on `group`), replace the `connection_id` guards with a `group.created` rule that writes both the `group#member` membership and the `connection` tuples from the event's `connection_id`, and see [B2B via Connections](https://docs.fga.dev/relationship-sync/use-cases/b2b-connections.md#model) for that shape.

## Roles

If different group members need different access, assign roles directly to the group. See [Roles: a group, at the tenant level](https://docs.fga.dev/relationship-sync/use-cases/roles.md#a-group-at-the-tenant-level) for the model, mapping, and tests, and [Roles: a group, within an organization](https://docs.fga.dev/relationship-sync/use-cases/roles.md#a-group-within-an-organization) if you also assign roles to groups from inside an organization.
