1313
1414from sentry .auth .exceptions import IdentityNotValid
1515from sentry .http import safe_urlopen , safe_urlread
16+ from sentry .integrations .utils .metrics import (
17+ IntegrationPipelineViewEvent ,
18+ IntegrationPipelineViewType ,
19+ )
1620from sentry .pipeline import PipelineView
1721from sentry .shared_integrations .exceptions import ApiError
1822from sentry .utils .http import absolute_uri
2327
2428logger = logging .getLogger (__name__ )
2529ERR_INVALID_STATE = "An error occurred while validating your request."
30+ ERR_TOKEN_RETRIEVAL = "Failed to retrieve token from the upstream service."
2631
2732
2833class OAuth2Provider (Provider ):
@@ -207,6 +212,20 @@ def refresh_identity(self, identity, *args, **kwargs):
207212from rest_framework .request import Request
208213
209214
215+ def record_event (event : IntegrationPipelineViewType , provider : str ):
216+ from sentry .integrations .base import INTEGRATION_PROVIDER_TO_TYPE , IntegrationProviderSlug
217+
218+ try :
219+ provider_slug = IntegrationProviderSlug (provider )
220+ domain = INTEGRATION_PROVIDER_TO_TYPE [provider_slug ]
221+ except ValueError :
222+ provider_slug = "unknown"
223+ domain = "unknown"
224+ logger .exception ("oauth2.record_event.invalid_provider" , extra = {"provider" : provider })
225+
226+ return IntegrationPipelineViewEvent (event , domain , provider_slug )
227+
228+
210229class OAuth2LoginView (PipelineView ):
211230 authorize_url = None
212231 client_id = None
@@ -238,22 +257,23 @@ def get_authorize_params(self, state, redirect_uri):
238257
239258 @method_decorator (csrf_exempt )
240259 def dispatch (self , request : Request , pipeline ) -> HttpResponse :
241- for param in ("code" , "error" , "state" ):
242- if param in request .GET :
243- return pipeline .next_step ()
260+ with record_event (IntegrationPipelineViewType .OAUTH_LOGIN , pipeline .provider .key ).capture ():
261+ for param in ("code" , "error" , "state" ):
262+ if param in request .GET :
263+ return pipeline .next_step ()
244264
245- state = secrets .token_hex ()
265+ state = secrets .token_hex ()
246266
247- params = self .get_authorize_params (
248- state = state , redirect_uri = absolute_uri (pipeline .redirect_url ())
249- )
250- redirect_uri = f"{ self .get_authorize_url ()} ?{ urlencode (params )} "
267+ params = self .get_authorize_params (
268+ state = state , redirect_uri = absolute_uri (pipeline .redirect_url ())
269+ )
270+ redirect_uri = f"{ self .get_authorize_url ()} ?{ urlencode (params )} "
251271
252- pipeline .bind_state ("state" , state )
253- if request .subdomain :
254- pipeline .bind_state ("subdomain" , request .subdomain )
272+ pipeline .bind_state ("state" , state )
273+ if request .subdomain :
274+ pipeline .bind_state ("subdomain" , request .subdomain )
255275
256- return self .redirect (redirect_uri )
276+ return self .redirect (redirect_uri )
257277
258278
259279class OAuth2CallbackView (PipelineView ):
@@ -280,70 +300,90 @@ def get_token_params(self, code, redirect_uri):
280300 }
281301
282302 def exchange_token (self , request : Request , pipeline , code ):
283- # TODO: this needs the auth yet
284- data = self .get_token_params (code = code , redirect_uri = absolute_uri (pipeline .redirect_url ()))
285- verify_ssl = pipeline .config .get ("verify_ssl" , True )
286- try :
287- req = safe_urlopen (self .access_token_url , data = data , verify_ssl = verify_ssl )
288- body = safe_urlread (req )
289- if req .headers .get ("Content-Type" , "" ).startswith ("application/x-www-form-urlencoded" ):
290- return dict (parse_qsl (body ))
291- return orjson .loads (body )
292- except SSLError :
293- logger .info (
294- "identity.oauth2.ssl-error" ,
295- extra = {"url" : self .access_token_url , "verify_ssl" : verify_ssl },
303+ with record_event (
304+ IntegrationPipelineViewType .TOKEN_EXCHANGE , pipeline .provider .key
305+ ).capture () as lifecycle :
306+ # TODO: this needs the auth yet
307+ data = self .get_token_params (
308+ code = code , redirect_uri = absolute_uri (pipeline .redirect_url ())
296309 )
297- url = self .access_token_url
298- return {
299- "error" : "Could not verify SSL certificate" ,
300- "error_description" : f"Ensure that { url } has a valid SSL certificate" ,
301- }
302- except ConnectionError :
303- url = self .access_token_url
304- logger .info ("identity.oauth2.connection-error" , extra = {"url" : url })
305- return {
306- "error" : "Could not connect to host or service" ,
307- "error_description" : f"Ensure that { url } is open to connections" ,
308- }
309- except orjson .JSONDecodeError :
310- logger .info ("identity.oauth2.json-error" , extra = {"url" : self .access_token_url })
311- return {
312- "error" : "Could not decode a JSON Response" ,
313- "error_description" : "We were not able to parse a JSON response, please try again." ,
314- }
310+ verify_ssl = pipeline .config .get ("verify_ssl" , True )
311+ try :
312+ req = safe_urlopen (self .access_token_url , data = data , verify_ssl = verify_ssl )
313+ body = safe_urlread (req )
314+ if req .headers .get ("Content-Type" , "" ).startswith (
315+ "application/x-www-form-urlencoded"
316+ ):
317+ return dict (parse_qsl (body ))
318+ return orjson .loads (body )
319+ except SSLError :
320+ logger .info (
321+ "identity.oauth2.ssl-error" ,
322+ extra = {"url" : self .access_token_url , "verify_ssl" : verify_ssl },
323+ )
324+ lifecycle .record_failure ({"failure_reason" : "ssl_error" })
325+ url = self .access_token_url
326+ return {
327+ "error" : "Could not verify SSL certificate" ,
328+ "error_description" : f"Ensure that { url } has a valid SSL certificate" ,
329+ }
330+ except ConnectionError :
331+ url = self .access_token_url
332+ logger .info ("identity.oauth2.connection-error" , extra = {"url" : url })
333+ lifecycle .record_failure ({"failure_reason" : "connection_error" })
334+ return {
335+ "error" : "Could not connect to host or service" ,
336+ "error_description" : f"Ensure that { url } is open to connections" ,
337+ }
338+ except orjson .JSONDecodeError :
339+ logger .info ("identity.oauth2.json-error" , extra = {"url" : self .access_token_url })
340+ lifecycle .record_failure ({"failure_reason" : "json_error" })
341+ return {
342+ "error" : "Could not decode a JSON Response" ,
343+ "error_description" : "We were not able to parse a JSON response, please try again." ,
344+ }
315345
316346 def dispatch (self , request : Request , pipeline ) -> HttpResponse :
317- error = request .GET .get ("error" )
318- state = request .GET .get ("state" )
319- code = request .GET .get ("code" )
320-
321- if error :
322- pipeline .logger .info ("identity.token-exchange-error" , extra = {"error" : error })
323- return pipeline .error (ERR_INVALID_STATE )
347+ with record_event (
348+ IntegrationPipelineViewType .OAUTH_CALLBACK , pipeline .provider .key
349+ ).capture () as lifecycle :
350+ error = request .GET .get ("error" )
351+ state = request .GET .get ("state" )
352+ code = request .GET .get ("code" )
353+
354+ if error :
355+ pipeline .logger .info ("identity.token-exchange-error" , extra = {"error" : error })
356+ lifecycle .record_failure (
357+ {"failure_reason" : "token_exchange_error" , "msg" : ERR_INVALID_STATE }
358+ )
359+ return pipeline .error (ERR_INVALID_STATE )
324360
325- if state != pipeline .fetch_state ("state" ):
326- pipeline .logger .info (
327- "identity.token-exchange-error" ,
328- extra = {
329- "error" : "invalid_state" ,
330- "state" : state ,
331- "pipeline_state" : pipeline .fetch_state ("state" ),
332- "code" : code ,
333- },
334- )
335- return pipeline .error (ERR_INVALID_STATE )
361+ if state != pipeline .fetch_state ("state" ):
362+ pipeline .logger .info (
363+ "identity.token-exchange-error" ,
364+ extra = {
365+ "error" : "invalid_state" ,
366+ "state" : state ,
367+ "pipeline_state" : pipeline .fetch_state ("state" ),
368+ "code" : code ,
369+ },
370+ )
371+ lifecycle .record_failure (
372+ {"failure_reason" : "token_exchange_error" , "msg" : ERR_INVALID_STATE }
373+ )
374+ return pipeline .error (ERR_INVALID_STATE )
336375
376+ # separate lifecycle event inside exchange_token
337377 data = self .exchange_token (request , pipeline , code )
338378
379+ # these errors are based off of the results of exchange_token, lifecycle errors are captured inside
339380 if "error_description" in data :
340381 error = data .get ("error" )
341- pipeline .logger .info ("identity.token-exchange-error" , extra = {"error" : error })
342382 return pipeline .error (data ["error_description" ])
343383
344384 if "error" in data :
345385 pipeline .logger .info ("identity.token-exchange-error" , extra = {"error" : data ["error" ]})
346- return pipeline .error ("Failed to retrieve token from the upstream service." )
386+ return pipeline .error (ERR_TOKEN_RETRIEVAL )
347387
348388 # we can either expect the API to be implicit and say "im looking for
349389 # blah within state data" or we need to pass implementation + call a
0 commit comments