Skip to content

Teams — services

Responsibilities

The services in the Teams app contain the core business logic for:

  • team visibility within an organization
  • team membership visibility
  • team creation and updates
  • team activation/deactivation
  • membership creation and updates
  • membership activation/deactivation
  • enforcing team/org consistency
  • managing the “primary team” rule
  • delegating broad authorization checks to the policy layer
  • writing audit log entries for team and membership mutations

They are responsible for keeping:

  • views thin
  • query logic centralized
  • validation rules reusable
  • cross-org safety explicit
  • permission rules consistent
  • workflow rules hard to bypass accidentally

They are not responsible for:

  • HTTP request/response handling
  • serializer field validation
  • URL routing
  • admin behavior

Main service modules

teams.services.team

Handles team-level operations, safe org-scoped retrieval, listing, management validation, and team mutation workflows.

teams.services.membership

Handles membership-level operations, team/member visibility, membership mutation workflows, and primary-team logic.


Architecture note: policy layer vs service layer

The Teams app now uses a layered authorization model.

Policy layer

Located in: - core.policies.orgs - core.policies.teams

The policy layer answers broad questions such as: - who may view all teams in an org - who may manage teams in an org - who may manage team memberships - who may view a specific team

Service layer

Located in: - teams.services.team - teams.services.membership

The service layer answers workflow and integrity questions such as: - is this team in the current org - is this user allowed to manage this workflow - does the target user belong to the team’s org - is the target membership active - is the primary-team invariant still valid

This split keeps authorization centralized while preserving explicit business rules in services.


teams.services.team

Responsibilities

This module is responsible for:

  • determining whether a user may manage teams
  • retrieving a single team in an org safely
  • listing teams in an org
  • listing the current user’s teams
  • creating teams
  • updating teams
  • activating/deactivating teams
  • writing audit entries for team mutations

Broad authorization is delegated to: - core.policies.orgs.can_manage_teams(...)


can_manage_teams(user, org)

Returns whether a user may manage teams in the org.

Inputs - user - org

Behavior - requires authentication - allows superuser - otherwise delegates to core.policies.orgs.can_manage_teams(...)

Returns - Boolean

This is a service-level convenience helper built on top of the policy layer.


validate_team_management(actor, org)

Validates whether the actor may manage teams in the org.

Inputs - actor - org

Behavior - requires authentication - delegates broad authorization to the policy layer - raises if actor does not have team-management capability

Raises - TeamPermissionError

Used by - create/update/deactivate/reactivate team flows


get_team_in_org(team_id, org, active_only=False)

Safely resolves a team within an organization.

Inputs - team_id - org - active_only

Behavior - restricts lookup to the given org - optionally restricts to active teams only

Returns - Team

Raises - TeamNotFoundError

Why it exists Prevents accidental cross-org team access.


list_all_teams_in_org(org, active_only=True)

Lists all teams for an organization.

Inputs - org - active_only

Behavior - filters by org - optionally filters by active teams only - orders by name, id

Returns - QuerySet of Team

Used by - “all teams” endpoints for users with org-level team visibility


list_my_teams_in_org(user, org, active_only=True)

Lists the teams the current user belongs to in a given org.

Inputs - user - org - active_only

Behavior - filters through active TeamMembership - restricts to current org - uses distinct() to avoid duplicates - orders by name, id

Returns - QuerySet of Team

Used by - “my teams” endpoint


create_team(org, name, is_active=True, actor=None, request=None)

Creates a new team in an organization.

Inputs - org - name - is_active - actor (optional, for audit attribution) - request (optional, for audit metadata)

Behavior - strips and validates team name - ensures team name is unique within the org - creates the team - writes audit entry: - team.created

Returns - Team

Raises - TeamValidationError

Rules enforced - team name cannot be blank - team name must be unique within the org


update_team(team, data, actor=None, request=None)

Updates allowed fields on a team.

Inputs - team - data - actor (optional, for audit attribution) - request (optional, for audit metadata)

Allowed fields - name - is_active

Behavior - rejects unknown fields - validates non-empty name - prevents duplicate team names within the same org - tracks field-level changes - no-ops if nothing changed - runs full_clean() before save - writes audit entry when changed: - team.updated

Returns - updated Team

Raises - TeamValidationError


deactivate_team(team, actor=None, request=None)

Deactivates a team.

Inputs - team - actor (optional, for audit attribution) - request (optional, for audit metadata)

Behavior - no-op if already inactive - sets is_active=False - writes audit entry when changed: - team.deactivated

Returns - updated Team

Used by - team deactivation endpoint


reactivate_team(team, actor=None, request=None)

Reactivates a team.

Inputs - team - actor (optional, for audit attribution) - request (optional, for audit metadata)

Behavior - no-op if already active - sets is_active=True - writes audit entry when changed: - team.reactivated

Returns - updated Team

Used by - team reactivation endpoint


Exceptions in teams.services.team

TeamError

Base exception for team service errors.

TeamNotFoundError

Raised when a team cannot be found within the requested org scope.

TeamPermissionError

Raised when a user is not allowed to perform a team-level action.

TeamValidationError

Raised when team creation or update input is invalid.


teams.services.membership

Responsibilities

This module is responsible for:

  • retrieving memberships safely in org scope
  • validating whether a user may see or manage team members
  • listing team members
  • listing teammates of the current user
  • listing visible membership pairs
  • listing visible membership objects
  • resolving a user for membership creation
  • adding users to teams
  • updating team membership roles
  • managing primary team state
  • activating/deactivating memberships
  • ensuring team membership is valid for the org
  • writing audit entries for membership mutations

Broad authorization and visibility are delegated to: - core.policies.orgs.can_manage_team_memberships(...) - core.policies.teams.can_view_team(...)


can_manage_team_members(actor, org)

Returns whether the actor may manage team memberships in the org.

Inputs - actor - org

Behavior - requires authentication - allows superuser - otherwise delegates to core.policies.orgs.can_manage_team_memberships(...)

Returns - Boolean

Used by - membership management endpoints


validate_team_membership_management(actor, org)

Validates whether the actor may manage team memberships in the org.

Inputs - actor - org

Behavior - requires authentication - delegates broad authorization to the policy layer - raises if actor does not have team-membership-management capability

Raises - TeamMembershipPermissionError

Used by - add/update/deactivate/reactivate membership flows


get_user_for_team_membership(user_id)

Resolves an active user for membership creation or update flows.

Inputs - user_id

Behavior - restricts to active users

Returns - User

Raises - TeamMembershipValidationError

Used by - add-user-to-team endpoint


get_team_membership(user, team)

Returns a membership for a user in a given team.

Inputs - user - team

Returns - TeamMembership or None


get_active_team_membership(user, team)

Returns the active membership for a user in a given team.

Inputs - user - team

Returns - TeamMembership or None


get_membership_by_id_in_org(membership_id, org)

Resolves a membership within a specific org.

Inputs - membership_id - org

Behavior - restricts lookup through team__org=org - uses select_related("user", "team", "team__org")

Returns - TeamMembership

Raises - TeamMembershipNotFoundError

Why it exists Prevents cross-org membership access.


user_is_in_team(user, team)

Checks whether a user has an active membership in a team.

Inputs - user - team

Returns - Boolean


list_team_members(team)

Lists active members of a team.

Inputs - team

Behavior - returns only active memberships - excludes inactive users - uses select_related("user") - orders by user identity fields

Returns - QuerySet of TeamMembership

Used by - “members by team” endpoint


list_my_team_member_options(user, org)

Lists the current user’s teammates across all their teams in the current org.

Inputs - user - org

Behavior - gets the team IDs for the user in the org - gets all user IDs in those teams - returns matching active OrgMembership rows - uses select_related("user")

Returns - QuerySet of OrgMembership

Used by - “my team members” endpoint

Why it uses OrgMembership This makes it easy to include org-level role information for teammate option lists.


can_view_all_team_memberships(actor, org)

Returns whether the actor may view all team memberships in the org.

Inputs - actor - org

Behavior - requires authentication - allows superuser - otherwise delegates to org-level team-membership capability

Returns - Boolean

This helper keeps membership visibility queries readable.


list_visible_membership_pairs_in_org(actor, org)

Returns lightweight visible team-user membership pairs in the org.

Inputs - actor - org

Behavior - users with org-level team-membership visibility capability see all active memberships in the org - other users only see memberships for teams they belong to - returns .only("team_id", "user_id")

Returns - QuerySet of TeamMembership

Used by - membership graph/pair endpoints


list_visible_memberships_in_org(actor, org)

Returns visible full team memberships in the org.

Inputs - actor - org

Behavior - users with org-level team-membership visibility capability see all active memberships in the org - other users only see memberships for teams they belong to - uses select_related("user", "team") - orders by team and user identity

Returns - QuerySet of TeamMembership

Used by - rich membership listing endpoints


validate_team_visibility(actor, team)

Checks whether the actor may see members of a team.

Inputs - actor - team

Behavior - requires authentication - delegates team visibility decision to core.policies.teams.can_view_team(...) - allows users with org-level team visibility capability - allows users who are active members of the team - rejects everyone else

Raises - TeamMembershipPermissionError

Used by - “members by team” endpoint


_ensure_user_in_team_org(user, team)

Internal helper that ensures a user is an active member of the team’s organization.

Inputs - user - team

Behavior - requires an active OrgMembership in team.org

Raises - TeamMembershipValidationError

Why it matters Prevents invalid data such as assigning a user to a team in an org they do not belong to.


add_user_to_team(team, user, role=..., is_primary=False, is_active=True, actor=None, request=None)

Adds a user to a team, or updates an existing membership.

Inputs - team - user - role - is_primary - is_active - actor (optional, for audit attribution) - request (optional, for audit metadata)

Behavior - ensures the user belongs to the team’s org - creates or updates the membership - writes audit entry: - team_membership.added when created - team_membership.updated when updated - applies is_primary via set_primary_team_membership(...) if requested

Returns - TeamMembership

Raises - TeamMembershipValidationError


update_team_membership_role(membership, role, actor=None, request=None)

Updates the role of a team membership.

Inputs - membership - role - actor (optional, for audit attribution) - request (optional, for audit metadata)

Behavior - validates that the role is in TeamMembership.Role.choices - no-ops if unchanged - saves updated role - writes audit entry when changed: - team_membership.role_changed

Returns - updated TeamMembership

Raises - TeamMembershipValidationError


set_primary_team_membership(membership, actor=None, request=None)

Sets a membership as the user’s primary team membership.

Inputs - membership - actor (optional, for audit attribution) - request (optional, for audit metadata)

Behavior - rejects inactive memberships - clears is_primary on any other membership for the same user - sets the target membership as primary - writes audit entry when changed: - team_membership.primary_set

Returns - updated TeamMembership

Raises - TeamMembershipValidationError

Important rule Only one primary team membership may exist per user.


clear_primary_team_membership(membership, actor=None, request=None)

Clears the primary-team flag from a membership.

Inputs - membership - actor (optional, for audit attribution) - request (optional, for audit metadata)

Behavior - clears is_primary if currently set - writes audit entry when changed: - team_membership.primary_cleared

Returns - updated TeamMembership


deactivate_team_membership(membership, actor=None, request=None)

Deactivates a team membership.

Inputs - membership - actor (optional, for audit attribution) - request (optional, for audit metadata)

Behavior - no-op if already inactive - clears is_primary if necessary - sets is_active=False - writes audit entry when changed: - team_membership.deactivated

Returns - updated TeamMembership


reactivate_team_membership(membership, actor=None, request=None)

Reactivates a team membership.

Inputs - membership - actor (optional, for audit attribution) - request (optional, for audit metadata)

Behavior - no-op if already active - re-checks that the user still belongs to the team’s org - sets is_active=True - writes audit entry when changed: - team_membership.reactivated

Returns - updated TeamMembership

Raises - TeamMembershipValidationError


Exceptions in teams.services.membership

TeamMembershipError

Base exception for team membership service errors.

TeamMembershipNotFoundError

Raised when a membership cannot be found in the requested org scope.

TeamMembershipPermissionError

Raised when a user may not view or manage a team/membership.

TeamMembershipValidationError

Raised when a membership change is invalid.

Examples: - assigning user to a team outside their org - setting inactive membership as primary - using an invalid team role - resolving a missing user for membership management


Service relationships

flowchart TD
    TeamService["teams.services.team"]
    MembershipService["teams.services.membership"]
    OrgPolicies["core.policies.orgs"]
    TeamPolicies["core.policies.teams"]
    Audit["core.audit.log_audit"]

    Team["Team"]
    TeamMembership["TeamMembership"]
    OrgMembership["OrgMembership"]
    Organization["Organization"]
    User["User"]

    TeamService --> OrgPolicies
    TeamService --> Audit
    TeamService --> Team
    TeamService --> Organization

    MembershipService --> OrgPolicies
    MembershipService --> TeamPolicies
    MembershipService --> Audit
    MembershipService --> TeamMembership
    MembershipService --> Team
    MembershipService --> OrgMembership
    MembershipService --> Organization
    MembershipService --> User
sequenceDiagram
    participant View as AllTeamsInOrgView
    participant Service as teams.services.team
    participant Model as Team/OrgMembership

    View->>Service: is_privileged_in_org(user, org)
    Service->>Model: resolve org role
    Model-->>Service: role
    Service-->>View: privileged / not privileged

    View->>Service: list_all_teams_in_org(org)
    Service->>Model: query teams
    Model-->>Service: teams
    Service-->>View: teams

Call flow examples

Team visibility flkow

sequenceDiagram
    participant View as AllTeamsInOrgView
    participant Policy as core.policies.orgs
    participant Service as teams.services.team
    participant Model as Team

    View->>Policy: can_view_all_teams(user, org)
    Policy-->>View: allowed / denied
    View->>Service: list_all_teams_in_org(org)
    Service->>Model: query teams
    Model-->>Service: teams
    Service-->>View: teams

Team management permission flow


sequenceDiagram
    participant View as TeamCreateView / TeamUpdateView
    participant Service as teams.services.team
    participant Policy as core.policies.orgs

    View->>Service: validate_team_management(actor, org)
    Service->>Policy: can_manage_teams(user, org)
    Policy-->>Service: allowed / denied
    Service-->>View: allowed / TeamPermissionError

primary team switch flow

sequenceDiagram
    participant Service as teams.services.membership
    participant Model as TeamMembership
    participant Audit as core.audit.log_audit

    Service->>Model: clear existing primary memberships for user
    Service->>Model: set target membership is_primary=True
    Model-->>Service: updated membership
    Service->>Audit: team_membership.primary_set

Add user to team flow

sequenceDiagram
    participant Service as teams.services.membership
    participant User as User
    participant OrgMembership as OrgMembership
    participant TeamMembership as TeamMembership
    participant Audit as core.audit.log_audit

    Service->>User: resolve active user by id
    Service->>OrgMembership: verify user belongs to team.org
    OrgMembership-->>Service: valid / invalid
    Service->>TeamMembership: get_or_create membership
    Service->>Service: optionally set primary team
    Service->>Audit: team_membership.added / updated
    TeamMembership-->>Service: updated membership

Key design notes

1. Teams use policy-driven authorization

The Teams app no longer treats raw org-role checks as the main authorization layer.

Instead:

  • the policy layer decides broad capabilities such as:
  • who may view all teams
  • who may manage teams
  • who may manage team memberships
  • team membership determines local team visibility
  • the service layer enforces workflow rules and data integrity

This keeps authorization centralized while preserving explicit business rules in services.


2. Membership visibility is capability-sensitive

There are two visibility modes:

  • users with org-level team-membership visibility capability can see all memberships in the org
  • other users only see memberships for teams they belong to

This rule is centralized in the membership service and backed by the policy layer.


3. Primary team is enforced in services and DB

The one-primary-team rule is protected in two places:

  • DB constraint
  • service-layer switching logic

This is the right pattern because it protects both:

  • correctness
  • developer ergonomics

4. Org membership is a prerequisite for team membership

A user may not belong to a team unless they are an active member of that team’s org.

This prevents cross-org leakage and invalid staffing state.


5. Team and membership mutations are audited

Write operations in the Teams app emit audit events through core.audit.log_audit(...).

Examples:

  • team.created
  • team.updated
  • team_membership.role_changed
  • team_membership.primary_set

This improves:

  • traceability
  • accountability
  • operational debugging
  • future audit-history UI support

6. Management checks stay centralized

Team and membership management permissions have explicit helpers:

  • validate_team_management(...)
  • validate_team_membership_management(...)

This reduces duplication in views and makes future endpoint expansion safer.