From d7b3a5def7a900839378028b1e9649b77666d103 Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Thu, 24 Oct 2024 13:17:13 -0700 Subject: [PATCH] :wrench: chore: published endpoints and privated some --- .../api/endpoints/organization_index.py | 6 +- .../endpoints/organization_recent_searches.py | 4 +- src/sentry/apidocs/build.py | 10 ++ .../apidocs/examples/organization_examples.py | 32 ------ src/sentry/apidocs/examples/user_examples.py | 70 ++++++++++++ src/sentry/apidocs/parameters.py | 7 ++ src/sentry/users/api/endpoints/user_emails.py | 100 ++++++++++++------ .../api/endpoints/user_emails_confirm.py | 2 +- 8 files changed, 160 insertions(+), 71 deletions(-) create mode 100644 src/sentry/apidocs/examples/user_examples.py diff --git a/src/sentry/api/endpoints/organization_index.py b/src/sentry/api/endpoints/organization_index.py index 675a32c13bb308..0cf1493ab8776f 100644 --- a/src/sentry/api/endpoints/organization_index.py +++ b/src/sentry/api/endpoints/organization_index.py @@ -16,7 +16,7 @@ from sentry.api.serializers.models.organization import BaseOrganizationSerializer from sentry.api.serializers.types import OrganizationSerializerResponse from sentry.apidocs.constants import RESPONSE_FORBIDDEN, RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED -from sentry.apidocs.examples.organization_examples import OrganizationExamples +from sentry.apidocs.examples.user_examples import UserExamples from sentry.apidocs.parameters import CursorQueryParam, OrganizationParams from sentry.apidocs.utils import inline_sentry_response_serializer from sentry.auth.superuser import is_active_superuser @@ -55,7 +55,7 @@ def validate_agreeTerms(self, value): return value -@extend_schema(tags=["Organizations"]) +@extend_schema(tags=["Users"]) @region_silo_endpoint class OrganizationIndexEndpoint(Endpoint): publish_status = { @@ -81,7 +81,7 @@ class OrganizationIndexEndpoint(Endpoint): 403: RESPONSE_FORBIDDEN, 404: RESPONSE_NOT_FOUND, }, - examples=OrganizationExamples.LIST_ORGANIZATIONS, + examples=UserExamples.LIST_ORGANIZATIONS, ) def get(self, request: Request) -> Response: """ diff --git a/src/sentry/api/endpoints/organization_recent_searches.py b/src/sentry/api/endpoints/organization_recent_searches.py index f7359d7c35efce..b1b70726b798bc 100644 --- a/src/sentry/api/endpoints/organization_recent_searches.py +++ b/src/sentry/api/endpoints/organization_recent_searches.py @@ -28,8 +28,8 @@ class OrganizationRecentSearchPermission(OrganizationPermission): class OrganizationRecentSearchesEndpoint(OrganizationEndpoint): owner = ApiOwner.UNOWNED publish_status = { - "GET": ApiPublishStatus.UNKNOWN, - "POST": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.PRIVATE, + "POST": ApiPublishStatus.PRIVATE, } permission_classes = (OrganizationRecentSearchPermission,) diff --git a/src/sentry/apidocs/build.py b/src/sentry/apidocs/build.py index 10075e282612ba..edd9f3ddf2bf80 100644 --- a/src/sentry/apidocs/build.py +++ b/src/sentry/apidocs/build.py @@ -164,4 +164,14 @@ def get_old_json_components(filename: str) -> Any: "url": "https://github.com/getsentry/sentry-docs/issues/new/?title=API%20Documentation%20Error:%20/api/integration-platform/&template=api_error_template.md", }, }, + { + "name": "Users", + "x-sidebar-name": "Users", + "description": "Endpoints for Users", + "x-display-description": False, + "externalDocs": { + "description": "Found an error? Let us know.", + "url": "https://github.com/getsentry/sentry-docs/issues/new/?title=API%20Documentation%20Error:%20/api/integration-platform/&template=api_error_template.md", + }, + }, ] diff --git a/src/sentry/apidocs/examples/organization_examples.py b/src/sentry/apidocs/examples/organization_examples.py index 109ae5d434152f..9cf5fca8d1881d 100644 --- a/src/sentry/apidocs/examples/organization_examples.py +++ b/src/sentry/apidocs/examples/organization_examples.py @@ -417,38 +417,6 @@ class OrganizationExamples: ) ] - LIST_ORGANIZATIONS = [ - OpenApiExample( - "List your organizations", - value=[ - { - "avatar": {"avatarType": "letter_avatar", "avatarUuid": None}, - "dateCreated": "2018-11-06T21:19:55.101Z", - "features": [ - "session-replay-video", - "onboarding", - "advanced-search", - "monitor-seat-billing", - "issue-platform", - ], - "hasAuthProvider": False, - "id": "2", - "isEarlyAdopter": False, - "links": { - "organizationUrl": "https://the-interstellar-jurisdiction.sentry.io", - "regionUrl": "https://us.sentry.io", - }, - "name": "The Interstellar Jurisdiction", - "require2FA": False, - "slug": "the-interstellar-jurisdiction", - "status": {"id": "active", "name": "active"}, - } - ], - status_codes=["200"], - response_only=True, - ) - ] - LIST_PROJECTS = [ OpenApiExample( "List an organization's projects", diff --git a/src/sentry/apidocs/examples/user_examples.py b/src/sentry/apidocs/examples/user_examples.py new file mode 100644 index 00000000000000..f6258649d63063 --- /dev/null +++ b/src/sentry/apidocs/examples/user_examples.py @@ -0,0 +1,70 @@ +from drf_spectacular.utils import OpenApiExample + + +class UserExamples: + LIST_ORGANIZATIONS = [ + OpenApiExample( + "List your organizations", + value=[ + { + "avatar": {"avatarType": "letter_avatar", "avatarUuid": None}, + "dateCreated": "2018-11-06T21:19:55.101Z", + "features": [ + "session-replay-video", + "onboarding", + "advanced-search", + "monitor-seat-billing", + "issue-platform", + ], + "hasAuthProvider": False, + "id": "2", + "isEarlyAdopter": False, + "links": { + "organizationUrl": "https://the-interstellar-jurisdiction.sentry.io", + "regionUrl": "https://us.sentry.io", + }, + "name": "The Interstellar Jurisdiction", + "require2FA": False, + "slug": "the-interstellar-jurisdiction", + "status": {"id": "active", "name": "active"}, + } + ], + status_codes=["200"], + response_only=True, + ) + ] + + LIST_USER_EMAILS = [ + OpenApiExample( + "List user emails", + value=[ + { + "email": "billy@sentry.io", + "isPrimary": True, + "isVerified": True, + }, + { + "email": "billybob@sentry.io", + "isPrimary": False, + "isVerified": True, + }, + ], + status_codes=["200"], + response_only=True, + ) + ] + + ADD_SECONDARY_EMAIL = [ + OpenApiExample( + "Adds a secondary email", + value=[ + { + "email": "billybob@sentry.io", + "isPrimary": True, + "isVerified": True, + }, + ], + status_codes=["200", "201"], + response_only=True, + ) + ] diff --git a/src/sentry/apidocs/parameters.py b/src/sentry/apidocs/parameters.py index 8bcc38d34c9bfe..d8c911dff8a6ff 100644 --- a/src/sentry/apidocs/parameters.py +++ b/src/sentry/apidocs/parameters.py @@ -20,6 +20,13 @@ def build_typed_list(type: Any): class GlobalParams: + USER_ID = OpenApiParameter( + name="user_id", + description="The ID of the user the resource belongs to.", + required=True, + type=str, + location="path", + ) ORG_ID_OR_SLUG = OpenApiParameter( name="organization_id_or_slug", description="The ID or slug of the organization the resource belongs to.", diff --git a/src/sentry/users/api/endpoints/user_emails.py b/src/sentry/users/api/endpoints/user_emails.py index 5f6008f4434c4d..00adaf81e74d7c 100644 --- a/src/sentry/users/api/endpoints/user_emails.py +++ b/src/sentry/users/api/endpoints/user_emails.py @@ -2,17 +2,23 @@ from django.db import IntegrityError, router, transaction from django.db.models import Q +from drf_spectacular.utils import extend_schema from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.decorators import sudo_required from sentry.api.serializers import serialize from sentry.api.validators import AllowedEmailField +from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_FORBIDDEN, RESPONSE_NO_CONTENT +from sentry.apidocs.examples.user_examples import UserExamples +from sentry.apidocs.parameters import GlobalParams +from sentry.apidocs.utils import inline_sentry_response_serializer from sentry.users.api.bases.user import UserEndpoint -from sentry.users.api.serializers.useremail import UserEmailSerializer +from sentry.users.api.serializers.useremail import UserEmailSerializer, UserEmailSerializerResponse from sentry.users.models.user import User from sentry.users.models.user_option import UserOption from sentry.users.models.useremail import UserEmail @@ -29,7 +35,7 @@ class DuplicateEmailError(Exception): class EmailValidator(serializers.Serializer[UserEmail]): - email = AllowedEmailField(required=True) + email = AllowedEmailField(required=True, help_text="The email address to add/remove.") def add_email(email: str, user: User) -> UserEmail: @@ -58,23 +64,32 @@ def add_email(email: str, user: User) -> UserEmail: return new_email +@extend_schema(tags=["Users"]) @control_silo_endpoint class UserEmailsEndpoint(UserEndpoint): publish_status = { - "DELETE": ApiPublishStatus.UNKNOWN, - "GET": ApiPublishStatus.UNKNOWN, - "PUT": ApiPublishStatus.UNKNOWN, - "POST": ApiPublishStatus.UNKNOWN, + "DELETE": ApiPublishStatus.PUBLIC, + "GET": ApiPublishStatus.PUBLIC, + "PUT": ApiPublishStatus.PUBLIC, + "POST": ApiPublishStatus.PUBLIC, } - + owner = ApiOwner.UNOWNED + + @extend_schema( + operation_id="List user emails", + parameters=[GlobalParams.USER_ID], + request=None, + responses={ + 200: inline_sentry_response_serializer( + "UserEmailSerializerResponse", list[UserEmailSerializerResponse] + ), + 403: RESPONSE_FORBIDDEN, + }, + examples=UserExamples.LIST_USER_EMAILS, + ) def get(self, request: Request, user: User) -> Response: """ - Get list of emails - `````````````````` - Returns a list of emails. Primary email will have `isPrimary: true` - - :auth required: """ emails = user.emails.all() @@ -84,16 +99,25 @@ def get(self, request: Request, user: User) -> Response: status=200, ) + @extend_schema( + operation_id="Add a secondary email address", + parameters=[GlobalParams.USER_ID], + request=EmailValidator, + responses={ + 200: inline_sentry_response_serializer( + "UserEmailSerializerResponse", list[UserEmailSerializerResponse] + ), + 201: inline_sentry_response_serializer( + "UserEmailSerializerResponse", list[UserEmailSerializerResponse] + ), + 403: RESPONSE_FORBIDDEN, + }, + examples=UserExamples.ADD_SECONDARY_EMAIL, + ) @sudo_required def post(self, request: Request, user: User) -> Response: """ - Adds a secondary email address - `````````````````````````````` - - Adds a secondary email address to account. - - :param string email: email to add - :auth required: + Add a secondary email address to account """ validator = EmailValidator(data=request.data) @@ -125,16 +149,22 @@ def post(self, request: Request, user: User) -> Response: status=201, ) + @extend_schema( + operation_id="Update a primary email address", + parameters=[GlobalParams.USER_ID], + request=EmailValidator, + responses={ + 200: inline_sentry_response_serializer( + "UserEmailSerializerResponse", list[UserEmailSerializerResponse] + ), + 403: RESPONSE_FORBIDDEN, + 400: RESPONSE_BAD_REQUEST, + }, + ) @sudo_required def put(self, request: Request, user: User) -> Response: """ - Updates primary email - ````````````````````` - - Changes primary email - - :param string email: the email to set as primary email - :auth required: + Update a primary email address """ validator = EmailValidator(data=request.data) @@ -222,16 +252,20 @@ def put(self, request: Request, user: User) -> Response: status=200, ) + @extend_schema( + operation_id="Remove an email address", + parameters=[GlobalParams.USER_ID], + request=UserEmailSerializer, + responses={ + 204: RESPONSE_NO_CONTENT, + 403: RESPONSE_FORBIDDEN, + 400: RESPONSE_BAD_REQUEST, + }, + ) @sudo_required def delete(self, request: Request, user: User) -> Response: """ - Removes an email from account - ````````````````````````````` - - Removes an email from account, can not remove primary email - - :param string email: email to remove - :auth required: + Removes an email associated with the user account """ validator = EmailValidator(data=request.data) if not validator.is_valid(): diff --git a/src/sentry/users/api/endpoints/user_emails_confirm.py b/src/sentry/users/api/endpoints/user_emails_confirm.py index 8817e0b8a49f90..8de526f6c66202 100644 --- a/src/sentry/users/api/endpoints/user_emails_confirm.py +++ b/src/sentry/users/api/endpoints/user_emails_confirm.py @@ -38,7 +38,7 @@ class EmailSerializer(serializers.Serializer[UserEmail]): @control_silo_endpoint class UserEmailsConfirmEndpoint(UserEndpoint): publish_status = { - "POST": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.PRIVATE, } rate_limits = { "POST": {