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 |
|---|---|
| The entity that inherits the role. To assign a role to a group, this is the |
| The role being assigned — the Gatekeeper role |
| The |
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 |
|---|---|
| Assign one or more roles |
| Unassign one or more roles |
Both take the same input shape — input.policies is a list of GroupingPolicyCreateParamsInput:
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 theadmin.user-management.writeZitadel 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 thecompanyIdclaim 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) withCannot 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
adminReloadPoliciesmutation 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:
query FindGroup {
adminGroupList {
id
name
isExternallyManaged
memberCount
roles
}
}Example response (abridged):
{
"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 viaadminGroupDetail(groupId: ...)in Gatekeeper.
Step 2 — Find the role ids
List all available roles (system + custom):
query FindRoles {
adminGetAllRoles {
id
name
}
}Example response (abridged):
{
"data": {
"adminGetAllRoles": [
{ "id": "Analytics_Admin", "name": "Analytics Administrator" },
{ "id": "custom__senior-analyst__a1b2c3d4", "name": "Senior Analyst" }
]
}
}System roles use the
GatekeeperRolesenum value asid(e.g.Analytics_Admin,Chat_User,KB_Viewer). Thenameis the human-readable label shown in the UI. Full list: System Roles.Custom roles use a generated
idof the formcustom__<slugified-name>__<8 hex chars>(e.g.custom__senior-analyst__a1b2c3d4). You cannot choose this id; copy it fromadminGetAllRolesafter 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:
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:
{ "data": { "adminAddGroupingPolicies": true } }Step 4 — Verify
Confirm the roles are attached to the group:
query VerifyGroupRoles {
adminGroupDetail(groupId: "grp_data_team_123") {
id
name
roles
members {
id
email
}
}
}Expected: roles now contains both assigned Gatekeeper ids:
{
"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:
mutation UnassignRoleFromGroup {
adminRemoveGroupingPolicies(input: {
policies: [
{ subject: "grp_data_team_123", group: "custom__senior-analyst__a1b2c3d4", company: "company_789" }
]
})
}Response:
{ "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:
Open Access Management → Groups tab.
Find the group in the Groups table and choose Manage Roles.
Assign or remove roles in the dialog — each assignment/removal corresponds to an
adminAddGroupingPolicies/adminRemoveGroupingPoliciescall with the group's id assubjectand the Gatekeeper roleidasgroup.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 |
|
|
Create group |
|
|
Update group |
|
|
Delete group |
|
|
Group configuration |
|
|
List group members |
| (service access) |
Add/remove members | membership mutations ( |
|
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):
adminRemoveGroupingPolicieswith the same shapegroupis a Gatekeeper id fromadminGetAllRoles(Analytics_Admin,custom__senior-analyst__a1b2c3d4) — never a Zitadel role nameOne policy entry per role; multiple roles = multiple entries in one call
Requires
admin.user-management.write(User Management Admin); all calls are audit-loggedMembers 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 withadminReloadPoliciesonly if needed
See also: System Roles