Problem
Group membership currently uses direct group#member@user relations, while organization and project membership use policies (role bindings). This inconsistency means groups require special handling.
Current State
| Resource |
How membership works |
| Organization |
Policies (role bindings) |
| Project |
Policies (role bindings) |
| Group |
Direct group#member@user relation |
When a user is added to a group, we create both a policy AND a direct relation.
Source: core/group/service.go:168-191
// AddMember adds a subject(user) to group as member
func (s Service) AddMember(ctx context.Context, groupID string, principal authenticate.Principal) error {
// first create a policy for the user as member of the group
if err := s.addMemberPolicy(ctx, groupID, principal); err != nil {
return err
}
// then create a relation between group and user as member
rel := relation.Relation{
Object: relation.Object{
ID: groupID,
Namespace: schema.GroupNamespace,
},
Subject: relation.Subject{
ID: principal.ID,
Namespace: principal.Type,
},
RelationName: schema.MemberRelationName,
}
if _, err := s.relationService.Create(ctx, rel); err != nil {
return err
}
return nil
}
Same pattern exists for addOwner at lines 193-221.
Proposed State
| Resource |
How membership works |
| Organization |
Policies (role bindings) |
| Project |
Policies (role bindings) |
| Group |
Policies (role bindings) |
Group membership becomes computed from policies, just like org and project.
How it works
Schema change:
// Before
definition group {
relation member: app/user // direct relation
permission get = ... + member
}
// After
definition group {
relation granted: app/rolebinding
permission members = granted->app_group_member // computed from policies
permission get = ...
}
SpiceDB resolves group members by:
- Find all role bindings granted to the group
- Filter to those with "Group Member" role
- Return their bearers
Tested in AuthZed Playground - this approach works.
Benefits
| Benefit |
Description |
| Consistency |
All resources (org, project, group) use the same pattern |
| Single source of truth |
Membership determined by policies only, no sync issues |
| Simpler code |
Remove direct relation management for groups |
| SDK simplicity |
Same role-based API for all resources |
Potential Downsides
| Concern |
Impact |
Mitigation |
| Query complexity |
Membership is computed, not direct lookup |
SpiceDB optimizes and caches these |
| Schema migration |
Need to update SpiceDB schema |
Can be done incrementally |
| Breaking change |
External systems using group#member relation |
Document in release notes |
Code Changes
- Update
base_schema.zed - change group#member from relation to computed permission
core/group/service.go - AddMember: remove relationService.Create call, keep only policy
core/group/service.go - addOwner: remove relationService.Create call, keep only policy
- Update references from
group#member to group#members (the computed permission)
Problem
Group membership currently uses direct
group#member@userrelations, while organization and project membership use policies (role bindings). This inconsistency means groups require special handling.Current State
group#member@userrelationWhen a user is added to a group, we create both a policy AND a direct relation.
Source:
core/group/service.go:168-191Same pattern exists for
addOwnerat lines 193-221.Proposed State
Group membership becomes computed from policies, just like org and project.
How it works
Schema change:
SpiceDB resolves group members by:
Tested in AuthZed Playground - this approach works.
Benefits
Potential Downsides
group#memberrelationCode Changes
base_schema.zed- changegroup#memberfrom relation to computed permissioncore/group/service.go-AddMember: removerelationService.Createcall, keep only policycore/group/service.go-addOwner: removerelationService.Createcall, keep only policygroup#membertogroup#members(the computed permission)