def _verify_user(request, userid_in_session): """ Logs an error if the user marked at the time of process_request does not match either the current user in the request or the given userid_in_session. """ if hasattr(request, 'safe_cookie_verified_user_id'): if request.safe_cookie_verified_user_id != request.user.id: # The user at response time is expected to be None when the user # is logging out. To prevent extra noise in the logs, # conditionally set the log level. log_func = log.debug if request.user.id is None else log.warning log_func(( "SafeCookieData user at request '{0}' does not match user at response: '{1}' " "for request path '{2}'").format( request.safe_cookie_verified_user_id, request.user.id, request.path, ), ) set_custom_attribute("safe_sessions.user_mismatch", "request-response-mismatch") if request.safe_cookie_verified_user_id != userid_in_session: log.warning( ("SafeCookieData user at request '{0}' does not match user in session: '{1}' " "for request path '{2}'").format( # pylint: disable=logging-format-interpolation request.safe_cookie_verified_user_id, userid_in_session, request.path, ), ) set_custom_attribute("safe_sessions.user_mismatch", "request-session-mismatch")
def try_load_course(self, course_dir, course_ids=None, target_course_id=None): ''' Load a course, keeping track of errors as we go along. If course_ids is not None, then reject the course unless its id is in course_ids. ''' # Special-case code here, since we don't have a location for the # course before it loads. # So, make a tracker to track load-time errors, then put in the right # place after the course loads and we have its location errorlog = make_error_tracker() course_descriptor = None try: course_descriptor = self.load_course(course_dir, course_ids, errorlog.tracker, target_course_id) except Exception as exc: # pylint: disable=broad-except msg = "ERROR: Failed to load courselike '{}': {}".format( course_dir.encode("utf-8"), str(exc) ) set_custom_attribute('course_import_failure', f"Courselike load failure: {msg}") log.exception(msg) errorlog.tracker(msg) self.errored_courses[course_dir] = errorlog if course_descriptor is None: pass elif isinstance(course_descriptor, ErrorBlock): # Didn't load course. Instead, save the errors elsewhere. self.errored_courses[course_dir] = errorlog else: self.courses[course_dir] = course_descriptor course_descriptor.parent = None course_id = self.id_from_descriptor(course_descriptor) self._course_errors[course_id] = errorlog
def _on_user_authentication_failed(request): """ To be called when user authentication fails when processing requests in the middleware. Sets a flag to delete the user's cookie and redirects the user to the login page. """ _mark_cookie_for_deletion(request) # Mobile apps have custom handling of authentication failures. They # should *not* be redirected to the website's login page. if is_request_from_mobile_app(request): return HttpResponse(status=401) # .. toggle_name: REDIRECT_TO_LOGIN_ON_SAFE_SESSION_AUTH_FAILURE # .. toggle_implementation: SettingToggle # .. toggle_default: True # .. toggle_description: Turn this toggle off to roll out new functionality, # which returns a 401 rather than redirecting to login, when HTML is not expected by the client. # .. toggle_use_cases: temporary # .. toggle_creation_date: 2021-10-18 # .. toggle_target_removal_date: 2021-10-22 # .. toggle_tickets: https://openedx.atlassian.net/browse/ARCHBOM-1911 REDIRECT_TO_LOGIN_ON_SAFE_SESSION_AUTH_FAILURE = getattr( settings, 'REDIRECT_TO_LOGIN_ON_SAFE_SESSION_AUTH_FAILURE', True) if REDIRECT_TO_LOGIN_ON_SAFE_SESSION_AUTH_FAILURE or 'text/html' in request.META.get( 'HTTP_ACCEPT', ''): set_custom_attribute("safe_sessions.auth_failure", "redirect_to_login") return redirect_to_login(request.path) set_custom_attribute("safe_sessions.auth_failure", "401") return HttpResponse(status=401)
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_attribute(namespaced_flag_name, value) return value value = self._cached_flags.get(namespaced_flag_name) if value is not None: self._set_waffle_flag_attribute(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_attribute(namespaced_flag_name, value) set_custom_attribute('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_attribute(namespaced_flag_name, value) return value
def _set_code_owner_attribute_from_current_transaction(self, request): """ Uses the current transaction name to set the code owner attribute. Side-effects: Sets code_owner_transaction_name custom attribute, used to determine code_owner Returns: (str, str): (code_owner, error_message), where at least one of these should be None """ if not is_code_owner_mappings_configured(): return None, None try: # Example: openedx.core.djangoapps.contentserver.middleware:StaticContentServer transaction_name = get_current_transaction().name if not transaction_name: return None, 'No current transaction name found.' set_custom_attribute('code_owner_transaction_name', transaction_name) module_name = transaction_name.split(':')[0] code_owner = get_code_owner_from_module(module_name) return code_owner, None except Exception as e: # pylint: disable=broad-except return None, str(e)
def _verify_user(request, userid_in_session): """ Logs an error if the user marked at the time of process_request does not match either the current user in the request or the given userid_in_session. """ if hasattr(request, 'safe_cookie_verified_user_id'): if hasattr(request.user, 'real_user'): # If a view overrode the request.user with a masqueraded user, this will # revert/clean-up that change during response processing. request.user = request.user.real_user # The user at response time is expected to be None when the user # is logging out. We won't log that. if request.safe_cookie_verified_user_id != request.user.id and request.user.id is not None: log.warning( ("SafeCookieData user at request '{}' does not match user at response: '{}' " "for request path '{}'").format( # pylint: disable=logging-format-interpolation request.safe_cookie_verified_user_id, request.user.id, request.path, ), ) set_custom_attribute("safe_sessions.user_mismatch", "request-response-mismatch") # The user session at response time is expected to be None when the user # is logging out. We won't log that. if request.safe_cookie_verified_user_id != userid_in_session and userid_in_session is not None: log.warning( ("SafeCookieData user at request '{}' does not match user in session: '{}' " "for request path '{}'").format( # pylint: disable=logging-format-interpolation request.safe_cookie_verified_user_id, userid_in_session, request.path, ), ) set_custom_attribute("safe_sessions.user_mismatch", "request-session-mismatch")
def _check_user_auth_flow(site, user): """ Check if user belongs to an allowed domain and not whitelisted then ask user to login through allowed domain SSO provider. """ if user and ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY.is_enabled(): allowed_domain = site.configuration.get_value( 'THIRD_PARTY_AUTH_ONLY_DOMAIN', '').lower() email_parts = user.email.split('@') if len(email_parts) != 2: # User has a nonstandard email so we record their id. # we don't record their e-mail in case there is sensitive info accidentally # in there. set_custom_attribute('login_tpa_domain_shortcircuit_user_id', user.id) log.warn( "User %s has nonstandard e-mail. Shortcircuiting THIRD_PART_AUTH_ONLY_DOMAIN check.", user.id) return user_domain = email_parts[1].strip().lower() # If user belongs to allowed domain and not whitelisted then user must login through allowed domain SSO if user_domain == allowed_domain and not AllowedAuthUser.objects.filter( site=site, email=user.email).exists(): if not should_redirect_to_logistration_mircrofrontend(): msg = _create_message(site, None, allowed_domain) else: root_url = configuration_helpers.get_value( 'LMS_ROOT_URL', settings.LMS_ROOT_URL) msg = _create_message(site, root_url, allowed_domain) raise AuthFailedError(msg)
def _set_request_auth_type_guess_attribute(self, request): """ Add custom attribute 'request_auth_type_guess' 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 jwt-cookie session-or-other (catch all) """ if not hasattr(request, 'user') or not request.user: auth_type = 'no-user' elif not request.user.is_authenticated: auth_type = 'unauthenticated' elif '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 USE_JWT_COOKIE_HEADER in request.META and jwt_cookie_name( ) in request.COOKIES: auth_type = 'jwt-cookie' else: auth_type = 'session-or-other' monitoring.set_custom_attribute('request_auth_type_guess', auth_type)
def _set_request_referer_attribute(self, request): """ Add custom attribute 'request_referer' for http referer. """ if 'HTTP_REFERER' in request.META and request.META['HTTP_REFERER']: monitoring.set_custom_attribute('request_referer', request.META['HTTP_REFERER'])
def associate_by_email_if_login_api(auth_entry, backend, details, user, current_partial=None, *args, **kwargs): # lint-amnesty, pylint: disable=keyword-arg-before-vararg """ This pipeline step associates the current social auth with the user with the same email address in the database. It defers to the social library's associate_by_email implementation, which verifies that only a single database user is associated with the email. This association is done ONLY if the user entered the pipeline through a LOGIN API. """ if auth_entry == AUTH_ENTRY_LOGIN_API: # Temporary custom attribute to help ensure there is no usage. set_custom_attribute('deprecated_auth_entry_login_api', True) association_response = associate_by_email(backend, details, user, *args, **kwargs) if (association_response and association_response.get('user') and association_response['user'].is_active): # Only return the user matched by email if their email has been activated. # Otherwise, an illegitimate user can create an account with another user's # email address and the legitimate user would now login to the illegitimate # account. return association_response
def _check_user_auth_flow(site, user): """ Check if user belongs to an allowed domain and not whitelisted then ask user to login through allowed domain SSO provider. """ if user and ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY.is_enabled(): allowed_domain = site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_DOMAIN', '').lower() email_parts = user.email.split('@') if len(email_parts) != 2: # User has a nonstandard email so we record their id. # we don't record their e-mail in case there is sensitive info accidentally # in there. set_custom_attribute('login_tpa_domain_shortcircuit_user_id', user.id) log.warn("User %s has nonstandard e-mail. Shortcircuiting THIRD_PART_AUTH_ONLY_DOMAIN check.", user.id) return user_domain = email_parts[1].strip().lower() # If user belongs to allowed domain and not whitelisted then user must login through allowed domain SSO if user_domain == allowed_domain and not AllowedAuthUser.objects.filter(site=site, email=user.email).exists(): msg = Text(_( u'As {allowed_domain} user, You must login with your {allowed_domain} ' u'{link_start}{provider} account{link_end}.' )).format( allowed_domain=allowed_domain, link_start=HTML("<a href='{tpa_provider_link}'>").format( tpa_provider_link='{dashboard_url}?tpa_hint={tpa_hint}'.format( dashboard_url=reverse('dashboard'), tpa_hint=site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_HINT'), ) ), provider=site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_PROVIDER'), link_end=HTML("</a>") ) raise AuthFailedError(msg)
def import_waffle_switch(): """ Helper method that imports WaffleSwitch from edx-platform at runtime. WARNING: This method is now deprecated and should not be relied upon. """ set_custom_attribute("deprecated_edx_ora2", "import_waffle_switch") return WaffleSwitch
def authenticate(self, request): try: user_and_auth = super().authenticate(request) # Unauthenticated, CSRF validation not required if not user_and_auth: return user_and_auth # Not using JWT cookies, CSRF validation not required use_jwt_cookie_requested = request.META.get(USE_JWT_COOKIE_HEADER) if not use_jwt_cookie_requested: return user_and_auth self.enforce_csrf(request) # CSRF passed validation with authenticated user return user_and_auth except jwt.InvalidTokenError as token_error: # Note: I think this case is not used, but will monitor the custom attribute to verify. set_custom_attribute( 'jwt_auth_failed', 'InvalidTokenError:{}'.format(repr(token_error))) raise exceptions.AuthenticationFailed() from token_error except Exception as exception: # Errors in production do not need to be logged (as they may be noisy), # but debug logging can help quickly resolve issues during development. logger.debug('Failed JWT Authentication,', exc_info=exception) # Note: I think this case should only include AuthenticationFailed and PermissionDenied, # but will monitor the custom attribute to verify. set_custom_attribute('jwt_auth_failed', 'Exception:{}'.format(repr(exception))) raise
def _update_publish_report(course_outline: CourseOutlineData, content_errors: List[ContentErrorData], course_context: CourseContext): """ Record ContentErrors for this course publish. Deletes previous errors. """ set_custom_attribute('learning_sequences.api.num_content_errors', len(content_errors)) learning_context = course_context.learning_context try: # Normal path if we're updating a PublishReport publish_report = learning_context.publish_report publish_report.num_errors = len(content_errors) publish_report.num_sections = len(course_outline.sections) publish_report.num_sequences = len(course_outline.sequences) publish_report.content_errors.all().delete() except PublishReport.DoesNotExist: # Case where we're creating it for the first time. publish_report = PublishReport( learning_context=learning_context, num_errors=len(content_errors), num_sections=len(course_outline.sections), num_sequences=len(course_outline.sequences), ) publish_report.save() publish_report.content_errors.bulk_create([ ContentError( publish_report=publish_report, usage_key=error_data.usage_key, message=error_data.message, ) for error_data in content_errors ])
def authenticate(self, request): set_custom_attribute("BearerAuthentication", "Failed") # default value if not self.get_user_info_url(): logger.warning('The setting OAUTH2_USER_INFO_URL is invalid!') set_custom_attribute("BearerAuthentication", "NoURL") return None set_custom_attribute("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_attribute("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_attribute("BearerAuthentication", "Success") return output
def replace_course_outline(course_outline: CourseOutlineData, content_errors: Optional[List[ContentErrorData]] = None): """ Replace the model data stored for the Course Outline with the contents of course_outline (a CourseOutlineData). Record any content errors. This isn't particularly optimized at the moment. """ log.info( "Replacing CourseOutline for %s (version %s, %d sequences)", course_outline.course_key, course_outline.published_version, len(course_outline.sequences) ) set_custom_attribute('learning_sequences.api.course_id', str(course_outline.course_key)) if content_errors is None: content_errors = [] with transaction.atomic(): # Update or create the basic CourseContext... course_context = _update_course_context(course_outline) # Wipe out the CourseSectionSequences join+ordering table course_context.section_sequences.all().delete() _update_sections(course_outline, course_context) _update_sequences(course_outline, course_context) _update_course_section_sequences(course_outline, course_context) _update_publish_report(course_outline, content_errors, course_context)
def _monitor_value(self, flag_name, value): """ Monitoring method preserved for backward compatibility. You should use `WaffleFlag.set_monitor_value` instead. """ set_custom_attribute("deprecated_edx_toggles_waffle", "WaffleFlagNamespace._monitor_value") return NewWaffleFlag(self._namespaced_name(flag_name), module_name=__name__).set_monitor_value(value)
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_attribute('oauth_client_id', client_id) return self.dot_adapter
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_attribute('jwt_expires_in', expires_in) return now, now + expires_in
def __init__(self, waffle_namespace, switch_name, module_name=None): if not isinstance(waffle_namespace, str): waffle_namespace = waffle_namespace.name set_custom_attribute("deprecated_edx_toggles_waffle", "WaffleSwitch") self._switch_name = switch_name name = "{}.{}".format(waffle_namespace, switch_name) super().__init__(name, module_name=module_name)
def __init__(self, name, log_prefix=None): super().__init__(name, log_prefix=log_prefix) warnings.warn( "Importing WaffleFlagNamespace from waffle_utils is deprecated. Instead, import from edx_toggles.toggles.", DeprecationWarning, stacklevel=2, ) set_custom_attribute("deprecated_waffle_utils", "WaffleFlagNamespace[{}]".format(name))
def set_monitor_value(self, _value): """ This used to send waffle flag values to monitoring, but is now a no-op. This method is preserved for backward compatibility. """ set_custom_attribute( "deprecated_waffle_method", "WaffleFlag[{}].set_monitor_value".format(self.name), )
def __init__(self, waffle_namespace, flag_name, module_name=None): super().__init__(waffle_namespace, flag_name, module_name=module_name) warnings.warn( "Importing WaffleFlag from waffle_utils is deprecated. Instead, import from edx_toggles.toggles.", DeprecationWarning, stacklevel=2, ) set_custom_attribute("deprecated_waffle_utils", "WaffleFlag[{}]".format(self.name))
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('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('Number of schedules = %d', num_schedules) # This should give us a sense of the volume of data being processed by each task. set_custom_attribute('num_schedules', num_schedules) return schedules
def warn_deprecated_import(old_import, new_import): """ Warn that a module is being imported from its old location. """ set_custom_attribute("deprecated_edx_platform_import", old_import) warnings.warn( DeprecatedEdxPlatformImportWarning(old_import, new_import), stacklevel=3, # Should surface the line that is doing the importing. )
def namespaced_flag_name(self): """ Preserved for backward compatibility. """ set_custom_attribute( "deprecated_waffle_legacy_method", f"WaffleFlag[{self.name}].namespaced_flag_name", ) return self.name
def _cached_flags(self): """ Legacy property used by CourseWaffleFlag. """ set_custom_attribute( "deprecated_waffle_legacy_method", f"WaffleFlagNamespace[{self.name}]._cached_flags", ) return NewWaffleFlag.cached_flags()
def _monitor_value(self, _flag_name, _value): """ Monitoring method preserved for backward compatibility. This is a no-op now that `WaffleFlag.set_monitor_value` is deprecated. """ set_custom_attribute( "deprecated_waffle_legacy_method", f"WaffleFlagNamespace[{self.name}]._monitor_value", )
def namespaced_switch_name(self): """ This is now equivalent to the switch name. """ set_custom_attribute( "deprecated_waffle_legacy_method", f"WaffleSwitch[{self.name}].namespaced_switch_name", ) return self.name
def switch_name(self): """ Non-namespaced switch_name attribute preserved for backward compatibility. """ set_custom_attribute( "deprecated_waffle_legacy_method", f"WaffleSwitch[{self.name}].switch_name", ) return self._switch_name