test_authentication.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. import uuid
  2. from datetime import datetime
  3. import pytest
  4. from django.http import HttpRequest
  5. from django.test import RequestFactory, override_settings
  6. from rest_framework.exceptions import AuthenticationFailed
  7. from rest_framework.request import Request
  8. from sentry_relay.auth import generate_key_pair
  9. from sentry.api.authentication import (
  10. ClientIdSecretAuthentication,
  11. DSNAuthentication,
  12. OrgAuthTokenAuthentication,
  13. RelayAuthentication,
  14. RpcSignatureAuthentication,
  15. UserAuthTokenAuthentication,
  16. )
  17. from sentry.auth.system import SystemToken, is_system_auth
  18. from sentry.hybridcloud.models import ApiKeyReplica, ApiTokenReplica, OrgAuthTokenReplica
  19. from sentry.models.apikey import is_api_key_auth
  20. from sentry.models.apitoken import ApiToken, is_api_token_auth
  21. from sentry.models.orgauthtoken import OrgAuthToken, is_org_auth_token_auth
  22. from sentry.models.projectkey import ProjectKeyStatus
  23. from sentry.models.relay import Relay
  24. from sentry.services.hybrid_cloud.auth import AuthenticatedToken
  25. from sentry.services.hybrid_cloud.rpc import (
  26. RpcAuthenticationSetupException,
  27. generate_request_signature,
  28. )
  29. from sentry.silo import SiloMode
  30. from sentry.testutils.cases import TestCase
  31. from sentry.testutils.pytest.fixtures import django_db_all
  32. from sentry.testutils.silo import assume_test_silo_mode, control_silo_test, no_silo_test
  33. from sentry.utils.security.orgauthtoken_token import hash_token
  34. @control_silo_test
  35. class TestClientIdSecretAuthentication(TestCase):
  36. def setUp(self):
  37. super().setUp()
  38. self.auth = ClientIdSecretAuthentication()
  39. self.org = self.create_organization(owner=self.user)
  40. self.sentry_app = self.create_sentry_app(name="foo", organization=self.org)
  41. self.api_app = self.sentry_app.application
  42. def test_authenticate(self):
  43. request = HttpRequest()
  44. request.json_body = {
  45. "client_id": self.api_app.client_id,
  46. "client_secret": self.api_app.client_secret,
  47. }
  48. user, _ = self.auth.authenticate(request)
  49. assert user.id == self.sentry_app.proxy_user.id
  50. def test_without_json_body(self):
  51. request = HttpRequest()
  52. request.json_body = None
  53. with pytest.raises(AuthenticationFailed):
  54. self.auth.authenticate(request)
  55. def test_missing_client_id(self):
  56. request = HttpRequest()
  57. request.json_body = {"client_secret": self.api_app.client_secret}
  58. with pytest.raises(AuthenticationFailed):
  59. self.auth.authenticate(request)
  60. def test_missing_client_secret(self):
  61. request = HttpRequest()
  62. request.json_body = {"client_id": self.api_app.client_id}
  63. with pytest.raises(AuthenticationFailed):
  64. self.auth.authenticate(request)
  65. def test_incorrect_client_id(self):
  66. request = HttpRequest()
  67. request.json_body = {"client_id": "notit", "client_secret": self.api_app.client_secret}
  68. with pytest.raises(AuthenticationFailed):
  69. self.auth.authenticate(request)
  70. def test_incorrect_client_secret(self):
  71. request = HttpRequest()
  72. request.json_body = {"client_id": self.api_app.client_id, "client_secret": "notit"}
  73. with pytest.raises(AuthenticationFailed):
  74. self.auth.authenticate(request)
  75. class TestDSNAuthentication(TestCase):
  76. def setUp(self):
  77. super().setUp()
  78. self.auth = DSNAuthentication()
  79. self.org = self.create_organization(owner=self.user)
  80. self.project = self.create_project(organization=self.org)
  81. self.project_key = self.create_project_key(project=self.project)
  82. def test_authenticate(self):
  83. request = HttpRequest()
  84. request.META["HTTP_AUTHORIZATION"] = f"DSN {self.project_key.dsn_public}"
  85. result = self.auth.authenticate(request)
  86. assert result is not None
  87. user, auth = result
  88. assert user.is_anonymous
  89. assert auth == self.project_key
  90. def test_inactive_key(self):
  91. self.project_key.update(status=ProjectKeyStatus.INACTIVE)
  92. request = HttpRequest()
  93. request.META["HTTP_AUTHORIZATION"] = f"DSN {self.project_key.dsn_public}"
  94. with pytest.raises(AuthenticationFailed):
  95. self.auth.authenticate(request)
  96. class TestOrgAuthTokenAuthentication(TestCase):
  97. def setUp(self):
  98. super().setUp()
  99. self.auth = OrgAuthTokenAuthentication()
  100. self.org = self.create_organization(owner=self.user)
  101. self.token = "sntrys_abc123_xyz"
  102. self.org_auth_token = OrgAuthToken.objects.create(
  103. name="Test Token 1",
  104. token_hashed=hash_token(self.token),
  105. organization_id=self.org.id,
  106. token_last_characters="xyz",
  107. scope_list=[],
  108. date_last_used=None,
  109. )
  110. def test_authenticate(self):
  111. request = HttpRequest()
  112. request.META["HTTP_AUTHORIZATION"] = f"Bearer {self.token}"
  113. result = self.auth.authenticate(request)
  114. assert result is not None
  115. user, auth = result
  116. assert user.is_anonymous
  117. assert AuthenticatedToken.from_token(auth) == AuthenticatedToken.from_token(
  118. self.org_auth_token
  119. )
  120. def test_no_match(self):
  121. request = HttpRequest()
  122. request.META["HTTP_AUTHORIZATION"] = "Bearer sntrys_abc"
  123. with pytest.raises(AuthenticationFailed):
  124. self.auth.authenticate(request)
  125. def test_inactive_key(self):
  126. self.org_auth_token.update(date_deactivated=datetime.now())
  127. request = HttpRequest()
  128. request.META["HTTP_AUTHORIZATION"] = f"Bearer {self.token}"
  129. with pytest.raises(AuthenticationFailed):
  130. self.auth.authenticate(request)
  131. @control_silo_test
  132. class TestTokenAuthentication(TestCase):
  133. def setUp(self):
  134. super().setUp()
  135. self.auth = UserAuthTokenAuthentication()
  136. self.org = self.create_organization(owner=self.user)
  137. self.token = "abc123"
  138. self.api_token = ApiToken.objects.create(
  139. token=self.token,
  140. user=self.user,
  141. )
  142. def test_authenticate(self):
  143. request = HttpRequest()
  144. request.META["HTTP_AUTHORIZATION"] = f"Bearer {self.token}"
  145. result = self.auth.authenticate(request)
  146. assert result is not None
  147. user, auth = result
  148. assert user.is_anonymous is False
  149. assert user.id == self.user.id
  150. assert AuthenticatedToken.from_token(auth) == AuthenticatedToken.from_token(self.api_token)
  151. def test_no_match(self):
  152. request = HttpRequest()
  153. request.META["HTTP_AUTHORIZATION"] = "Bearer abc"
  154. with pytest.raises(AuthenticationFailed):
  155. self.auth.authenticate(request)
  156. @django_db_all
  157. @pytest.mark.parametrize("internal", [True, False])
  158. def test_registered_relay(internal):
  159. sk, pk = generate_key_pair()
  160. relay_id = str(uuid.uuid4())
  161. data = {"some_data": "hello"}
  162. packed, signature = sk.pack(data)
  163. request = RequestFactory().post("/", data=packed, content_type="application/json")
  164. request.META["HTTP_X_SENTRY_RELAY_SIGNATURE"] = signature
  165. request.META["HTTP_X_SENTRY_RELAY_ID"] = relay_id
  166. request.META["REMOTE_ADDR"] = "200.200.200.200" # something that is NOT local network
  167. Relay.objects.create(relay_id=relay_id, public_key=str(pk))
  168. if internal:
  169. white_listed_pk = [str(pk)] # mark the relay as internal
  170. else:
  171. white_listed_pk = []
  172. authenticator = RelayAuthentication()
  173. with override_settings(SENTRY_RELAY_WHITELIST_PK=white_listed_pk):
  174. authenticator.authenticate(request)
  175. # now the request should contain a relay
  176. relay = request.relay
  177. assert relay.is_internal == internal
  178. assert relay.public_key == str(pk)
  179. # data should be deserialized in request.relay_request_data
  180. assert request.relay_request_data == data
  181. @django_db_all
  182. @pytest.mark.parametrize("internal", [True, False])
  183. def test_statically_configured_relay(settings, internal):
  184. sk, pk = generate_key_pair()
  185. relay_id = str(uuid.uuid4())
  186. data = {"some_data": "hello"}
  187. packed, signature = sk.pack(data)
  188. request = RequestFactory().post("/", data=packed, content_type="application/json")
  189. request.META["HTTP_X_SENTRY_RELAY_SIGNATURE"] = signature
  190. request.META["HTTP_X_SENTRY_RELAY_ID"] = relay_id
  191. request.META["REMOTE_ADDR"] = "200.200.200.200" # something that is NOT local network
  192. relay_options = {relay_id: {"internal": internal, "public_key": str(pk)}}
  193. settings.SENTRY_OPTIONS["relay.static_auth"] = relay_options
  194. authenticator = RelayAuthentication()
  195. authenticator.authenticate(request)
  196. # now the request should contain a relay
  197. relay = request.relay
  198. assert relay.is_internal == internal
  199. assert relay.public_key == str(pk)
  200. # data should be deserialized in request.relay_request_data
  201. assert request.relay_request_data == data
  202. @control_silo_test
  203. class TestRpcSignatureAuthentication(TestCase):
  204. def setUp(self):
  205. super().setUp()
  206. self.auth = RpcSignatureAuthentication()
  207. self.org = self.create_organization(owner=self.user)
  208. @override_settings(RPC_SHARED_SECRET=["a-long-secret-key"])
  209. def test_authenticate_success(self):
  210. data = b'{"meta":{},"args":{"id":1}'
  211. request = RequestFactory().post("/", data=data, content_type="application/json")
  212. request = Request(request=request)
  213. signature = generate_request_signature(request.path_info, request.body)
  214. request.META["HTTP_AUTHORIZATION"] = f"rpcsignature {signature}"
  215. user, token = self.auth.authenticate(request)
  216. assert user.is_anonymous
  217. assert token == signature
  218. def test_authenticate_old_key_validates(self):
  219. request = RequestFactory().post("/", data="", content_type="application/json")
  220. with override_settings(RPC_SHARED_SECRET=["an-old-key"]):
  221. signature = generate_request_signature(request.path_info, request.body)
  222. request.META["HTTP_AUTHORIZATION"] = f"rpcsignature {signature}"
  223. request = Request(request=request)
  224. # Update settings so that we have a new key
  225. with override_settings(RPC_SHARED_SECRET=["a-long-secret-key", "an-old-key"]):
  226. user, token = self.auth.authenticate(request)
  227. assert user.is_anonymous
  228. assert token == signature
  229. def test_authenticate_without_signature(self):
  230. request = RequestFactory().post("/", data="", content_type="application/json")
  231. request.META["HTTP_AUTHORIZATION"] = "Bearer abcdef"
  232. request = Request(request=request)
  233. assert self.auth.authenticate(request) is None
  234. @override_settings(RPC_SHARED_SECRET=["a-long-secret-key"])
  235. def test_authenticate_invalid_signature(self):
  236. request = RequestFactory().post("/", data="", content_type="application/json")
  237. request.META["HTTP_AUTHORIZATION"] = "rpcsignature abcdef"
  238. request = Request(request=request)
  239. with pytest.raises(AuthenticationFailed):
  240. self.auth.authenticate(request)
  241. def test_authenticate_no_shared_secret(self):
  242. request = RequestFactory().post("/", data="", content_type="application/json")
  243. request.META["HTTP_AUTHORIZATION"] = "rpcsignature abcdef"
  244. request = Request(request=request)
  245. with override_settings(RPC_SHARED_SECRET=None):
  246. with pytest.raises(RpcAuthenticationSetupException):
  247. self.auth.authenticate(request)
  248. @no_silo_test
  249. class TestAuthTokens(TestCase):
  250. def test_system_tokens(self):
  251. sys_token = SystemToken()
  252. auth_token = AuthenticatedToken.from_token(sys_token)
  253. assert auth_token.entity_id is None
  254. assert auth_token.user_id is None
  255. assert is_system_auth(sys_token) and is_system_auth(auth_token)
  256. assert auth_token.organization_id is None
  257. assert auth_token.application_id is None
  258. assert auth_token.allowed_origins == sys_token.get_allowed_origins()
  259. assert auth_token.scopes == sys_token.get_scopes()
  260. assert auth_token.audit_log_data == sys_token.get_audit_log_data()
  261. def test_api_tokens(self):
  262. app = self.create_sentry_app(user=self.user, organization_id=self.organization.id)
  263. app_install = self.create_sentry_app_installation(
  264. organization=self.organization, user=self.user, slug=app.slug
  265. )
  266. with assume_test_silo_mode(SiloMode.CONTROL):
  267. at = app_install.api_token
  268. with assume_test_silo_mode(SiloMode.REGION):
  269. atr = ApiTokenReplica.objects.get(apitoken_id=at.id)
  270. assert at.organization_id
  271. for token in [at, atr]:
  272. auth_token = AuthenticatedToken.from_token(token)
  273. assert auth_token.entity_id == at.id
  274. assert auth_token.user_id == app.proxy_user_id
  275. assert is_api_token_auth(token) and is_api_token_auth(auth_token)
  276. assert auth_token.organization_id == self.organization.id
  277. assert auth_token.application_id == app.application_id
  278. assert auth_token.allowed_origins == token.get_allowed_origins()
  279. assert auth_token.scopes == token.get_scopes()
  280. assert auth_token.audit_log_data == token.get_audit_log_data()
  281. def test_api_keys(self):
  282. ak = self.create_api_key(organization=self.organization, scope_list=["projects:read"])
  283. with assume_test_silo_mode(SiloMode.REGION):
  284. akr = ApiKeyReplica.objects.get(apikey_id=ak.id)
  285. for token in [ak, akr]:
  286. auth_token = AuthenticatedToken.from_token(token)
  287. assert auth_token.entity_id == ak.id
  288. assert auth_token.user_id is None
  289. assert is_api_key_auth(token) and is_api_key_auth(auth_token)
  290. assert auth_token.organization_id == self.organization.id
  291. assert auth_token.application_id is None
  292. assert auth_token.allowed_origins == token.get_allowed_origins()
  293. assert auth_token.scopes == token.get_scopes()
  294. assert auth_token.audit_log_data == token.get_audit_log_data()
  295. def test_org_auth_tokens(self):
  296. oat = OrgAuthToken.objects.create(
  297. organization_id=self.organization.id,
  298. name="token 1",
  299. token_hashed="ABCDEF",
  300. token_last_characters="xyz1",
  301. scope_list=["org:ci"],
  302. date_last_used=None,
  303. )
  304. with assume_test_silo_mode(SiloMode.REGION):
  305. oatr = OrgAuthTokenReplica.objects.get(orgauthtoken_id=oat.id)
  306. for token in [oat, oatr]:
  307. auth_token = AuthenticatedToken.from_token(token)
  308. assert auth_token.entity_id == oat.id
  309. assert auth_token.user_id is None
  310. assert is_org_auth_token_auth(token) and is_org_auth_token_auth(auth_token)
  311. assert auth_token.organization_id == self.organization.id
  312. assert auth_token.application_id is None
  313. assert auth_token.allowed_origins == token.get_allowed_origins()
  314. assert auth_token.scopes == token.get_scopes()
  315. assert auth_token.audit_log_data == token.get_audit_log_data()