Skip to content
Open
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
31 changes: 30 additions & 1 deletion src/sentry/sentry_apps/token_exchange/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.utils.functional import cached_property

from sentry.constants import SentryAppInstallationStatus, SentryAppStatus
from sentry.models.apiapplication import ApiApplication
from sentry.sentry_apps.models.sentry_app import SentryApp
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
Expand All @@ -24,6 +25,7 @@ def run(self) -> bool:
self._validate_is_sentry_app_making_request()
self._validate_app_is_owned_by_user()
self._validate_application_is_active()
self._validate_sentry_app()
self._validate_installation()
return True

Expand Down Expand Up @@ -58,13 +60,40 @@ def _validate_application_is_active(self) -> None:
},
)

def _validate_sentry_app(self) -> None:
if self.sentry_app.status == SentryAppStatus.DELETION_IN_PROGRESS:
raise SentryAppIntegratorError(
"Integration is being deleted",
status_code=401,
webhook_context={"sentry_app_id": self.sentry_app.id},
)

def _validate_installation(self) -> None:
if self.install.sentry_app.id != self.sentry_app.id:
# The caller may have loaded the installation before deletion started.
try:
installation = SentryAppInstallation.objects.get(
id=self.install.id, date_deleted__isnull=True
)
except SentryAppInstallation.DoesNotExist:
raise SentryAppIntegratorError(
"Installation not found",
status_code=401,
webhook_context={"installation_uuid": self.install.uuid},
)

if installation.sentry_app_id != self.sentry_app.id:
raise SentryAppIntegratorError(
f"Given installation is not for integration: {self.sentry_app.slug}",
webhook_context={"installation_uuid": self.install.uuid},
)

if installation.status == SentryAppInstallationStatus.PENDING_DELETION:
raise SentryAppIntegratorError(
"Installation is being deleted",
status_code=401,
webhook_context={"installation_uuid": self.install.uuid},
)

@cached_property
def sentry_app(self) -> SentryApp:
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from django.utils import timezone
from rest_framework.response import Response

from sentry.constants import SentryAppInstallationStatus, SentryAppStatus
from sentry.models.apiapplication import ApiApplication
from sentry.models.apigrant import ApiGrant
from sentry.models.apitoken import ApiToken
from sentry.sentry_apps.token_exchange.util import GrantTypes
from sentry.silo.base import SiloMode
Expand Down Expand Up @@ -82,6 +84,90 @@ def test_exchange_for_token_missing_data(self) -> None:
response = self.get_error_response(client_id=None)
assert response.status_code == 401

def test_rejects_authorization_for_deleting_app(self) -> None:
self.sentry_app.update(status=SentryAppStatus.DELETION_IN_PROGRESS)
assert self.sentry_app.application.is_active

response = self.get_error_response(status_code=401)

assert response.data["detail"] == "Integration is being deleted"
assert ApiGrant.objects.filter(id=self.install.api_grant_id).exists()
assert not ApiToken.objects.filter(application=self.sentry_app.application).exists()

def test_rejects_authorization_for_deleting_installation(self) -> None:
self.install.update(status=SentryAppInstallationStatus.PENDING_DELETION)
assert self.sentry_app.application.is_active

response = self.get_error_response(status_code=401)

assert response.data["detail"] == "Installation is being deleted"
assert ApiGrant.objects.filter(id=self.install.api_grant_id).exists()
assert not ApiToken.objects.filter(application=self.sentry_app.application).exists()

def test_rejects_refresh_for_deleting_app(self) -> None:
initial_response = self.get_success_response()
self.sentry_app.update(status=SentryAppStatus.DELETION_IN_PROGRESS)

response = self.get_error_response(
grant_type=GrantTypes.REFRESH,
refresh_token=initial_response.data["refreshToken"],
status_code=401,
)

assert response.data["detail"] == "Integration is being deleted"
self._assert_token_unchanged(initial_response.data["id"])

def test_rejects_refresh_for_deleting_installation(self) -> None:
initial_response = self.get_success_response()
self.install.update(status=SentryAppInstallationStatus.PENDING_DELETION)

response = self.get_error_response(
grant_type=GrantTypes.REFRESH,
refresh_token=initial_response.data["refreshToken"],
status_code=401,
)

assert response.data["detail"] == "Installation is being deleted"
self._assert_token_unchanged(initial_response.data["id"])

def test_rejects_client_secret_jwt_refresh_for_deleting_app(self) -> None:
initial_response = self.get_success_response()
jwt_token = self._create_jwt(
self.sentry_app.application.client_id, self.sentry_app.application.client_secret
)
self.sentry_app.update(status=SentryAppStatus.DELETION_IN_PROGRESS)

response = self.get_error_response(
grant_type=GrantTypes.CLIENT_SECRET_JWT,
extra_headers={"HTTP_AUTHORIZATION": f"Bearer {jwt_token}"},
status_code=401,
)

assert response.data["detail"] == "Integration is being deleted"
self._assert_token_unchanged(initial_response.data["id"])

def test_rejects_client_secret_jwt_refresh_for_deleting_installation(self) -> None:
initial_response = self.get_success_response()
jwt_token = self._create_jwt(
self.sentry_app.application.client_id, self.sentry_app.application.client_secret
)
self.install.update(status=SentryAppInstallationStatus.PENDING_DELETION)

response = self.get_error_response(
grant_type=GrantTypes.CLIENT_SECRET_JWT,
extra_headers={"HTTP_AUTHORIZATION": f"Bearer {jwt_token}"},
status_code=401,
)

assert response.data["detail"] == "Installation is being deleted"
self._assert_token_unchanged(initial_response.data["id"])

def _assert_token_unchanged(self, token_id: str) -> None:
token = ApiToken.objects.get(application=self.sentry_app.application)
assert str(token.id) == token_id
self.install.refresh_from_db()
assert self.install.api_token_id == token.id

def test_incorrect_grant_type(self) -> None:
self.get_error_response(grant_type="notit", status_code=403)

Expand Down
40 changes: 40 additions & 0 deletions tests/sentry/sentry_apps/token_exchange/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import pytest

from sentry.constants import SentryAppInstallationStatus, SentryAppStatus
from sentry.sentry_apps.models.sentry_app import SentryApp
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
from sentry.sentry_apps.token_exchange.validator import Validator
from sentry.sentry_apps.utils.errors import SentryAppIntegratorError, SentryAppSentryError
from sentry.testutils.cases import TestCase
Expand All @@ -24,6 +26,44 @@ def setUp(self) -> None:
def test_happy_path(self) -> None:
assert self.validator.run()

def test_allows_pending_installation(self) -> None:
self.install.update(status=SentryAppInstallationStatus.PENDING)

assert self.validator.run()

def test_rejects_deleting_app(self) -> None:
SentryApp.objects.get(id=self.install.sentry_app_id).update(
status=SentryAppStatus.DELETION_IN_PROGRESS
)
assert self.install.sentry_app.status != SentryAppStatus.DELETION_IN_PROGRESS

with pytest.raises(SentryAppIntegratorError) as exc:
self.validator.run()

assert exc.value.message == "Integration is being deleted"
assert exc.value.status_code == 401

def test_rejects_deleting_installation_with_stale_instance(self) -> None:
SentryAppInstallation.objects.get(id=self.install.id).update(
status=SentryAppInstallationStatus.PENDING_DELETION
)
assert self.install.status != SentryAppInstallationStatus.PENDING_DELETION

with pytest.raises(SentryAppIntegratorError) as exc:
self.validator.run()

assert exc.value.message == "Installation is being deleted"
assert exc.value.status_code == 401

def test_rejects_deleted_installation(self) -> None:
self.install.delete()

with pytest.raises(SentryAppIntegratorError) as exc:
self.validator.run()

assert exc.value.message == "Installation not found"
assert exc.value.status_code == 401

def test_request_must_be_made_by_sentry_app(self) -> None:
self.validator.user = self.create_user()

Expand Down
Loading