diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 51b9939da5..0ef9909242 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,10 @@ Unreleased * nothing unreleased +[8.0.6] - 2026-05-05 +--------------------- +* fix: gate plugin_settings on ENABLE_ENTERPRISE_INTEGRATION + [8.0.5] - 2026-05-04 --------------------- * feat: add TPA pipeline step and social auth disconnect handler diff --git a/consent/settings/common.py b/consent/settings/common.py index 2e4668eb4d..47c96450ac 100644 --- a/consent/settings/common.py +++ b/consent/settings/common.py @@ -8,7 +8,7 @@ def plugin_settings(settings): # pylint: disable=unused-argument Override platform settings for the consent app. This is called by the Open edX plugin system during LMS/CMS startup. Add - any Django settings overrides here (e.g. ``settings.FEATURES['...'] = True``). + any Django settings overrides here (e.g. ``settings.SOME_FLAG = True``). Args: settings: The Django settings module being configured. diff --git a/enterprise/__init__.py b/enterprise/__init__.py index 9d947a2989..9e15c049e6 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "8.0.5" +__version__ = "8.0.6" diff --git a/enterprise/admin/__init__.py b/enterprise/admin/__init__.py index e7fe568c87..4e2c4f9c2c 100644 --- a/enterprise/admin/__init__.py +++ b/enterprise/admin/__init__.py @@ -776,8 +776,7 @@ def has_delete_permission(self, request, obj=None): """ Disable deletion for EnterpriseCourseEnrollment. """ - features = getattr(settings, 'FEATURES', {}) - return features.get(constants.ALLOW_ADMIN_ENTERPRISE_COURSE_ENROLLMENT_DELETION, False) + return getattr(settings, constants.ALLOW_ADMIN_ENTERPRISE_COURSE_ENROLLMENT_DELETION, False) def changelist_view(self, request, extra_context=None): """ diff --git a/enterprise/settings/common.py b/enterprise/settings/common.py index a29402e20c..a935fb376a 100644 --- a/enterprise/settings/common.py +++ b/enterprise/settings/common.py @@ -8,11 +8,15 @@ def plugin_settings(settings): Override platform settings for the enterprise app. This is called by the Open edX plugin system during LMS/CMS startup. Add - any Django settings overrides here (e.g. ``settings.FEATURES['...'] = True``). + any Django settings overrides here (e.g. ``settings.SOME_FLAG = True``). Args: settings: The Django settings module being configured. """ + # Skip injecting ANY default enterprise settings if the enterprise feature is entirely disabled. + if not getattr(settings, 'ENABLE_ENTERPRISE_INTEGRATION', False): + return + pipeline = getattr(settings, 'SOCIAL_AUTH_PIPELINE', None) if pipeline is not None: email_step = 'enterprise.tpa_pipeline.enterprise_associate_by_email' diff --git a/enterprise/settings/test.py b/enterprise/settings/test.py index 5e359aed4d..cee10270ca 100644 --- a/enterprise/settings/test.py +++ b/enterprise/settings/test.py @@ -9,6 +9,7 @@ from os.path import abspath, dirname, join from celery import Celery +from edx_django_utils.plugins import add_plugins from enterprise.constants import ( DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE, @@ -25,6 +26,8 @@ SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE, ) +############################ BASE LMS TEST SETTINGS ############################ +# Only non-enterprise settings live in this section. def here(*args): """ @@ -62,8 +65,11 @@ def root(*args): "django.contrib.staticfiles", "waffle", + # Force install enterprise and consent. This is automatically accomplished + # in prod via stevedore, but we need to do it manually here for unit tests. "enterprise", "consent", + "integrated_channels.integrated_channel", "integrated_channels.cornerstone", "integrated_channels.degreed", @@ -143,19 +149,6 @@ def root(*args): LMS_INTERNAL_ROOT_URL = "http://localhost:8000" LMS_ENROLLMENT_API_PATH = "/api/enrollment/v1/" ECOMMERCE_PUBLIC_URL_ROOT = "http://localhost:18130" -ENTERPRISE_CATALOG_INTERNAL_ROOT_URL = "http://localhost:18160" - -ENTERPRISE_ENROLLMENT_API_URL = LMS_INTERNAL_ROOT_URL + LMS_ENROLLMENT_API_PATH - -ENTERPRISE_LEARNER_PORTAL_BASE_URL = 'http://localhost:8734' - -ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = ENTERPRISE_ENROLLMENT_API_URL - -ENTERPRISE_API_CACHE_TIMEOUT = 60 - -ENTERPRISE_SUPPORT_URL = "http://foo" - -ENTERPRISE_TAGLINE = "High-quality online learning opportunities from the world's best universities" OAUTH_ID_TOKEN_EXPIRATION = 60 * 60 # in seconds @@ -186,22 +179,7 @@ def root(*args): ALLOWED_HOSTS = ["testserver.enterprise"] MEDIA_URL = "/" -# Defines the usernames of service users who should be throttled -# at a higher rate than normal users. -ENTERPRISE_ALL_SERVICE_USERNAMES = [ - 'ecommerce_worker', - 'enterprise_worker', - 'license-manager_worker', - 'enterprise-catalog_worker', - 'enterprise-subsidy_worker', -] - ECOMMERCE_SERVICE_WORKER_USERNAME = 'ecommerce_worker' -ENTERPRISE_SERVICE_WORKER_USERNAME = 'enterprise_worker' - -ENTERPRISE_CUSTOMER_LOGO_IMAGE_SIZE = 512 # Enterprise logo image size limit in KB's - -ENTERPRISE_COURSE_ENROLLMENT_AUDIT_MODES = ['audit', 'honor'] # These are standard regexes for pulling out info like course_ids, usage_ids, etc. COURSE_KEY_PATTERN = r'(?P[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)' @@ -211,31 +189,8 @@ def root(*args): TIME_ZONE = 'UTC' -# Business logic should be allowed to assume that FEATURES is set. In a -# running app, it's set by the platform, but in enterprise unit tests we'll -# just seed one here. -FEATURES = { - 'ENABLE_ENTERPRISE_INTEGRATION': True, -} - MKTG_URLS = {} -ENTERPRISE_CUSTOMER_CATALOG_DEFAULT_CONTENT_FILTER = { - 'content_type': 'course', - 'partner': 'edx', - 'level_type': [ - 'Introductory', - 'Intermediate', - 'Advanced' - ], - 'availability': [ - 'Current', - 'Starting Soon', - 'Upcoming' - ], - 'status': 'published' -} - SNOWFLAKE_SERVICE_USER = 'TEST@EDX.ORG' SNOWFLAKE_SERVICE_USER_PASSWORD = 'secret' @@ -282,8 +237,6 @@ def root(*args): }, } -################################### TRACKING ################################### - LMS_SEGMENT_KEY = 'SOME_KEY' EVENT_TRACKING_ENABLED = True EVENT_TRACKING_BACKENDS = { @@ -306,18 +259,13 @@ def root(*args): } EVENT_TRACKING_PROCESSORS = [] -#################################### CELERY #################################### - +# Celery settings app = Celery('enterprise') app.conf.task_protocol = 1 app.config_from_object('django.conf:settings') - CELERY_ALWAYS_EAGER = True - CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION = False -##### END CELERY ##### - JWT_AUTH = { 'JWT_AUDIENCE': 'test-aud', 'JWT_DECODE_HANDLER': 'edx_rest_framework_extensions.auth.jwt.decoder.jwt_decode_handler', @@ -346,17 +294,9 @@ def root(*args): } -INTEGRATED_CHANNELS_API_CHUNK_TRANSMISSION_LIMIT = { - 'SAP': 1, -} - LANGUAGE_COOKIE_NAME = "openedx-language-preference" SHARED_COOKIE_DOMAIN = '' -ENTERPRISE_BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL = f'{LMS_INTERNAL_ROOT_URL}/oauth2' -ENTERPRISE_BACKEND_SERVICE_EDX_OAUTH2_KEY = 'test_backend_oauth2_key' -ENTERPRISE_BACKEND_SERVICE_EDX_OAUTH2_SECRET = 'test_backend_oauth2_secret' - ECOMMERCE_API_URL = 'https://ecommerce.example.com/api/v2/' LOGIN_REDIRECT_WHITELIST = [ @@ -366,13 +306,118 @@ def root(*args): 'facebook.com' ] -ENTERPRISE_MANUAL_REPORTING_CUSTOMER_UUIDS = ['12aacfee8ffa4cb3bed1059565a57f06',] EXEC_ED_LANDING_PAGE = 'https://www.edx-external.com/account' - # disable indexing on history_date. Otherwise it will add new alter migrations. SIMPLE_HISTORY_DATE_INDEX = False +STORAGES = { + 'default': { + 'BACKEND': 'django.core.files.storage.FileSystemStorage', + }, + 'staticfiles': { + 'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage', + }, +} + +# Enterprise-free social auth / TPA pipeline which is conditionally augmented by add_plugins() below. +SOCIAL_AUTH_PIPELINE = [ + 'common.djangoapps.third_party_auth.pipeline.parse_query_params', + 'social_core.pipeline.social_auth.social_details', + 'social_core.pipeline.social_auth.social_uid', + 'social_core.pipeline.social_auth.auth_allowed', + 'social_core.pipeline.social_auth.social_user', + 'common.djangoapps.third_party_auth.pipeline.associate_by_email_if_login_api', + 'common.djangoapps.third_party_auth.pipeline.associate_by_email_if_oauth', + 'common.djangoapps.third_party_auth.pipeline.get_username', + 'common.djangoapps.third_party_auth.pipeline.set_pipeline_timeout', + 'common.djangoapps.third_party_auth.pipeline.ensure_user_information', + 'social_core.pipeline.user.create_user', + 'social_core.pipeline.social_auth.associate_user', + 'social_core.pipeline.social_auth.load_extra_data', + 'social_core.pipeline.user.user_details', + 'common.djangoapps.third_party_auth.pipeline.user_details_force_sync', + 'common.djangoapps.third_party_auth.pipeline.set_id_verification_status', + 'common.djangoapps.third_party_auth.pipeline.set_logged_in_cookies', + 'common.djangoapps.third_party_auth.pipeline.login_analytics', + 'common.djangoapps.third_party_auth.pipeline.ensure_redirect_url_is_safe', +] + +# Force the enterprise integration to be enabled so that plugin_settings() will activate. +ENABLE_ENTERPRISE_INTEGRATION = True + +##### END BASE LMS TEST SETTINGS ##### + +########################### PLUGIN COMMON SETTINGS ############################# + +# Apply plugin_settings() from every plugin registered in this repo's setup.py +# entry points so that all tests automatically pick up the post-startup state +# that openedx-platform sees in production. +# Notes: +# - We apply the "common" settings_type which seeds most of the basic enterprise settings needed. +# - Must be applied **after** LMS settings but **before** enterprise-specific test overrides. +add_plugins(__name__, 'lms.djangoapp', 'common') + +##### END PLUGIN COMMON SETTINGS ##### + +###################### ENTERPRISE-SPECIFIC TEST OVERRIDES ####################### + +ENTERPRISE_CATALOG_INTERNAL_ROOT_URL = "http://localhost:18160" + +ENTERPRISE_ENROLLMENT_API_URL = LMS_INTERNAL_ROOT_URL + LMS_ENROLLMENT_API_PATH + +ENTERPRISE_LEARNER_PORTAL_BASE_URL = 'http://localhost:8734' + +ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = ENTERPRISE_ENROLLMENT_API_URL + +ENTERPRISE_API_CACHE_TIMEOUT = 60 + +ENTERPRISE_SUPPORT_URL = "http://foo" + +ENTERPRISE_TAGLINE = "High-quality online learning opportunities from the world's best universities" + +ENTERPRISE_SERVICE_WORKER_USERNAME = 'enterprise_worker' + +ENTERPRISE_CUSTOMER_LOGO_IMAGE_SIZE = 512 # Enterprise logo image size limit in KB's + +ENTERPRISE_COURSE_ENROLLMENT_AUDIT_MODES = ['audit', 'honor'] + +ENTERPRISE_CUSTOMER_CATALOG_DEFAULT_CONTENT_FILTER = { + 'content_type': 'course', + 'partner': 'edx', + 'level_type': [ + 'Introductory', + 'Intermediate', + 'Advanced' + ], + 'availability': [ + 'Current', + 'Starting Soon', + 'Upcoming' + ], + 'status': 'published' +} + +# Defines the usernames of service users who should be throttled +# at a higher rate than normal users. +ENTERPRISE_ALL_SERVICE_USERNAMES = [ + 'ecommerce_worker', + 'enterprise_worker', + 'license-manager_worker', + 'enterprise-catalog_worker', + 'enterprise-subsidy_worker', +] + +INTEGRATED_CHANNELS_API_CHUNK_TRANSMISSION_LIMIT = { + 'SAP': 1, +} + +ENTERPRISE_BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL = f'{LMS_INTERNAL_ROOT_URL}/oauth2' +ENTERPRISE_BACKEND_SERVICE_EDX_OAUTH2_KEY = 'test_backend_oauth2_key' +ENTERPRISE_BACKEND_SERVICE_EDX_OAUTH2_SECRET = 'test_backend_oauth2_secret' + +ENTERPRISE_MANUAL_REPORTING_CUSTOMER_UUIDS = ['12aacfee8ffa4cb3bed1059565a57f06',] + CHAT_COMPLETION_API_V2 = 'https://example.com/chat/completion' ENTERPRISE_ANALYSIS_CLIENT_ID = 'test_client_id' ENTERPRISE_ANALYSIS_SYSTEM_PROMPT = 'This is a test prompt' @@ -395,15 +440,6 @@ def root(*args): BRAZE_ADMIN_INVITE_CAMPAIGN_ID = 'test-admin-invite-campaign-id' BRAZE_LEARNER_INVITE_CAMPAIGN_ID = 'test-learner-invite-campaign-id' -STORAGES = { - 'default': { - 'BACKEND': 'django.core.files.storage.FileSystemStorage', - }, - 'staticfiles': { - 'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage', - }, -} - ENTERPRISE_ADMIN_PORTAL_BASE_URL = 'http://localhost:1991' # Admin invite reminder settings @@ -432,3 +468,5 @@ def root(*args): # addresses (including cloud metadata endpoints like 169.254.169.254) are always # blocked regardless of this setting. SAML_METADATA_URL_ALLOW_PRIVATE_IPS = False + +##### END ENTERPRISE-SPECIFIC TEST OVERRIDES ##### diff --git a/enterprise/signals.py b/enterprise/signals.py index 4577b4833c..1d2b99ba55 100644 --- a/enterprise/signals.py +++ b/enterprise/signals.py @@ -552,6 +552,6 @@ def handle_social_auth_disconnect( saml_backend: the SAML auth backend instance. **kwargs: forward-compatible catch-all. """ - if not settings.FEATURES.get('ENABLE_ENTERPRISE_INTEGRATION', False): + if not getattr(settings, 'ENABLE_ENTERPRISE_INTEGRATION', False): return _unlink_enterprise_user_from_idp(request, user, saml_backend.name) diff --git a/enterprise/tpa_pipeline.py b/enterprise/tpa_pipeline.py index 94fb74db81..f2721afd83 100644 --- a/enterprise/tpa_pipeline.py +++ b/enterprise/tpa_pipeline.py @@ -50,7 +50,7 @@ def enterprise_associate_by_email(strategy, details, user=None, *args, **kwargs) ENT-11577: This step replaces the now-defunct ``associate_by_email_if_saml`` step from openedx-platform. """ - if not getattr(settings, 'FEATURES', {}).get('ENABLE_ENTERPRISE_INTEGRATION', False): + if not getattr(settings, 'ENABLE_ENTERPRISE_INTEGRATION', False): return None def associate_by_email_if_enterprise_user(current_user, current_provider): diff --git a/tests/test_enterprise/test_signals.py b/tests/test_enterprise/test_signals.py index 2dc85c6c2e..366c2fce27 100644 --- a/tests/test_enterprise/test_signals.py +++ b/tests/test_enterprise/test_signals.py @@ -1325,7 +1325,7 @@ class TestHandleSocialAuthDisconnect(unittest.TestCase): Tests for handle_social_auth_disconnect signal handler. """ - @override_settings(FEATURES={'ENABLE_ENTERPRISE_INTEGRATION': True}) + @override_settings(ENABLE_ENTERPRISE_INTEGRATION=True) @mock.patch( 'enterprise.signals._unlink_enterprise_user_from_idp', ) @@ -1345,7 +1345,7 @@ def test_calls_unlink(self, mock_unlink): ) mock_unlink.assert_called_once_with(request, user, saml_backend.name) - @override_settings(FEATURES={'ENABLE_ENTERPRISE_INTEGRATION': False}) + @override_settings(ENABLE_ENTERPRISE_INTEGRATION=False) @mock.patch( 'enterprise.signals._unlink_enterprise_user_from_idp', ) diff --git a/tests/test_settings.py b/tests/test_settings.py index 9f81c18134..611c217c56 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -14,9 +14,12 @@ class TestPluginSettingsPipelineInjection(unittest.TestCase): Tests for SOCIAL_AUTH_PIPELINE step injection in plugin_settings(). """ - def _make_settings(self, pipeline=None): + def _make_settings(self, pipeline=None, enable_enterprise_integration=True): """Return a simple settings namespace with the given pipeline.""" - return SimpleNamespace(SOCIAL_AUTH_PIPELINE=pipeline) + return SimpleNamespace( + SOCIAL_AUTH_PIPELINE=pipeline, + ENABLE_ENTERPRISE_INTEGRATION=enable_enterprise_integration, + ) def _base_pipeline(self): """Return a minimal pipeline list resembling the platform default.""" @@ -111,9 +114,10 @@ def test_raises_when_associate_user_reference_step_missing(self): def test_no_pipeline_attribute(self): """ - If settings has no SOCIAL_AUTH_PIPELINE, plugin_settings is a no-op. + If settings has no SOCIAL_AUTH_PIPELINE, plugin_settings is a no-op + (even with enterprise integration enabled). """ - settings = SimpleNamespace() + settings = SimpleNamespace(ENABLE_ENTERPRISE_INTEGRATION=True) # Should not raise plugin_settings(settings) @@ -124,3 +128,15 @@ def test_pipeline_is_none(self): settings = self._make_settings(pipeline=None) # Should not raise plugin_settings(settings) + + def test_no_op_when_enterprise_integration_disabled(self): + """ + If ENABLE_ENTERPRISE_INTEGRATION is False, plugin_settings makes no + changes to SOCIAL_AUTH_PIPELINE. + """ + pipeline = self._base_pipeline() + settings = self._make_settings(pipeline=pipeline, enable_enterprise_integration=False) + plugin_settings(settings) + + assert 'enterprise.tpa_pipeline.enterprise_associate_by_email' not in pipeline + assert 'enterprise.tpa_pipeline.handle_enterprise_logistration' not in pipeline diff --git a/tests/test_tpa_pipeline.py b/tests/test_tpa_pipeline.py index af5a63b9cf..f5977e98c1 100644 --- a/tests/test_tpa_pipeline.py +++ b/tests/test_tpa_pipeline.py @@ -482,7 +482,7 @@ def test_logs_exception_on_unexpected_error(self): mock_log.exception.assert_called_once() assert '[Multiple_SSO_SAML_Accounts_Association_to_User]' in mock_log.exception.call_args[0][0] - @override_settings(FEATURES={'ENABLE_ENTERPRISE_INTEGRATION': False}) + @override_settings(ENABLE_ENTERPRISE_INTEGRATION=False) def test_returns_none_when_enterprise_disabled(self): """ If ENABLE_ENTERPRISE_INTEGRATION is False, return None without