def authenticate(self, request): set_custom_metric("BearerAuthentication", "Failed") # default value if not self.get_user_info_url(): logger.warning('The setting OAUTH2_USER_INFO_URL is invalid!') set_custom_metric("BearerAuthentication", "NoURL") return None set_custom_metric("BearerAuthentication_user_info_url", self.get_user_info_url()) auth = get_authorization_header(request).split() if not auth or auth[0].lower() != b'bearer': set_custom_metric("BearerAuthentication", "None") return None if len(auth) == 1: raise exceptions.AuthenticationFailed( 'Invalid token header. No credentials provided.') if len(auth) > 2: raise exceptions.AuthenticationFailed( 'Invalid token header. Token string should not contain spaces.' ) output = self.authenticate_credentials(auth[1].decode('utf8')) set_custom_metric("BearerAuthentication", "Success") return output
def get_schedules_with_target_date_by_bin_and_orgs( self, order_by='enrollment__user__id' ): """ Returns Schedules with the target_date, related to Users whose id matches the bin_num, and filtered by org_list. Arguments: order_by -- string for field to sort the resulting Schedules by """ target_day = _get_datetime_beginning_of_day(self.target_datetime) schedule_day_equals_target_day_filter = { 'courseenrollment__schedule__{}__gte'.format(self.schedule_date_field): target_day, 'courseenrollment__schedule__{}__lt'.format(self.schedule_date_field): target_day + datetime.timedelta(days=1), } users = User.objects.filter( courseenrollment__is_active=True, is_active=True, **schedule_day_equals_target_day_filter ).annotate( id_mod=self.bin_num_for_user_id(F('id')) ).filter( id_mod=self.bin_num ) schedule_day_equals_target_day_filter = { '{}__gte'.format(self.schedule_date_field): target_day, '{}__lt'.format(self.schedule_date_field): target_day + datetime.timedelta(days=1), } schedules = Schedule.objects.select_related( 'enrollment__user__profile', 'enrollment__course', 'enrollment__fbeenrollmentexclusion', ).filter( Q(enrollment__course__end__isnull=True) | Q( enrollment__course__end__gte=self.current_datetime ), self.experience_filter, enrollment__user__in=users, enrollment__is_active=True, active=True, **schedule_day_equals_target_day_filter ).order_by(order_by) schedules = self.filter_by_org(schedules) if "read_replica" in settings.DATABASES: schedules = schedules.using("read_replica") LOG.info(u'Query = %r', schedules.query.sql_with_params()) with function_trace('schedule_query_set_evaluation'): # This will run the query and cache all of the results in memory. num_schedules = len(schedules) LOG.info(u'Number of schedules = %d', num_schedules) # This should give us a sense of the volume of data being processed by each task. set_custom_metric('num_schedules', num_schedules) return schedules
def process_request(self, request): """ Reconstitute the full JWT and add a new cookie on the request object. """ use_jwt_cookie_requested = request.META.get(USE_JWT_COOKIE_HEADER) header_payload_cookie = request.COOKIES.get(jwt_cookie_header_payload_name()) signature_cookie = request.COOKIES.get(jwt_cookie_signature_name()) if not use_jwt_cookie_requested: metric_value = 'not-requested' elif header_payload_cookie and signature_cookie: # Reconstitute JWT auth cookie if split cookies are available and jwt cookie # authentication was requested by the client. request.COOKIES[jwt_cookie_name()] = '{}{}{}'.format( header_payload_cookie, JWT_DELIMITER, signature_cookie, ) metric_value = 'success' elif header_payload_cookie or signature_cookie: # Log unexpected case of only finding one cookie. if not header_payload_cookie: log_message, metric_value = self._get_missing_cookie_message_and_metric( jwt_cookie_header_payload_name() ) if not signature_cookie: log_message, metric_value = self._get_missing_cookie_message_and_metric( jwt_cookie_signature_name() ) log.warning(log_message) else: metric_value = 'missing-both' monitoring.set_custom_metric('request_jwt_cookie', metric_value)
def _set_request_auth_type_metric(self, request): """ Add metric 'request_auth_type' for the authentication type used. NOTE: This is a best guess at this point. Possible values include: no-user unauthenticated jwt/bearer/other-token-type session-or-unknown (catch all) """ if 'HTTP_AUTHORIZATION' in request.META and request.META[ 'HTTP_AUTHORIZATION']: token_parts = request.META['HTTP_AUTHORIZATION'].split() # Example: "JWT eyJhbGciO..." if len(token_parts) == 2: auth_type = token_parts[0].lower( ) # 'jwt' or 'bearer' (for example) else: auth_type = 'other-token-type' elif not hasattr(request, 'user') or not request.user: auth_type = 'no-user' elif not request.user.is_authenticated: auth_type = 'unauthenticated' else: auth_type = 'session-or-unknown' monitoring.set_custom_metric('request_auth_type', auth_type)
def _set_request_referer_metric(self, request): """ Add metric 'request_referer' for http referer. """ if 'HTTP_REFERER' in request.META and request.META['HTTP_REFERER']: monitoring.set_custom_metric('request_referer', request.META['HTTP_REFERER'])
def lms_user_id_with_metric(self, usage=None): """ Returns the LMS user_id, or None if not found. Also sets a metric with the result. Arguments: usage (string): Optional. A description of how the returned id will be used. This will be included in log messages if the LMS user id cannot be found. Side effect: If found, writes custom metric: 'ecommerce_found_lms_user_id' If not found, writes custom metric: 'ecommerce_missing_lms_user_id' """ # Read the lms_user_id from the ecommerce_user. lms_user_id = self.lms_user_id if lms_user_id: monitoring_utils.set_custom_metric('ecommerce_found_lms_user_id', lms_user_id) return lms_user_id # Could not find the lms_user_id monitoring_utils.set_custom_metric('ecommerce_missing_lms_user_id', self.id) log.warn(u'Could not find lms_user_id for user %s for %s', self.id, usage) return None
def lms_user_id(self): """ Returns the LMS user_id, or None if not found. """ # JWT cookie is used with API calls from new microfrontends. This is not persisted. lms_user_id = self._get_lms_user_id_from_jwt_cookie() if lms_user_id: return lms_user_id # This is persisted to the database during any new oAuth+SSO flow. lms_user_id = self._get_lms_user_id_from_social_auth() if lms_user_id: return lms_user_id # Server-to-server calls from LMS to ecommerce use a specially crafted JWT. lms_user_id = self._get_lms_user_id_from_tracking_context() if lms_user_id: return lms_user_id # If we get here, it means either: # 1. The user has an old social_auth session created before the LMS user_id was written to the database, or # 2. This could be a server-to-server call that isn't properly handled, or # 3. Some other unknown flow. monitoring_utils.set_custom_metric( 'ecommerce_user_missing_lms_user_id', self.id) return None
def is_flag_active(self, flag_name, check_before_waffle_callback=None): """ Returns and caches whether the provided flag is active. If the flag value is already cached in the request, it is returned. If check_before_waffle_callback is supplied, it is called before checking waffle. If check_before_waffle_callback returns None, or if it is not supplied, then waffle is used to check the flag. Important: Caching for the check_before_waffle_callback must be handled by the callback itself. Note: A waffle flag's default is False if not defined. If you think you need the default to be True, see the module docstring for alternatives. Arguments: flag_name (String): The name of the flag to check. check_before_waffle_callback (function): (Optional) A function that will be checked before continuing on to waffle. If check_before_waffle_callback(namespaced_flag_name) returns True or False, it is returned. If it returns None, then waffle is used. """ # validate arguments namespaced_flag_name = self._namespaced_name(flag_name) if check_before_waffle_callback: value = check_before_waffle_callback(namespaced_flag_name) if value is not None: # Do not cache value for the callback, because the key might be different. # The callback needs to handle its own caching if it wants it. self._set_waffle_flag_metric(namespaced_flag_name, value) return value value = self._cached_flags.get(namespaced_flag_name) if value is not None: self._set_waffle_flag_metric(namespaced_flag_name, value) return value request = crum.get_current_request() if not request: log.warning(u"%sFlag '%s' accessed without a request", self.log_prefix, namespaced_flag_name) # Return the Flag's Everyone value if not in a request context. # Note: this skips the cache as the value might be different # in a normal request context. This case seems to occur when # a page redirects to a 404, or for celery workers. value = self._is_flag_active_for_everyone(namespaced_flag_name) self._set_waffle_flag_metric(namespaced_flag_name, value) set_custom_metric('warn_flag_no_request_return_value', value) return value value = flag_is_active(request, namespaced_flag_name) self._cached_flags[namespaced_flag_name] = value self._set_waffle_flag_metric(namespaced_flag_name, value) return value
def _get_jwt_builder(self, user, is_client_restricted): """ Creates and returns a JWTBuilder object for creating JWTs. """ # If JWT scope enforcement is enabled, we need to sign tokens # given to restricted applications with a key that # other IDAs do not have access to. This prevents restricted # applications from getting access to API endpoints available # on other IDAs which have not yet been protected with the # scope-related DRF permission classes. Once all endpoints have # been protected, we can enable all IDAs to use the same new # (asymmetric) key. # TODO: ARCH-162 use_asymmetric_key = ENFORCE_JWT_SCOPES.is_enabled( ) and is_client_restricted monitoring_utils.set_custom_metric('oauth_asymmetric_jwt', use_asymmetric_key) log.info("Using Asymmetric JWT: %s", use_asymmetric_key) return JwtBuilder( user, asymmetric=use_asymmetric_key, secret=settings.JWT_AUTH['JWT_SECRET_KEY'], issuer=settings.JWT_AUTH['JWT_ISSUER'], )
def process_request(self, request): """ Reconstitute the full JWT and add a new cookie on the request object. """ use_jwt_cookie_requested = request.META.get(USE_JWT_COOKIE_HEADER) header_payload_cookie = request.COOKIES.get(jwt_cookie_header_payload_name()) signature_cookie = request.COOKIES.get(jwt_cookie_signature_name()) if not use_jwt_cookie_requested: metric_value = 'not-requested' elif header_payload_cookie and signature_cookie: # Reconstitute JWT auth cookie if split cookies are available and jwt cookie # authentication was requested by the client. request.COOKIES[jwt_cookie_name()] = '{}{}{}'.format( header_payload_cookie, JWT_DELIMITER, signature_cookie, ) metric_value = 'success' elif header_payload_cookie or signature_cookie: # Log unexpected case of only finding one cookie. if not header_payload_cookie: log_message, metric_value = self._get_missing_cookie_message_and_metric( jwt_cookie_header_payload_name() ) if not signature_cookie: log_message, metric_value = self._get_missing_cookie_message_and_metric( jwt_cookie_signature_name() ) log.warning(log_message) else: metric_value = 'missing-both' monitoring.set_custom_metric('request_jwt_cookie', metric_value)
def lms_user_id(self): """ Returns the LMS user_id, or None if not found. """ # JWT cookie is used with API calls from new microfrontends. This is not persisted. # TODO: Rename ``_get_lms_user_id_from_jwt_cookie`` to ``_get_lms_user_id_from_jwt`` # and update to use new method to be added to JwtAuthentication in edx-drf-extensions # to get the decoded JWT used for authentication, no matter where it came from. # See https://github.com/edx/edx-drf-extensions/pull/69#discussion_r286618922 lms_user_id = self._get_lms_user_id_from_jwt_cookie() if lms_user_id: return lms_user_id # This is persisted to the database during any new oAuth+SSO flow. lms_user_id = self._get_lms_user_id_from_social_auth() if lms_user_id: return lms_user_id # Server-to-server calls from LMS to ecommerce use a specially crafted JWT. lms_user_id = self._get_lms_user_id_from_tracking_context() if lms_user_id: return lms_user_id # If we get here, it means either: # 1. The user has an old social_auth session created before the LMS user_id was written to the database, or # 2. This could be a server-to-server call that isn't properly handled, or # 3. Some other unknown flow. monitoring_utils.set_custom_metric('ecommerce_user_missing_lms_user_id', self.id) return None
def activate_account(activation_key): """Activate a user's account. Args: activation_key (unicode): The activation key the user received via email. Returns: None Raises: errors.UserNotAuthorized errors.UserAPIInternalError: the operation failed due to an unexpected error. """ # TODO: Confirm this `activate_account` is only used for tests. If so, this should not be used for tests, and we # should instead use the `activate_account` used for /activate. set_custom_metric('user_api_activate_account', 'True') if waffle().is_enabled(PREVENT_AUTH_USER_WRITES): raise errors.UserAPIInternalError(SYSTEM_MAINTENANCE_MSG) try: registration = Registration.objects.get(activation_key=activation_key) except Registration.DoesNotExist: raise errors.UserNotAuthorized else: # This implicitly saves the registration registration.activate()
def _set_request_auth_type_metric(self, request): """ Add metric 'request_auth_type' for the authentication type used. NOTE: This is a best guess at this point. Possible values include: no-user unauthenticated jwt/bearer/other-token-type session-or-unknown (catch all) """ if 'HTTP_AUTHORIZATION' in request.META and request.META['HTTP_AUTHORIZATION']: token_parts = request.META['HTTP_AUTHORIZATION'].split() # Example: "JWT eyJhbGciO..." if len(token_parts) == 2: auth_type = token_parts[0].lower() # 'jwt' or 'bearer' (for example) else: auth_type = 'other-token-type' elif not hasattr(request, 'user') or not request.user: auth_type = 'no-user' elif not request.user.is_authenticated: auth_type = 'unauthenticated' else: auth_type = 'session-or-unknown' monitoring.set_custom_metric('request_auth_type', auth_type)
def _compute_time_fields(expires_in): """ Returns (iat, exp) tuple to be used as time-related values in a token. """ now = int(time()) expires_in = expires_in or settings.JWT_AUTH['JWT_EXPIRATION'] set_custom_metric('jwt_expires_in', expires_in) return now, now + expires_in
def _compute_time_fields(expires_in): """ Returns (iat, exp) tuple to be used as time-related values in a token. """ now = int(time()) expires_in = expires_in or settings.JWT_AUTH['JWT_EXPIRATION'] set_custom_metric('jwt_expires_in', expires_in) return now, now + expires_in
def get_adapter(self, request): """ Returns the appropriate adapter based on the OAuth client linked to the request. """ client_id = self._get_client_id(request) monitoring_utils.set_custom_metric('oauth_client_id', client_id) return self.dot_adapter
def get_schedules_with_target_date_by_bin_and_orgs( self, order_by='enrollment__user__id' ): """ Returns Schedules with the target_date, related to Users whose id matches the bin_num, and filtered by org_list. Arguments: order_by -- string for field to sort the resulting Schedules by """ target_day = _get_datetime_beginning_of_day(self.target_datetime) schedule_day_equals_target_day_filter = { 'courseenrollment__schedule__{}__gte'.format(self.schedule_date_field): target_day, 'courseenrollment__schedule__{}__lt'.format(self.schedule_date_field): target_day + datetime.timedelta(days=1), } users = User.objects.filter( courseenrollment__is_active=True, **schedule_day_equals_target_day_filter ).annotate( id_mod=F('id') % self.num_bins ).filter( id_mod=self.bin_num ) schedule_day_equals_target_day_filter = { '{}__gte'.format(self.schedule_date_field): target_day, '{}__lt'.format(self.schedule_date_field): target_day + datetime.timedelta(days=1), } schedules = Schedule.objects.select_related( 'enrollment__user__profile', 'enrollment__course', ).filter( Q(enrollment__course__end__isnull=True) | Q( enrollment__course__end__gte=self.current_datetime ), self.experience_filter, enrollment__user__in=users, enrollment__is_active=True, active=True, **schedule_day_equals_target_day_filter ).order_by(order_by) schedules = self.filter_by_org(schedules) if "read_replica" in settings.DATABASES: schedules = schedules.using("read_replica") LOG.info(u'Query = %r', schedules.query.sql_with_params()) with function_trace('schedule_query_set_evaluation'): # This will run the query and cache all of the results in memory. num_schedules = len(schedules) LOG.info(u'Number of schedules = %d', num_schedules) # This should give us a sense of the volume of data being processed by each task. set_custom_metric('num_schedules', num_schedules) return schedules
def _set_request_user_id_metric(self, request): """ Add request_user_id metric Metrics: request_user_id """ if hasattr(request, 'user') and hasattr(request.user, 'id') and request.user.id: monitoring.set_custom_metric('request_user_id', request.user.id)
def _set_request_user_id_metric(self, request): """ Add request_user_id metric Metrics: request_user_id """ if hasattr(request, 'user') and hasattr(request.user, 'id') and request.user.id: monitoring.set_custom_metric('request_user_id', request.user.id)
def dispatch(self, request, *args, **kwargs): response = super(AccessTokenView, self).dispatch(request, *args, **kwargs) token_type = request.POST.get('token_type', 'no_token_type_supplied').lower() monitoring_utils.set_custom_metric('oauth_token_type', token_type) monitoring_utils.set_custom_metric('oauth_grant_type', request.POST.get('grant_type', '')) if response.status_code == 200 and token_type == 'jwt': response.content = self._build_jwt_response_from_access_token_response(request, response) return response
def request(self, method, url, **kwargs): # pylint: disable=arguments-differ """ Overrides Session.request to ensure that the session is authenticated. Note: Typically, users of the client won't call this directly, but will instead use Session.get or Session.post. """ set_custom_metric('api_client', 'OAuthAPIClient') self._ensure_authentication() return super(OAuthAPIClient, self).request(method, url, **kwargs)
def dispatch(self, request, *args, **kwargs): response = super(AccessTokenView, self).dispatch(request, *args, **kwargs) token_type = request.POST.get('token_type', 'no_token_type_supplied').lower() monitoring_utils.set_custom_metric('oauth_token_type', token_type) monitoring_utils.set_custom_metric('oauth_grant_type', request.POST.get('grant_type', '')) if response.status_code == 200 and token_type == 'jwt': response.content = self._build_jwt_response_from_access_token_response(request, response) return response
def get_basket(self, request): """ Return the open basket for this request """ # pylint: disable=protected-access if request._basket_cache is not None: monitoring_utils.set_custom_metric('basket_id', request._basket_cache.id) return request._basket_cache manager = Basket.open cookie_key = self.get_cookie_key(request) cookie_basket = self.get_cookie_basket(cookie_key, request, manager) if hasattr(request, 'user') and request.user.is_authenticated(): # Signed-in user: if they have a cookie basket too, it means # that they have just signed in and we need to merge their cookie # basket into their user basket, then delete the cookie. try: basket, __ = manager.get_or_create(owner=request.user, site=request.site) except Basket.MultipleObjectsReturned: # Not sure quite how we end up here with multiple baskets. # We merge them and create a fresh one old_baskets = list( manager.filter(owner=request.user, site=request.site)) basket = old_baskets[0] for other_basket in old_baskets[1:]: self.merge_baskets(basket, other_basket) # Assign user onto basket to prevent further SQL queries when # basket.owner is accessed. basket.owner = request.user if cookie_basket: self.merge_baskets(basket, cookie_basket) request.cookies_to_delete.append(cookie_key) elif cookie_basket: # Anonymous user with a basket tied to the cookie basket = cookie_basket else: # Anonymous user with no basket - instantiate a new basket instance. No need to save yet. basket = Basket(site=request.site) # Cache basket instance for the duration of this request request._basket_cache = basket if request._basket_cache is not None: monitoring_utils.set_custom_metric('basket_id', request._basket_cache.id) else: # pragma: no cover pass return basket
def get_view_for_backend(self, backend): """ Return the appropriate view from the requested backend. """ if backend == self.dot_adapter.backend: monitoring_utils.set_custom_metric('oauth_view', 'dot') return self.dot_view.as_view() elif backend == self.dop_adapter.backend: monitoring_utils.set_custom_metric('oauth_view', 'dop') return self.dop_view.as_view() else: raise KeyError( 'Failed to dispatch view. Invalid backend {}'.format(backend))
def __init__(self, url, signing_key=None, username=None, full_name=None, email=None, timeout=5, issuer=None, expires_in=30, tracking_context=None, oauth_access_token=None, session=None, jwt=None, **kwargs): """ EdxRestApiClient is deprecated. Use OAuthAPIClient instead. Instantiate a new client. You can pass extra kwargs to Slumber like 'append_slash'. Raises: ValueError: If a URL is not provided. """ set_custom_metric('api_client', 'EdxRestApiClient') if not url: raise ValueError('An API url must be supplied!') if jwt: auth = SuppliedJwtAuth(jwt) elif oauth_access_token: auth = BearerAuth(oauth_access_token) elif signing_key and username: auth = JwtAuth(username, full_name, email, signing_key, issuer=issuer, expires_in=expires_in, tracking_context=tracking_context) else: auth = None session = session or requests.Session() session.headers['User-Agent'] = self.user_agent() session.timeout = timeout super(EdxRestApiClient, self).__init__(url, session=session, auth=auth, **kwargs)
def _get_lms_user_id_from_tracking_context(self): """ Return LMS user_id passed through tracking_context, if found. Returns None if not found. Side effect: If found, writes custom metric: 'lms_user_id_tracking_context' """ # Return lms_user_id passed through tracking_context, if found. tracking_context = self.tracking_context or {} lms_user_id_tracking_context = tracking_context.get('lms_user_id') if lms_user_id_tracking_context: monitoring_utils.set_custom_metric('lms_user_id_tracking_context', lms_user_id_tracking_context) return lms_user_id_tracking_context
def _annotate_for_monitoring(message_type, course_key, target_day_str, day_offset): """ Set custom metrics in monitoring to make it easier to identify what messages are being sent and why. """ # This identifies the type of message being sent, for example: schedules.recurring_nudge3. set_custom_metric('message_name', '{0}.{1}'.format(message_type.app_label, message_type.name)) # The domain name of the site we are sending the message for. set_custom_metric('course_key', course_key) # The date we are processing data for. set_custom_metric('target_day', target_day_str) # The number of days relative to the current date to process data for. set_custom_metric('day_offset', day_offset) # A unique identifier for this batch of messages being sent. set_custom_metric('send_uuid', message_type.uuid)
def process_view(self, request, view_func, view_args, view_kwargs): # pylint: disable=unused-argument """ Reconstitute the full JWT and add a new cookie on the request object. """ assert hasattr( request, 'session' ), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." # noqa E501 line too long use_jwt_cookie_requested = request.META.get(USE_JWT_COOKIE_HEADER) header_payload_cookie = request.COOKIES.get( jwt_cookie_header_payload_name()) signature_cookie = request.COOKIES.get(jwt_cookie_signature_name()) is_set_request_user_for_jwt_cookie_enabled = get_setting( ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE) if use_jwt_cookie_requested and is_set_request_user_for_jwt_cookie_enabled: # DRF does not set request.user until process_response. This makes it available in process_view. # For more info, see https://github.com/jpadilla/django-rest-framework-jwt/issues/45#issuecomment-74996698 request.user = SimpleLazyObject( lambda: _get_user_from_jwt(request, view_func)) if not use_jwt_cookie_requested: metric_value = 'not-requested' elif header_payload_cookie and signature_cookie: # Reconstitute JWT auth cookie if split cookies are available and jwt cookie # authentication was requested by the client. request.COOKIES[jwt_cookie_name()] = '{}{}{}'.format( header_payload_cookie, JWT_DELIMITER, signature_cookie, ) metric_value = 'success' elif header_payload_cookie or signature_cookie: # Log unexpected case of only finding one cookie. if not header_payload_cookie: log_message, metric_value = self._get_missing_cookie_message_and_metric( jwt_cookie_header_payload_name()) if not signature_cookie: log_message, metric_value = self._get_missing_cookie_message_and_metric( jwt_cookie_signature_name()) log.warning(log_message) else: metric_value = 'missing-both' log.warning( 'Both JWT auth cookies missing. JWT auth cookies will not be reconstituted.' ) monitoring.set_custom_metric('request_jwt_cookie', metric_value)
def _set_view_func_compare_metric(self, view_func): """ Set temporary metric to ensure that the view_func of `process_view` always matches the one from using `resolve` on the request. """ try: view_func_module = view_func.__module__ cached_response = DEFAULT_REQUEST_CACHE.get_cached_response( self._VIEW_FUNC_MODULE_METRIC_CACHE_KEY) if cached_response.is_found: view_func_compare = 'success' if view_func_module == cached_response.value else view_func_module else: view_func_compare = 'missing' set_custom_metric('temp_view_func_compare', view_func_compare) except Exception as e: set_custom_metric('temp_view_func_compare_error', e)
def get_user_tracking_id(user): """ Returns the tracking ID associated with this user or None. The tracking ID is cached. """ cache_key = _get_tracking_cache_key(user) tracking_id = cache.get(cache_key) # if tracking ID was not found in cache, fetch and cache it if tracking_id is None: # first, attempt to get the tracking id from an oauth2 social_auth record tracking_id = _get_lms_user_id_from_social_auth(user) cache.set(cache_key, tracking_id) set_custom_metric('tracking_id', tracking_id) return tracking_id
def has_permission(self, request, view): """ Check if the OAuth client associated with auth token in current request has permission to access the information for provider """ provider_id = view.kwargs.get('provider_id') if not request.auth or not provider_id: # doesn't have access token or no provider_id specified return False try: ProviderApiPermissions.objects.get(client__pk=request.auth.client_id, provider_id=provider_id) except ProviderApiPermissions.DoesNotExist: return False set_custom_metric('deprecated_ThirdPartyAuthProviderApiPermission', True) return True
def _encode_and_sign(payload, use_asymmetric_key, secret): """Encode and sign the provided payload.""" set_custom_metric('jwt_is_asymmetric', use_asymmetric_key) keys = jwk.KEYS() if use_asymmetric_key: serialized_keypair = json.loads(settings.JWT_AUTH['JWT_PRIVATE_SIGNING_JWK']) keys.add(serialized_keypair) algorithm = settings.JWT_AUTH['JWT_SIGNING_ALGORITHM'] else: key = secret if secret else settings.JWT_AUTH['JWT_SECRET_KEY'] keys.add({'key': key, 'kty': 'oct'}) algorithm = settings.JWT_AUTH['JWT_ALGORITHM'] data = json.dumps(payload) jws = JWS(data, alg=algorithm) return jws.sign_compact(keys=keys)
def _encode_and_sign(payload, use_asymmetric_key, secret): """Encode and sign the provided payload.""" set_custom_metric('jwt_is_asymmetric', use_asymmetric_key) keys = jwk.KEYS() if use_asymmetric_key: serialized_keypair = json.loads(settings.JWT_AUTH['JWT_PRIVATE_SIGNING_JWK']) keys.add(serialized_keypair) algorithm = settings.JWT_AUTH['JWT_SIGNING_ALGORITHM'] else: key = secret if secret else settings.JWT_AUTH['JWT_SECRET_KEY'] keys.add({'key': key, 'kty': 'oct'}) algorithm = settings.JWT_AUTH['JWT_ALGORITHM'] data = json.dumps(payload) jws = JWS(data, alg=algorithm) return jws.sign_compact(keys=keys)
def jwt_decode_handler(token): """ Attempt to decode the given token with each of the configured JWT issuers. Args: token (str): The JWT to decode. Returns: dict: The JWT's payload. Raises: InvalidTokenError: If the token is invalid, or if none of the configured issuer/secret-key combos can properly decode the token. """ # First, try ecommerce decoder that handles multiple issuers. # See ARCH-276 for details of removing additional issuers and retiring this # custom jwt_decode_handler. try: jwt_payload = _ecommerce_jwt_decode_handler_multiple_issuers(token) monitoring_utils.set_custom_metric(JWT_DECODE_HANDLER_METRIC_KEY, 'ecommerce-multiple-issuers') return jwt_payload except Exception: # pylint: disable=broad-except if waffle.switch_is_active( 'jwt_decode_handler.log_exception.ecommerce-multiple-issuers'): logger.info( 'Failed to use ecommerce multiple issuer jwt_decode_handler.', exc_info=True) # Next, try jwt_decode_handler from edx_drf_extensions # Note: this jwt_decode_handler can handle asymmetric keys, but only a # single issuer. Therefore, the LMS must be the first configured issuer. try: jwt_payload = edx_drf_extensions_jwt_decode_handler(token) monitoring_utils.set_custom_metric(JWT_DECODE_HANDLER_METRIC_KEY, 'edx-drf-extensions') return jwt_payload except Exception: # pylint: disable=broad-except # continue and try again if waffle.switch_is_active( 'jwt_decode_handler.log_exception.edx-drf-extensions'): logger.info('Failed to use edx-drf-extensions jwt_decode_handler.', exc_info=True) raise
def add_lms_user_id(self, missing_metric_key, called_from, allow_missing=False): """ If this user does not already have an LMS user id, look for the id in social auth. If the id can be found, add it to the user and save the user. The LMS user_id may already be present for the user. It may have been added from the jwt (see the EDX_DRF_EXTENSIONS.JWT_PAYLOAD_USER_ATTRIBUTE_MAPPING settings) or by a previous call to this method. Arguments: missing_metric_key (String): Key name for metric that will be created if the LMS user id cannot be found. called_from (String): Descriptive string describing the caller. This will be included in log messages. allow_missing (boolean): True if the LMS user id is allowed to be missing. This affects the log messages, custom metrics, and (in combination with the allow_missing_lms_user_id switch), whether an MissingLmsUserIdException is raised. Defaults to False. Side effect: If the LMS id cannot be found, writes custom metrics. """ if not self.lms_user_id: # Check for the LMS user id in social auth lms_user_id_social_auth, social_auth_id = self._get_lms_user_id_from_social_auth( ) if lms_user_id_social_auth: self.lms_user_id = lms_user_id_social_auth self.save() log.info( u'Saving lms_user_id from social auth with id %s for user %s. Called from %s', social_auth_id, self.id, called_from) else: # Could not find the LMS user id if allow_missing or waffle.switch_is_active( ALLOW_MISSING_LMS_USER_ID): monitoring_utils.set_custom_metric( 'ecommerce_missing_lms_user_id_allowed', self.id) monitoring_utils.set_custom_metric( missing_metric_key + '_allowed', self.id) error_msg = ( u'Could not find lms_user_id for user {user_id}. Missing lms_user_id is allowed. ' u'Called from {called_from}'.format( user_id=self.id, called_from=called_from)) log.info(error_msg, exc_info=True) else: monitoring_utils.set_custom_metric( 'ecommerce_missing_lms_user_id', self.id) monitoring_utils.set_custom_metric(missing_metric_key, self.id) error_msg = u'Could not find lms_user_id for user {user_id}. Called from {called_from}'.format( user_id=self.id, called_from=called_from) log.error(error_msg, exc_info=True) raise MissingLmsUserIdException(error_msg)
def _get_lms_user_id_from_social_auth(self): """ Return LMS user_id passed through social auth, if found. Returns None if not found. Side effect: If found, writes custom metric: 'lms_user_id_social_auth' """ try: lms_user_id_social_auth = self.social_auth.first().extra_data[u'user_id'] # pylint: disable=no-member if lms_user_id_social_auth: monitoring_utils.set_custom_metric('lms_user_id_social_auth', lms_user_id_social_auth) return lms_user_id_social_auth else: # pragma: no cover pass # allows coverage skip for just this case. except Exception: # pylint: disable=broad-except pass
def has_permission(self, request, view): """ Check for permissions by matching the configured API key and header Allow the request if and only if settings.EDX_API_KEY is set and the X-Edx-Api-Key HTTP header is present in the request and matches the setting. """ api_key = getattr(settings, "EDX_API_KEY", None) if api_key is not None and request.META.get( "HTTP_X_EDX_API_KEY") == api_key: audit_log("ApiKeyHeaderPermission used", path=request.path, ip=request.META.get("REMOTE_ADDR")) set_custom_metric('deprecated_api_key_header', True) return True return False
def _set_request_user_agent_metrics(self, request): """ Add metrics for user agent for python. Metrics: request_user_agent request_client_name: The client name from edx-rest-api-client calls. """ if 'HTTP_USER_AGENT' in request.META and request.META['HTTP_USER_AGENT']: user_agent = request.META['HTTP_USER_AGENT'] monitoring.set_custom_metric('request_user_agent', user_agent) if user_agent: # Example agent string from edx-rest-api-client: # python-requests/2.9.1 edx-rest-api-client/1.7.2 ecommerce # See https://github.com/edx/edx-rest-api-client/commit/692903c30b157f7a4edabc2f53aae1742db3a019 user_agent_parts = user_agent.split() if len(user_agent_parts) == 3 and user_agent_parts[1].startswith('edx-rest-api-client/'): monitoring.set_custom_metric('request_client_name', user_agent_parts[2])
def _get_jwt_builder(self, user, is_client_restricted): """ Creates and returns a JWTBuilder object for creating JWTs. """ # If JWT scope enforcement is enabled, we need to sign tokens # given to restricted applications with a key that # other IDAs do not have access to. This prevents restricted # applications from getting access to API endpoints available # on other IDAs which have not yet been protected with the # scope-related DRF permission classes. Once all endpoints have # been protected, we can enable all IDAs to use the same new # (asymmetric) key. # TODO: ARCH-162 use_asymmetric_key = ENFORCE_JWT_SCOPES.is_enabled() and is_client_restricted monitoring_utils.set_custom_metric('oauth_asymmetric_jwt', use_asymmetric_key) log.info("Using Asymmetric JWT: %s", use_asymmetric_key) return JwtBuilder( user, asymmetric=use_asymmetric_key, secret=settings.JWT_AUTH['JWT_SECRET_KEY'], issuer=settings.JWT_AUTH['JWT_ISSUER'], )
def get_adapter(self, request): """ Returns the appropriate adapter based on the OAuth client linked to the request. """ client_id = self._get_client_id(request) monitoring_utils.set_custom_metric('oauth_client_id', client_id) if dot_models.Application.objects.filter(client_id=client_id).exists(): monitoring_utils.set_custom_metric('oauth_adapter', 'dot') return self.dot_adapter else: monitoring_utils.set_custom_metric('oauth_adapter', 'dop') return self.dop_adapter
def _recalculate_subsection_grade(self, **kwargs): """ Updates a saved subsection grade. Keyword Arguments: user_id (int): id of applicable User object anonymous_user_id (int, OPTIONAL): Anonymous ID of the User course_id (string): identifying the course usage_id (string): identifying the course block only_if_higher (boolean): indicating whether grades should be updated only if the new raw_earned is higher than the previous value. expected_modified_time (serialized timestamp): indicates when the task was queued so that we can verify the underlying data update. score_deleted (boolean): indicating whether the grade change is a result of the problem's score being deleted. event_transaction_id (string): uuid identifying the current event transaction. event_transaction_type (string): human-readable type of the event at the root of the current event transaction. score_db_table (ScoreDatabaseTableEnum): database table that houses the changed score. Used in conjunction with expected_modified_time. """ try: course_key = CourseLocator.from_string(kwargs['course_id']) scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key) set_custom_metrics_for_course_key(course_key) set_custom_metric('usage_id', unicode(scored_block_usage_key)) # The request cache is not maintained on celery workers, # where this code runs. So we take the values from the # main request cache and store them in the local request # cache. This correlates model-level grading events with # higher-level ones. set_event_transaction_id(kwargs.get('event_transaction_id')) set_event_transaction_type(kwargs.get('event_transaction_type')) # Verify the database has been updated with the scores when the task was # created. This race condition occurs if the transaction in the task # creator's process hasn't committed before the task initiates in the worker # process. has_database_updated = _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs) if not has_database_updated: raise DatabaseNotReadyError _update_subsection_grades( course_key, scored_block_usage_key, kwargs['only_if_higher'], kwargs['user_id'], kwargs['score_deleted'], ) except Exception as exc: if not isinstance(exc, KNOWN_RETRY_ERRORS): log.info("tnl-6244 grades unexpected failure: {}. task id: {}. kwargs={}".format( repr(exc), self.request.id, kwargs, )) raise self.retry(kwargs=kwargs, exc=exc)
def change_enrollment(request, check_access=True): """ Modify the enrollment status for the logged-in user. TODO: This is lms specific and does not belong in common code. The request parameter must be a POST request (other methods return 405) that specifies course_id and enrollment_action parameters. If course_id or enrollment_action is not specified, if course_id is not valid, if enrollment_action is something other than "enroll" or "unenroll", if enrollment_action is "enroll" and enrollment is closed for the course, or if enrollment_action is "unenroll" and the user is not enrolled in the course, a 400 error will be returned. If the user is not logged in, 403 will be returned; it is important that only this case return 403 so the front end can redirect the user to a registration or login page when this happens. This function should only be called from an AJAX request, so the error messages in the responses should never actually be user-visible. Args: request (`Request`): The Django request object Keyword Args: check_access (boolean): If True, we check that an accessible course actually exists for the given course_key before we enroll the student. The default is set to False to avoid breaking legacy code or code with non-standard flows (ex. beta tester invitations), but for any standard enrollment flow you probably want this to be True. Returns: Response """ # Get the user user = request.user # Ensure the user is authenticated if not user.is_authenticated: return HttpResponseForbidden() # Ensure we received a course_id action = request.POST.get("enrollment_action") if 'course_id' not in request.POST: return HttpResponseBadRequest(_("Course id not specified")) try: course_id = CourseKey.from_string(request.POST.get("course_id")) except InvalidKeyError: log.warning( u"User %s tried to %s with invalid course id: %s", user.username, action, request.POST.get("course_id"), ) return HttpResponseBadRequest(_("Invalid course id")) # Allow us to monitor performance of this transaction on a per-course basis since we often roll-out features # on a per-course basis. monitoring_utils.set_custom_metric('course_id', text_type(course_id)) if action == "enroll": # Make sure the course exists # We don't do this check on unenroll, or a bad course id can't be unenrolled from if not modulestore().has_course(course_id): log.warning( u"User %s tried to enroll in non-existent course %s", user.username, course_id ) return HttpResponseBadRequest(_("Course id is invalid")) # Record the user's email opt-in preference if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'): _update_email_opt_in(request, course_id.org) available_modes = CourseMode.modes_for_course_dict(course_id) # Check whether the user is blocked from enrolling in this course # This can occur if the user's IP is on a global blacklist # or if the user is enrolling in a country in which the course # is not available. redirect_url = embargo_api.redirect_if_blocked( course_id, user=user, ip_address=get_ip(request), url=request.path ) if redirect_url: return HttpResponse(redirect_url) if CourseEntitlement.check_for_existing_entitlement_and_enroll(user=user, course_run_key=course_id): return HttpResponse(reverse('courseware', args=[unicode(course_id)])) # Check that auto enrollment is allowed for this course # (= the course is NOT behind a paywall) if CourseMode.can_auto_enroll(course_id): # Enroll the user using the default mode (audit) # We're assuming that users of the course enrollment table # will NOT try to look up the course enrollment model # by its slug. If they do, it's possible (based on the state of the database) # for no such model to exist, even though we've set the enrollment type # to "audit". try: enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes) if enroll_mode: CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode) except Exception: # pylint: disable=broad-except return HttpResponseBadRequest(_("Could not enroll")) # If we have more than one course mode or professional ed is enabled, # then send the user to the choose your track page. # (In the case of no-id-professional/professional ed, this will redirect to a page that # funnels users directly into the verification / payment flow) if CourseMode.has_verified_mode(available_modes) or CourseMode.has_professional_mode(available_modes): return HttpResponse( reverse("course_modes_choose", kwargs={'course_id': text_type(course_id)}) ) # Otherwise, there is only one mode available (the default) return HttpResponse() elif action == "unenroll": enrollment = CourseEnrollment.get_enrollment(user, course_id) if not enrollment: return HttpResponseBadRequest(_("You are not enrolled in this course")) certificate_info = cert_info(user, enrollment.course_overview) if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES: return HttpResponseBadRequest(_("Your certificate prevents you from unenrolling from this course")) CourseEnrollment.unenroll(user, course_id) REFUND_ORDER.send(sender=None, course_enrollment=enrollment) return HttpResponse() else: return HttpResponseBadRequest(_("Enrollment action is invalid"))
def _set_request_referer_metric(self, request): """ Add metric 'request_referer' for http referer. """ if 'HTTP_REFERER' in request.META and request.META['HTTP_REFERER']: monitoring.set_custom_metric('request_referer', request.META['HTTP_REFERER'])
def _annotate_for_monitoring(message_type, site, bin_num, target_day_str, day_offset): # This identifies the type of message being sent, for example: schedules.recurring_nudge3. set_custom_metric('message_name', '{0}.{1}'.format(message_type.app_label, message_type.name)) # The domain name of the site we are sending the message for. set_custom_metric('site', site.domain) # This is the "bin" of data being processed. We divide up the work into chunks so that we don't tie up celery # workers for too long. This could help us identify particular bins that are problematic. set_custom_metric('bin', bin_num) # The date we are processing data for. set_custom_metric('target_day', target_day_str) # The number of days relative to the current date to process data for. set_custom_metric('day_offset', day_offset) # A unique identifier for this batch of messages being sent. set_custom_metric('send_uuid', message_type.uuid)
def _annonate_send_task_for_monitoring(msg): # A unique identifier for this batch of messages being sent. set_custom_metric('send_uuid', msg.send_uuid) # A unique identifier for this particular message. set_custom_metric('uuid', msg.uuid)