from uuid import UUID from django.http import Http404, HttpResponse from django.shortcuts import aget_object_or_404 from ninja import Router from ninja.errors import ValidationError from ninja.pagination import paginate from apps.organizations_ext.models import Organization, OrganizationUserRole from apps.shared.types import MeID from apps.teams.models import Team from apps.teams.schema import ProjectTeamSchema from glitchtip.api.permissions import AuthHttpRequest, has_permission from .models import Project, ProjectKey, UserProjectAlert from .schema import ( ProjectIn, ProjectKeyIn, ProjectKeySchema, ProjectOrganizationSchema, ProjectSchema, StrKeyIntValue, ) router = Router() """ GET /api/0/projects/ GET /api/0/projects/{organization_slug}/{project_slug}/ DELETE /api/0/projects/{organization_slug}/{project_slug}/ POST /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/ (See teams) DELETE /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/ (See teams) GET /api/0/projects/{organization_slug}/{team_slug}/keys/ POST /api/0/projects/{organization_slug}/{team_slug}/keys/ GET /api/0/projects/{organization_slug}/{project_slug}/keys/{key_id}/ DELETE /api/0/projects/{organization_slug}/{project_slug}/keys/{key_id}/ GET /api/0/teams/{organization_slug}/{team_slug}/projects/ POST /api/0/teams/{organization_slug}/{team_slug}/projects/ GET /api/0/organizations/{organization_slug}/projects/ """ def get_projects_queryset( user_id: int, organization_slug: str = None, team_slug: str = None ): qs = Project.annotate_is_member( Project.undeleted_objects.filter(organization__users=user_id), user_id ) if organization_slug: qs = qs.filter(organization__slug=organization_slug) if team_slug: qs = qs.filter(teams__slug=team_slug) return qs def get_project_keys_queryset( user_id: int, organization_slug: str, project_slug: str, key_id: UUID | None = None ): qs = ProjectKey.objects.filter( project__organization__users=user_id, project__organization__slug=organization_slug, project__slug=project_slug, ) if key_id: qs = qs.filter(public_key=key_id) return qs @router.get( "projects/", response=list[ProjectOrganizationSchema], by_alias=True, ) @paginate @has_permission(["project:read"]) async def list_projects(request: AuthHttpRequest, response: HttpResponse): """List all projects that a user has access to""" return ( get_projects_queryset(request.auth.user_id) .select_related("organization") .order_by("name") ) @router.get( "projects/{slug:organization_slug}/{slug:project_slug}/", response=ProjectOrganizationSchema, by_alias=True, ) @has_permission(["project:read", "project:write", "project:admin"]) async def get_project( request: AuthHttpRequest, organization_slug: str, project_slug: str ): return await aget_object_or_404( get_projects_queryset(request.auth.user_id, organization_slug).select_related( "organization" ), slug=project_slug, ) @router.put( "projects/{slug:organization_slug}/{slug:project_slug}/", response=ProjectOrganizationSchema, by_alias=True, ) @has_permission(["project:write", "project:admin"]) async def update_project( request: AuthHttpRequest, organization_slug: str, project_slug: str, payload: ProjectIn, ): project = await aget_object_or_404( get_projects_queryset(request.auth.user_id, organization_slug).select_related( "organization" ), slug=project_slug, ) for attr, value in payload.dict().items(): setattr(project, attr, value) await project.asave() return project @router.delete( "projects/{slug:organization_slug}/{slug:project_slug}/", response={204: None}, ) @has_permission(["project:admin"]) async def delete_project( request: AuthHttpRequest, organization_slug: str, project_slug: str ): result, _ = ( await get_projects_queryset(request.auth.user_id, organization_slug) .filter( slug=project_slug, organization__organization_users__role__gte=OrganizationUserRole.ADMIN, ) .adelete() ) if not result: raise Http404 return 204, None @router.get( "teams/{slug:organization_slug}/{slug:team_slug}/projects/", response=list[ProjectSchema], by_alias=True, ) @paginate @has_permission(["project:read"]) async def list_team_projects( request: AuthHttpRequest, response: HttpResponse, organization_slug: str, team_slug: str, ): """List all projects for a given team""" return get_projects_queryset( request.auth.user_id, organization_slug=organization_slug, team_slug=team_slug ).order_by("name") @router.post( "teams/{slug:organization_slug}/{slug:team_slug}/projects/", response={201: ProjectSchema}, by_alias=True, ) @has_permission(["project:write", "project:admin"]) async def create_project( request: AuthHttpRequest, organization_slug: str, team_slug: str, payload: ProjectIn ): """Create a new project given a team and organization""" user_id = request.auth.user_id team = await aget_object_or_404( Team, slug=team_slug, organization__slug=organization_slug, organization__users=user_id, organization__organization_users__role__gte=OrganizationUserRole.ADMIN, ) organization = await aget_object_or_404( Organization, slug=organization_slug, users=user_id, organization_users__role__gte=OrganizationUserRole.ADMIN, ) project = await Project.objects.acreate(organization=organization, **payload.dict()) await project.teams.aadd(team) project = await get_projects_queryset(user_id).aget(id=project.id) return 201, project @router.get( "organizations/{slug:organization_slug}/projects/", response=list[ProjectTeamSchema], by_alias=True, ) @paginate @has_permission(["project:read"]) async def list_organization_projects( request: AuthHttpRequest, response: HttpResponse, organization_slug: str, query: str | None = None, ): """ Fetch list of organizations for a project Contains team information query: Filter on team, ex: ?query=!team:burke-software """ queryset = ( get_projects_queryset(request.auth.user_id, organization_slug=organization_slug) .prefetch_related("teams") .order_by("name") ) # This query param isn't documented in sentry api but exists if query: query_parts = query.split() for query in query_parts: query_part = query.split(":", 1) if len(query_part) == 2: query_name, query_value = query_part if query_name == "team": queryset = queryset.filter(teams__slug=query_value) if query_name == "!team": queryset = queryset.exclude(teams__slug=query_value) return queryset @router.get( "projects/{slug:organization_slug}/{slug:project_slug}/keys/", response=list[ProjectKeySchema], by_alias=True, ) @paginate @has_permission(["project:read", "project:write", "project:admin"]) async def list_project_keys( request: AuthHttpRequest, response: HttpResponse, organization_slug: str, project_slug: str, status: str | None = None, ): """List all DSN keys for a given project""" return get_project_keys_queryset( request.auth.user_id, organization_slug, project_slug ) @router.get( "projects/{slug:organization_slug}/{slug:project_slug}/keys/{uuid:key_id}/", response=ProjectKeySchema, by_alias=True, ) @has_permission(["project:read", "project:write", "project:admin"]) async def get_project_key( request: AuthHttpRequest, organization_slug: str, project_slug: str, key_id: UUID, ): return await aget_object_or_404( get_project_keys_queryset( request.auth.user_id, organization_slug, project_slug, key_id=key_id ) ) @router.put( "projects/{slug:organization_slug}/{slug:project_slug}/keys/{uuid:key_id}/", response=ProjectKeySchema, by_alias=True, ) @has_permission(["project:write", "project:admin"]) async def update_project_key( request: AuthHttpRequest, organization_slug: str, project_slug: str, key_id: UUID, payload: ProjectKeyIn, ): return await aget_object_or_404( get_project_keys_queryset( request.auth.user_id, organization_slug, project_slug, key_id=key_id ) ) @router.post( "projects/{slug:organization_slug}/{slug:project_slug}/keys/", response={201: ProjectKeySchema}, by_alias=True, ) @has_permission(["project:write", "project:admin"]) async def create_project_key( request: AuthHttpRequest, organization_slug: str, project_slug: str, payload: ProjectKeyIn, ): """Create new key for project. Rate limiting not implemented.""" project = await aget_object_or_404( get_projects_queryset(request.auth.user_id, organization_slug), slug=project_slug, ) return 201, await ProjectKey.objects.acreate( project=project, name=payload.name, rate_limit_count=payload.rate_limit.count if payload.rate_limit else None, rate_limit_window=payload.rate_limit.window if payload.rate_limit else None, ) @router.delete( "projects/{slug:organization_slug}/{slug:project_slug}/keys/{uuid:key_id}/", response={204: None}, ) @has_permission(["project:admin"]) async def delete_project_key( request: AuthHttpRequest, organization_slug: str, project_slug: str, key_id: UUID ): result, _ = ( await get_project_keys_queryset( request.auth.user_id, organization_slug, project_slug, key_id=key_id ) .filter( project__organization__organization_users__role__gte=OrganizationUserRole.ADMIN ) .adelete() ) if not result: raise Http404 return 204, None @router.get("/users/{slug:user_id}/notifications/alerts/", response=StrKeyIntValue) async def user_notification_alerts(request: AuthHttpRequest, user_id: MeID): """ Returns dictionary of project_id: status. Now project_id status means it's "default" To update, submit `{project_id: status}` where status is -1 (default), 0, or 1 """ if user_id != request.auth.user_id and user_id != "me": raise Http404 user_id = request.auth.user_id data = {} async for alert in UserProjectAlert.objects.filter(user_id=user_id): data[str(alert.project_id)] = alert.status return data @router.put("/users/{slug:user_id}/notifications/alerts/", response={204: None}) async def update_user_notification_alerts( request: AuthHttpRequest, user_id: MeID, payload: StrKeyIntValue ): if user_id != request.auth.user_id and user_id != "me": raise Http404 user_id = request.auth.user_id items = [x for x in payload.root.items()] if len(items) != 1: raise ValidationError("Invalid alert format, expected one value") project_id, alert_status = items[0] if alert_status not in [1, 0, -1]: raise ValidationError("Invalid status, must be -1, 0, or 1") alert = await UserProjectAlert.objects.filter( user_id=user_id, project_id=project_id ).afirst() if alert and alert_status == -1: await alert.adelete() else: await UserProjectAlert.objects.aupdate_or_create( user_id=user_id, project_id=project_id, defaults={"status": alert_status} ) return 204, None