123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- 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
|