Managing Groups: Assigning and Unassigning Roles via GraphQL

5 min read

This page explains how to manage the roles of a group programmatically. It documents what the Manage Roles action in the Access Management UI does under the hood, and how to reproduce it via the Gatekeeper GraphQL API so admins and engineers can script role assignment.

For an introduction to Gatekeeper, the Access Management UI, and the Groups table (Create Group, Manage Group, Manage Members, Manage Roles, Delete), see the parent page.


How role assignment works

Role membership is stored as Casbin grouping policies in the Gatekeeper service. A grouping policy is a triple:

{ subject, group, company }

Field

Meaning

subject

The entity that inherits the role. To assign a role to a group, this is the groupId. (The same mutation assigns roles to a user when subject is a userId.)

group

The role being assigned — the Gatekeeper role id. For system roles this is the enum value (e.g. Analytics_Admin). For custom roles this is the generated id (custom__<slug>__<8 hex>). The field name is the Casbin "parent"/role in the g, subject, role, company rule, not the Unique group.

company

The companyId the policy applies to.

To assign multiple roles in a single call, pass one policy entry per role.

Do not use Zitadel role names (chat.data.admin, analytics.admin, admin.user-management.write) as the group field. Those names are only used when mapping incoming Zitadel grants. Role assignment via this API always uses Gatekeeper ids from adminGetAllRoles.

The mutations

Both mutations live on the Gatekeeper admin resolver (next/services/gatekeeper/src/admin/admin.resolver.ts):

Mutation

Purpose

adminAddGroupingPolicies(input: AddGroupingPoliciesInput!)

Assign one or more roles

adminRemoveGroupingPolicies(input: RemoveGroupingPoliciesInput!)

Unassign one or more roles

Both take the same input shape — input.policies is a list of GroupingPolicyCreateParamsInput:

graphql
input AddGroupingPoliciesInput {
  policies: [GroupingPolicyCreateParamsInput!]!
}

input GroupingPolicyCreateParamsInput {
  subject: String!   # groupId (or userId)
  group: String!     # Gatekeeper role id: Analytics_Admin, custom__senior-analyst__a1b2c3d4, …
  company: String!   # companyId
}

Both mutations return a Boolean (true on success) and are audit-logged.

Required permissions and authentication

  • All admin* queries and mutations require the admin.user-management.write Zitadel role (User Management Admin). Without it the request is rejected.

  • Authenticate with a standard Zitadel bearer token (the same token your browser session uses; for scripting, obtain a token for a service user or your own user that holds the role) and send requests to the Gatekeeper service GraphQL endpoint.

  • companyId: this is the company you operate in. You can read it from your own token/session (it is the companyId claim of the authenticated user), from the Access Management UI. All policies in one call must target the same company.

Guardrails

  • Externally-managed groups are locked. Both mutations validate the policies and reject any that target an externally managed group (one synced from an external IdP, i.e. with an externalId) with Cannot modify members of externally managed group(s): <names>. Manage such groups in the external system instead.

  • Role inheritance. Assigning a role to a group means all current and future members of the group inherit that role — including the role's full inheritance chain (a role that itself inherits from other roles grants those too). See System Roles for the system role hierarchy and the Role ID column to use in group.

  • Policy reload. Policy changes made through these mutations take effect immediately. If you ever need to force Gatekeeper to re-read all policies from the database (e.g. after out-of-band changes), the adminReloadPolicies mutation reloads the enforcer. It is not required after a normal assign/unassign.


Worked example — assign two roles to the "Data Team" group

Goal: assign the Analytics Administrator system role and a custom Senior Analyst role to the Data Team group, verify, then unassign Senior Analyst again.

All requests below run against the Gatekeeper GraphQL endpoint with a bearer token holding admin.user-management.write. Replace the placeholder ids with your own values.

Step 1 — Find the group and capture its groupId

List groups via Gatekeeper's admin API:

graphql
query FindGroup {
  adminGroupList {
    id
    name
    isExternallyManaged
    memberCount
    roles
  }
}

Example response (abridged):

json
{
  "data": {
    "adminGroupList": [
      {
        "id": "grp_data_team_123",
        "name": "Data Team",
        "isExternallyManaged": false,
        "memberCount": 8,
        "roles": []
      }
    ]
  }
}

Capture the id: groupId = grp_data_team_123. Check isExternallyManaged is false — otherwise role assignment will be rejected.

Alternatively, groups can be listed via scope-management (allGroups / allGroupsWithMembersCount), or a single group inspected via adminGroupDetail(groupId: ...) in Gatekeeper.

Step 2 — Find the role ids

List all available roles (system + custom):

graphql
query FindRoles {
  adminGetAllRoles {
    id
    name
  }
}

Example response (abridged):

json
{
  "data": {
    "adminGetAllRoles": [
      { "id": "Analytics_Admin", "name": "Analytics Administrator" },
      { "id": "custom__senior-analyst__a1b2c3d4", "name": "Senior Analyst" }
    ]
  }
}
  • System roles use the GatekeeperRoles enum value as id (e.g. Analytics_Admin, Chat_User, KB_Viewer). The name is the human-readable label shown in the UI. Full list: System Roles.

  • Custom roles use a generated id of the form custom__<slugified-name>__<8 hex chars> (e.g. custom__senior-analyst__a1b2c3d4). You cannot choose this id; copy it from adminGetAllRoles after creating the role.

Use the id value as the group field in the policies below. Never pass the display name (Analytics Administrator) or a Zitadel role (chat.data.admin).

Step 3 — Assign the roles

Assign both roles in one call — one policy entry per role:

graphql
mutation AssignRolesToGroup {
  adminAddGroupingPolicies(input: {
    policies: [
      { subject: "grp_data_team_123", group: "Analytics_Admin", company: "company_789" },
      { subject: "grp_data_team_123", group: "custom__senior-analyst__a1b2c3d4", company: "company_789" }
    ]
  })
}

Response:

json
{ "data": { "adminAddGroupingPolicies": true } }

Step 4 — Verify

Confirm the roles are attached to the group:

graphql
query VerifyGroupRoles {
  adminGroupDetail(groupId: "grp_data_team_123") {
    id
    name
    roles
    members {
      id
      email
    }
  }
}

Expected: roles now contains both assigned Gatekeeper ids:

json
{
  "data": {
    "adminGroupDetail": {
      "id": "grp_data_team_123",
      "name": "Data Team",
      "roles": ["Analytics_Admin", "custom__senior-analyst__a1b2c3d4"],
      "members": [ { "id": "user_001", "email": "ana@example.com" } ]
    }
  }
}

All current and future members of the group now inherit both roles (and anything those roles inherit). You can spot-check a member's effective permissions with adminGetUserPermissions(userId: "user_001") or their implicit roles with getImplicitUserRoles.

Step 5 — Unassign a role

Remove the Senior Analyst role with the same policy shape:

graphql
mutation UnassignRoleFromGroup {
  adminRemoveGroupingPolicies(input: {
    policies: [
      { subject: "grp_data_team_123", group: "custom__senior-analyst__a1b2c3d4", company: "company_789" }
    ]
  })
}

Response:

json
{ "data": { "adminRemoveGroupingPolicies": true } }

Step 6 — Re-verify

Run the same adminGroupDetail query as in step 4. Expected: roles now contains only Analytics_Admin — members immediately stop inheriting Senior Analyst.


UI equivalent — Manage Roles

The GraphQL flow above is exactly what the UI does when you use Manage Roles:

  1. Open Access ManagementGroups tab.

  2. Find the group in the Groups table and choose Manage Roles.

  3. Assign or remove roles in the dialog — each assignment/removal corresponds to an adminAddGroupingPolicies / adminRemoveGroupingPolicies call with the group's id as subject and the Gatekeeper role id as group.

  4. All members of the group inherit the assigned roles.

Externally-managed groups are shown as locked in the UI, mirroring the API-side validation.


Other group operations (for reference)

Role assignment is only one of the group operations. Group CRUD, membership, and listing live in the scope-management service (next/services/node-scope-management/src/group/group.resolver.ts and membership.resolver.ts):

Operation

Resolver

Required Gatekeeper permission

List groups

allGroups, allGroupsWithMembersCount

GROUP / LIST

Create group

createGroup

GROUP / WRITE

Update group

updateGroup

GROUP / WRITE

Delete group

deleteGroup

GROUP / DELETE

Group configuration

updateGroupConfiguration

GROUP / MANAGE

List group members

groupUsers

(service access)

Add/remove members

membership mutations (membership.resolver.ts)

GROUP / MANAGE

These enforce the GatekeeperResources.GROUP resource permissions, whereas the role-assignment mutations documented above require the admin.user-management.write Zitadel role.


Quick reference

  • Assign role(s): adminAddGroupingPolicies(input: { policies: [{ subject: <groupId>, group: <GatekeeperRoleId>, company: <companyId> }] })

  • Unassign role(s): adminRemoveGroupingPolicies with the same shape

  • group is a Gatekeeper id from adminGetAllRoles (Analytics_Admin, custom__senior-analyst__a1b2c3d4) — never a Zitadel role name

  • One policy entry per role; multiple roles = multiple entries in one call

  • Requires admin.user-management.write (User Management Admin); all calls are audit-logged

  • Members inherit assigned roles (including the roles' inheritance chains)

  • Externally-managed groups cannot be modified via this API

  • Verify with adminGroupDetail(groupId); force a policy reload with adminReloadPolicies only if needed

See also: System Roles

Last updated