Skip to content

Groups & Invitations

NeoZen provides a group management system distinct from RBAC roles:

  • Roles grant permissions
  • Groups organize subjects (departments, teams, projects)

Groups

from neozen.core.groups import GroupService, GroupRole

service = GroupService(
    group_repository=group_repo,
    membership_repository=membership_repo,
)

# Create a group with the creator as owner
group = await service.create_group(
    name="engineering",
    description="Engineering team",
    owner_id="user_123",
)

# Add members
await service.add_member(
    group.id,
    "user_456",
    role=GroupRole.MEMBER,
    actor_id="user_123",  # must be admin/owner
)

# Change role (only owners can grant ownership)
await service.change_role(
    group.id,
    "user_456",
    GroupRole.ADMIN,
    actor_id="user_123",
)

# List members
members = await service.list_members(group.id)

# List a subject's groups
my_groups = await service.list_groups_for_subject("user_456")

# Remove (last owner cannot be removed)
await service.remove_member(group.id, "user_456")

Membership Roles

Role Capabilities
owner Full control: manage members, change roles (including ownership), delete the group
admin Manage members, change roles (except ownership)
member Regular member, no management rights

Permission Rules

  • Adding/removing members and changing roles requires admin or owner
  • Only owners can grant or revoke ownership
  • The last owner of a group cannot be removed
  • Subjects can always remove themselves

Invitations

The invitation system lets admins invite users via email with pre-assigned groups and roles:

from neozen.core.groups import InvitationConfig, InvitationService

service = InvitationService(
    config=InvitationConfig(
        accept_url_template="https://app.example.com/invite?token={token}",
    ),
    repository=invitation_repo,
    token_hash_key="secret",
)

# Create invitation
invitation, token = await service.create_invitation(
    email="newuser@example.com",
    invited_by="admin_1",
    group_ids=["grp_engineering"],
    role_names=["user"],
    message="Welcome to the team!",
)

# Send the email (use NotificationService or similar)
url = f"https://app.example.com/invite?token={token}"
# email_sender.send(...)

# Later: user clicks the link
details = await service.accept_invitation(token, accepted_by=new_user_id)
# details.email, details.group_ids, details.role_names
# Now apply the group memberships and role assignments

Invitation Lifecycle

Status Description
pending Created, not yet acted upon
accepted Successfully accepted
declined Explicitly declined by recipient
expired Token lifetime exceeded
revoked Cancelled by an admin

Security

  • Tokens use the standard NeoZen format with HMAC-SHA256 hashing
  • Only the hash is stored in the database
  • Single-use: accepting marks the invitation as accepted
  • Default lifetime: 7 days
  • Email addresses are lowercased for case-insensitive matching