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.createdteam.updatedteam_membership.role_changedteam_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.