Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔧 chore(api): published UserEmail Endpoint and Privated Some #79718

Merged
merged 1 commit into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/sentry/api/endpoints/organization_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand All @@ -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:
"""
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/api/endpoints/organization_recent_searches.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,)

Expand Down
10 changes: 10 additions & 0 deletions src/sentry/apidocs/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
]
32 changes: 0 additions & 32 deletions src/sentry/apidocs/examples/organization_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
70 changes: 70 additions & 0 deletions src/sentry/apidocs/examples/user_examples.py
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"isPrimary": True,
"isVerified": True,
},
{
"email": "[email protected]",
"isPrimary": False,
"isVerified": True,
},
],
status_codes=["200"],
response_only=True,
)
]

ADD_SECONDARY_EMAIL = [
OpenApiExample(
"Adds a secondary email",
value=[
{
"email": "[email protected]",
"isPrimary": True,
"isVerified": True,
},
],
status_codes=["200", "201"],
response_only=True,
)
]
7 changes: 7 additions & 0 deletions src/sentry/apidocs/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
100 changes: 67 additions & 33 deletions src/sentry/users/api/endpoints/user_emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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()
Expand 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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/users/api/endpoints/user_emails_confirm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading