Browse Source

feat(auth): API endpoint to rotate client secrets (#53124)

Basic endpoint that rotates an `ApiApplication`s client secret. Untested
for now due to time constraints, will come back and add tests + some
more TLC on July 19th.
Eric Hasegawa 1 year ago
parent
commit
c94e7ed78d

+ 27 - 0
src/sentry/api/endpoints/api_application_rotate_secret.py

@@ -0,0 +1,27 @@
+from rest_framework.authentication import SessionAuthentication
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.request import Request
+from rest_framework.response import Response
+
+from sentry.api.base import Endpoint, control_silo_endpoint
+from sentry.api.exceptions import ResourceDoesNotExist
+from sentry.api.serializers import serialize
+from sentry.models import ApiApplication, ApiApplicationStatus
+from sentry.models.apiapplication import generate_token
+
+
+@control_silo_endpoint
+class ApiApplicationRotateSecretEndpoint(Endpoint):
+    authentication_classes = (SessionAuthentication,)
+    permission_classes = (IsAuthenticated,)
+
+    def post(self, request: Request, app_id) -> Response:
+        try:
+            api_application = ApiApplication.objects.get(
+                owner_id=request.user.id, client_id=app_id, status=ApiApplicationStatus.active
+            )
+        except ApiApplication.DoesNotExist:
+            raise ResourceDoesNotExist
+        new_token = generate_token()
+        api_application.update(client_secret=new_token)
+        return Response(serialize({"clientSecret": new_token}))

+ 6 - 0
src/sentry/api/urls.py

@@ -107,6 +107,7 @@ from .endpoints.accept_organization_invite import AcceptOrganizationInvite
 from .endpoints.accept_project_transfer import AcceptProjectTransferEndpoint
 from .endpoints.admin_project_configs import AdminRelayProjectConfigsEndpoint
 from .endpoints.api_application_details import ApiApplicationDetailsEndpoint
+from .endpoints.api_application_rotate_secret import ApiApplicationRotateSecretEndpoint
 from .endpoints.api_applications import ApiApplicationsEndpoint
 from .endpoints.api_authorizations import ApiAuthorizationsEndpoint
 from .endpoints.api_tokens import ApiTokensEndpoint
@@ -2687,6 +2688,11 @@ urlpatterns = [
         ApiApplicationDetailsEndpoint.as_view(),
         name="sentry-api-0-api-application-details",
     ),
+    re_path(
+        r"^api-applications/(?P<app_id>[^\/]+)/rotate-secret/$",
+        ApiApplicationRotateSecretEndpoint.as_view(),
+        name="sentry-api-0-api-application-rotate-secret",
+    ),
     re_path(
         r"^api-authorizations/$",
         ApiAuthorizationsEndpoint.as_view(),

+ 30 - 0
tests/sentry/api/endpoints/test_api_application_rotate_secrets.py

@@ -0,0 +1,30 @@
+from django.urls import reverse
+
+from sentry.models import ApiApplication
+from sentry.testutils import APITestCase
+from sentry.testutils.silo import control_silo_test
+
+
+@control_silo_test(stable=True)
+class ApiApplicationRotateSecretTest(APITestCase):
+    def setUp(self):
+        self.app = ApiApplication.objects.create(owner=self.user, name="a")
+        self.path = reverse("sentry-api-0-api-application-rotate-secret", args=[self.app.client_id])
+
+    def test_unauthorized_call(self):
+        response = self.client.post(self.path)
+        assert response.status_code == 403
+
+    def test_invalid_app_id(self):
+        self.login_as(self.user)
+        path_with_invalid_id = reverse("sentry-api-0-api-application-rotate-secret", args=["abc"])
+        response = self.client.post(path_with_invalid_id)
+        assert response.status_code == 404
+
+    def test_valid_call(self):
+        self.login_as(self.user)
+        old_secret = self.app.client_secret
+        response = self.client.post(self.path, data={})
+        new_secret = response.data["clientSecret"]
+        assert len(new_secret) == len(old_secret)
+        assert new_secret != old_secret