diff --git a/lms/envs/common.py b/lms/envs/common.py index 8a851697c6d6..990e137754fd 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3286,6 +3286,13 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring ######################### Django Rest Framework ######################## REST_FRAMEWORK = { + # This matches the original DRF default of Session and Basic Authentication, but + # adds observability to help us potentially adjust the defaults. We would like to + # add JwtAuthentication and drop BasicAuthentication, based on our findings. + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'openedx.core.djangolib.default_auth_classes.DefaultSessionAuthentication', + 'openedx.core.djangolib.default_auth_classes.DefaultBasicAuthentication' + ], 'DEFAULT_PAGINATION_CLASS': 'edx_rest_framework_extensions.paginators.DefaultPagination', 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', @@ -3303,7 +3310,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # .. setting_name: REGISTRATION_VALIDATION_RATELIMIT # .. setting_default: 30/7d -# .. setting_description: Whenver a user tries to register on edx, the data entered during registration +# .. setting_description: Whenever a user tries to register on edx, the data entered during registration # is validated via RegistrationValidationView. # It's POST endpoint is rate-limited up to 30 requests per IP Address in a week by default. # It was introduced because an attacker can guess or brute force a series of names to enumerate valid users. diff --git a/openedx/core/djangolib/default_auth_classes.py b/openedx/core/djangolib/default_auth_classes.py new file mode 100644 index 000000000000..238993c7aa8a --- /dev/null +++ b/openedx/core/djangolib/default_auth_classes.py @@ -0,0 +1,87 @@ +""" +Default Authentication classes that are ONLY meant to be used by +DEFAULT_AUTHENTICATION_CLASSES for observability purposes. +""" +from edx_django_utils.monitoring import set_custom_attribute +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from rest_framework.authentication import BasicAuthentication, SessionAuthentication + + +class DefaultSessionAuthentication(SessionAuthentication): + """ Default SessionAuthentication with observability """ + + def authenticate(self, request): + # .. custom_attribute_name: using_default_auth_classes + # .. custom_attribute_description: This custom attribute will always be + # True (if not NULL), and signifies that a default authentication + # class was used. This can be used to find endpoints using the + # default authentication classes. + set_custom_attribute('using_default_auth_classes', True) + + try: + user_and_auth = super().authenticate(request) + if user_and_auth: + # .. custom_attribute_name: session_auth_result + # .. custom_attribute_description: The result of session auth, represented + # by: 'success', 'failure', or 'skipped'. + set_custom_attribute('session_auth_result', 'success') + else: + set_custom_attribute('session_auth_result', 'skipped') + return user_and_auth + except Exception as exception: + set_custom_attribute('session_auth_result', 'failure') + raise + + +class DefaultBasicAuthentication(BasicAuthentication): + """ + Default BasicAuthentication with observability + + Note that BasicAuthentication was a default because it was a DRF default. + Observability will be used to determine if BasicAuthentication could + instead be dropped as a default. + """ + + def authenticate(self, request): + # .. custom_attribute_name: using_default_auth_classes + # .. custom_attribute_description: This custom attribute will always be + # True (if not NULL), and signifies that a default authentication + # class was used. This can be used to find endpoints using the + # default authentication classes. + set_custom_attribute('using_default_auth_classes', True) + + try: + user_and_auth = super().authenticate(request) + if user_and_auth: + # .. custom_attribute_name: basic_auth_result + # .. custom_attribute_description: The result of basic auth, represented + # by: 'success', 'failure', or 'skipped'. + set_custom_attribute('basic_auth_result', 'success') + else: + set_custom_attribute('basic_auth_result', 'skipped') + return user_and_auth + except Exception as exception: + set_custom_attribute('basic_auth_result', 'failure') + raise + + +class DefaultJwtAuthentication(JwtAuthentication): + """ + Default JwtAuthentication with observability + + Note that the plan is to add JwtAuthentication as a default, but it + is not yet used. This class will be used during the transition. + """ + + def authenticate(self, request): + # .. custom_attribute_name: using_default_auth_classes + # .. custom_attribute_description: This custom attribute will always be + # True (if not NULL), and signifies that a default authentication + # class was used. This can be used to find endpoints using the + # default authentication classes. + set_custom_attribute('using_default_auth_classes', True) + + # Unlike the other DRF authentication classes, JwtAuthentication already + # includes a jwt_auth_result custom attribute, so we do not need to + # reimplement that observability in this class. + return super().authenticate(request)