Files

349 lines
11 KiB
Python

from typing import Optional
from django.db.models import Count, Exists, OuterRef, Prefetch
from django.http import Http404, HttpResponse
from django.shortcuts import aget_object_or_404
from ninja import Router
from ninja.errors import HttpError
from ninja.pagination import paginate
from apps.organizations_ext.constants import OrganizationUserRole
from apps.organizations_ext.models import Organization, OrganizationUser
from apps.projects.models import Project
from apps.shared.types import MeID
from glitchtip.api.authentication import AuthHttpRequest
from glitchtip.api.permissions import has_permission
from .models import Team
from .schema import ProjectTeamSchema, TeamIn, TeamProjectSchema, TeamSchema
router = Router()
"""
OSS Sentry supported
GET /teams/{org}/{team}/
PUT /teams/{org}/{team}/
DELETE /teams/{org}/{team}/
GET /teams/{org}/{team}/members/ (See organizations)
GET /teams/{org}/{team}/projects/ (See projects)
GET /teams/{org}/{team}/stats/ (Not implemented)
GET /organizations/{org}/teams/
POST /organizations/{org}/teams/
POST /organizations/{org}/members/{me|member_id}/teams/{team}/ (join)
DELETE /organizations/{org}/members/{me|member_id}/teams/{team}/ (leave)
GET /api/0/projects/{organization_slug}/{project_slug}/teams/ (Not documented)
POST /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/
DELETE /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/
"""
def get_team_queryset(
organization_slug: str,
team_slug: Optional[str] = None,
project_slug: Optional[str] = None,
user_id: Optional[int] = None,
id: Optional[int] = None,
add_details=False,
add_projects=False,
):
qs = Team.objects.filter(organization__slug=organization_slug)
if team_slug:
qs = qs.filter(slug=team_slug)
if project_slug:
qs = qs.filter(projects__slug=project_slug)
if id:
qs = qs.filter(id=id)
if user_id:
qs = qs.filter(organization__users=user_id)
if add_details:
qs = qs.annotate(
is_member=Exists(
OrganizationUser.objects.filter(
teams=OuterRef("pk"), user_id=user_id
)
),
member_count=Count("members"),
)
if add_projects:
qs = qs.prefetch_related(
Prefetch(
"projects",
queryset=Project.objects.annotate(
is_member=Exists(
OrganizationUser.objects.filter(
teams__members=OuterRef("pk"), user_id=user_id
)
),
),
)
)
return qs
@router.get(
"teams/{slug:organization_slug}/{slug:team_slug}/",
response=TeamProjectSchema,
by_alias=True,
)
@has_permission(["team:read", "team:write", "team:admin"])
async def get_team(request: AuthHttpRequest, organization_slug: str, team_slug: str):
user_id = request.auth.user_id
return await aget_object_or_404(
get_team_queryset(
organization_slug,
user_id=user_id,
team_slug=team_slug,
add_details=True,
add_projects=True,
)
)
@router.put(
"teams/{slug:organization_slug}/{slug:team_slug}/",
response=TeamProjectSchema,
by_alias=True,
)
@has_permission(["team:write", "team:admin"])
async def update_team(
request: AuthHttpRequest, organization_slug: str, team_slug: str, payload: TeamIn
):
user_id = request.auth.user_id
team = await aget_object_or_404(
get_team_queryset(
organization_slug,
user_id=user_id,
team_slug=team_slug,
add_details=True,
add_projects=True,
)
)
team.slug = payload.slug
await team.asave()
return team
@router.delete("teams/{slug:organization_slug}/{slug:team_slug}/", response={204: None})
@has_permission(["team:admin"])
async def delete_team(request: AuthHttpRequest, organization_slug: str, team_slug: str):
result, _ = (
await get_team_queryset(
organization_slug, team_slug=team_slug, user_id=request.auth.user_id
)
.filter(
organization__organization_users__role__gte=OrganizationUserRole.ADMIN,
)
.adelete()
)
if not result:
raise Http404
return 204, None
@router.get(
"/organizations/{slug:organization_slug}/teams/",
response=list[TeamProjectSchema],
by_alias=True,
)
@paginate
@has_permission(
["team:read", "team:write", "team:admin", "org:read", "org:write", "org:admin"]
)
async def list_teams(
request: AuthHttpRequest, response: HttpResponse, organization_slug: str
):
return get_team_queryset(
organization_slug,
user_id=request.auth.user_id,
add_details=True,
add_projects=True,
)
@router.post(
"/organizations/{slug:organization_slug}/teams/",
response={201: TeamProjectSchema},
by_alias=True,
)
@has_permission(["team:write", "team:admin", "org:admin", "org:write"])
async def create_team(
request: AuthHttpRequest, organization_slug: str, payload: TeamIn
):
user_id = request.auth.user_id
organization = await aget_object_or_404(
Organization,
slug=organization_slug,
users=user_id,
organization_users__role__gte=OrganizationUserRole.ADMIN,
)
team = await Team.objects.acreate(organization=organization, slug=payload.slug)
org_user = await organization.organization_users.filter(user=user_id).afirst()
await team.members.aadd(org_user)
return await get_team_queryset(
organization_slug,
user_id=user_id,
id=team.id,
add_details=True,
add_projects=True,
).aget()
async def modify_member_for_team(
organization_slug: str,
member_id: MeID,
team_slug: str,
user_id: int,
add_member=True,
):
team = await aget_object_or_404(
get_team_queryset(
organization_slug,
user_id=user_id,
team_slug=team_slug,
add_details=True,
add_projects=True,
)
)
org_user_qs = OrganizationUser.objects.filter(
organization__slug=organization_slug
).select_related("organization")
if member_id == "me":
org_user = await org_user_qs.aget(user_id=user_id)
else:
org_user = await aget_object_or_404(org_user_qs, id=member_id)
open_membership = org_user.organization.open_membership
is_self = org_user.user_id == user_id
if not (open_membership and is_self):
in_team = await team.members.filter(user_id=user_id).aexists()
if in_team:
required_role = OrganizationUserRole.ADMIN
else:
required_role = OrganizationUserRole.MANAGER
if not await OrganizationUser.objects.filter(
user_id=user_id, organization=org_user.organization, role__gte=required_role
).aexists():
raise HttpError(403, "Must be admin to modify teams")
if add_member:
await team.members.aadd(org_user)
team.is_member = True
else:
await team.members.aremove(org_user)
return team
@router.post(
"/organizations/{slug:organization_slug}/members/{slug:member_id}/teams/{slug:team_slug}/",
response={201: TeamProjectSchema},
by_alias=True,
)
@has_permission(["team:write", "team:admin"])
async def add_member_to_team(
request: AuthHttpRequest, organization_slug: str, member_id: MeID, team_slug: str
):
return 201, await modify_member_for_team(
organization_slug, member_id, team_slug, request.auth.user_id, True
)
@router.delete(
"/organizations/{slug:organization_slug}/members/{slug:member_id}/teams/{slug:team_slug}/",
response=TeamProjectSchema,
by_alias=True,
)
@has_permission(["team:write", "team:admin"])
async def delete_member_from_team(
request: AuthHttpRequest, organization_slug: str, member_id: MeID, team_slug: str
):
return await modify_member_for_team(
organization_slug, member_id, team_slug, request.auth.user_id, False
)
@router.get(
"/projects/{slug:organization_slug}/{slug:project_slug}/teams/",
response=list[TeamSchema],
by_alias=True,
)
@paginate
@has_permission(
["team:read", "team:write", "team:admin", "org:read", "org:write", "org:admin"]
)
async def list_project_teams(
request: AuthHttpRequest,
response: HttpResponse,
organization_slug: str,
project_slug: str,
):
return get_team_queryset(
organization_slug,
user_id=request.auth.user_id,
project_slug=project_slug,
add_details=True,
)
@router.post(
"/projects/{slug:organization_slug}/{slug:project_slug}/teams/{slug:team_slug}/",
response={201: ProjectTeamSchema},
by_alias=True,
)
@has_permission(["project.write", "project:admin"])
async def add_team_to_project(
request: AuthHttpRequest, organization_slug: str, project_slug: str, team_slug: str
):
"""Add team to project"""
user_id = request.auth.user_id
project = await aget_object_or_404(
Project,
slug=project_slug,
organization__slug=organization_slug,
organization__users=user_id,
organization__organization_users__role__gte=OrganizationUserRole.MANAGER,
)
team = await aget_object_or_404(
get_team_queryset(organization_slug, team_slug=team_slug)
)
await project.teams.aadd(team)
project = await (
Project.annotate_is_member(Project.objects, user_id)
.prefetch_related("teams")
.aget(id=project.id)
)
return 201, project
@router.delete(
"/projects/{slug:organization_slug}/{slug:project_slug}/teams/{slug:team_slug}/",
response=ProjectTeamSchema,
by_alias=True,
)
@has_permission(["project.write", "project:admin"])
async def delete_team_from_project(
request: AuthHttpRequest, organization_slug: str, project_slug: str, team_slug: str
):
"""Remove team from project"""
user_id = request.auth.user_id
team = await aget_object_or_404(
get_team_queryset(
organization_slug, project_slug=project_slug, team_slug=team_slug
)
)
project = await aget_object_or_404(
Project,
slug=project_slug,
organization__slug=organization_slug,
organization__users=user_id,
organization__organization_users__role__gte=OrganizationUserRole.MANAGER,
)
await project.teams.aremove(team)
return await (
Project.annotate_is_member(Project.objects, user_id)
.prefetch_related("teams")
.aget(id=project.id)
)