Skip to content

Use computed permissions for group membership instead of direct relations #1479

@whoAbhishekSah

Description

@whoAbhishekSah

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:

  1. Find all role bindings granted to the group
  2. Filter to those with "Group Member" role
  3. 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

  1. Update base_schema.zed - change group#member from relation to computed permission
  2. core/group/service.go - AddMember: remove relationService.Create call, keep only policy
  3. core/group/service.go - addOwner: remove relationService.Create call, keep only policy
  4. Update references from group#member to group#members (the computed permission)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions