Skip to content

Commit 8a02f26

Browse files
committed
feat: Add configurable registration field overrides for SAML providers
1 parent 4369055 commit 8a02f26

4 files changed

Lines changed: 443 additions & 8 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.13 on 2026-02-18 00:00
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('third_party_auth', '0013_default_site_id_wrapper_function'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='samlproviderconfig',
15+
name='other_settings',
16+
field=models.TextField(blank=True, help_text='For advanced use cases, enter a JSON object with addtional configuration. The tpa-saml backend supports {"requiredEntitlements": ["urn:..."]}, which can be used to require the presence of a specific eduPersonEntitlement, and {"extra_field_definitions": [{"name": "...", "urn": "..."},...]}, which can be used to define registration form fields and the URNs that can be used to retrieve the relevant values from the SAML response, and {"registration_field_overrides": {"field_name": "required|optional|hidden"}}, which can be used to control the visibility and requirement status of specific registration form fields (e.g., marketing_emails_opt_in, research). Custom provider types, as selected in the "Identity Provider Type" field, may make use of the information stored in this field for additional configuration.', verbose_name='Advanced settings'),
17+
),
18+
]

common/djangoapps/third_party_auth/models.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -753,9 +753,12 @@ class SAMLProviderConfig(ProviderConfig):
753753
'which can be used to require the presence of a specific eduPersonEntitlement, '
754754
'and {"extra_field_definitions": [{"name": "...", "urn": "..."},...]}, which can be '
755755
'used to define registration form fields and the URNs that can be used to retrieve '
756-
'the relevant values from the SAML response. Custom provider types, as selected '
757-
'in the "Identity Provider Type" field, may make use of the information stored '
758-
'in this field for additional configuration.'
756+
'the relevant values from the SAML response, '
757+
'and {"registration_field_overrides": {"field_name": "required|optional|hidden"}}, '
758+
'which can be used to control the visibility and requirement status of specific '
759+
'registration form fields (e.g., marketing_emails_opt_in, research). '
760+
'Custom provider types, as selected in the "Identity Provider Type" field, may make '
761+
'use of the information stored in this field for additional configuration.'
759762
)
760763
)
761764
archived = models.BooleanField(default=False)
@@ -838,7 +841,39 @@ def get_setting(self, name):
838841
return other_settings[name]
839842
raise KeyError
840843

841-
def get_config(self, backend):
844+
def get_registration_field_overrides(self):
845+
"""
846+
Get registration field visibility/requirement overrides for this provider.
847+
848+
This allows SAML providers to configure whether certain fields
849+
(like marketing_emails_opt_in or research) should be required,
850+
optional, or hidden on the registration form.
851+
852+
Returns:
853+
dict: Mapping of field names to settings ("required", "optional", "hidden")
854+
Returns empty dict if no overrides are configured.
855+
856+
Example:
857+
{
858+
"marketing_emails_opt_in": "optional",
859+
"research": "hidden"
860+
}
861+
"""
862+
try:
863+
overrides = self.get_setting('registration_field_overrides')
864+
# Validate override values
865+
valid_values = {'required', 'optional', 'hidden'}
866+
if isinstance(overrides, dict):
867+
return {
868+
field: value
869+
for field, value in overrides.items()
870+
if value in valid_values
871+
}
872+
return {}
873+
except KeyError:
874+
return {}
875+
876+
def get_config(self):
842877
"""
843878
Return a SAMLIdentityProvider instance for use by SAMLAuthBackend.
844879
@@ -898,7 +933,7 @@ def get_config(self, backend):
898933
SAMLConfiguration.current(self.site.id, 'default')
899934
)
900935
idp_class = get_saml_idp_class(self.identity_provider_type)
901-
return idp_class(backend, self.slug, **conf)
936+
return idp_class(self.slug, **conf)
902937

903938

904939
class SAMLProviderData(models.Model):

openedx/core/djangoapps/user_authn/views/registration_form.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ def __init__(self):
366366
"profession",
367367
"specialty",
368368
"marketing_emails_opt_in",
369+
"research",
369370
]
370371

371372
if settings.ENABLE_COPPA_COMPLIANCE and 'year_of_birth' in self.EXTRA_FIELDS:
@@ -401,7 +402,7 @@ def __init__(self):
401402

402403
field_order = configuration_helpers.get_value('REGISTRATION_FIELD_ORDER')
403404
if not field_order:
404-
field_order = settings.REGISTRATION_FIELD_ORDER or valid_fields
405+
field_order = getattr(settings, 'REGISTRATION_FIELD_ORDER', None) or valid_fields
405406
# Check that all of the valid_fields are in the field order and vice versa,
406407
# if not append missing fields at end of field order
407408
if set(valid_fields) != set(field_order):
@@ -712,6 +713,27 @@ def _add_marketing_emails_opt_in_field(self, form_desc, required=False):
712713
required=required,
713714
)
714715

716+
def _add_research_field(self, form_desc, required=False):
717+
"""Add a research participation checkbox to form description.
718+
Arguments:
719+
form_desc: A form description
720+
Keyword Arguments:
721+
required (bool): Whether this field is required; defaults to False
722+
"""
723+
research_label = _(
724+
'I agree to allow {platform_name} to use my de-identified data for research purposes.').format(
725+
platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
726+
)
727+
728+
form_desc.add_field(
729+
'research',
730+
label=research_label,
731+
field_type="checkbox",
732+
exposed=True,
733+
default=False,
734+
required=required,
735+
)
736+
715737
def _add_field_with_configurable_select_options(self, field_name, field_label, form_desc, required=False):
716738
"""Add a field to a form description.
717739
If select options are given for this field, it will be a select type
@@ -1111,6 +1133,57 @@ def _add_terms_of_service_field(self, form_desc, required=True):
11111133
},
11121134
)
11131135

1136+
def _apply_provider_field_overrides(self, form_desc, provider_overrides, field_name):
1137+
"""
1138+
Apply SAML provider-specific overrides to a registration form field.
1139+
1140+
This allows SAML providers to configure whether certain fields
1141+
(like marketing_emails_opt_in or research) should be required,
1142+
optional, or hidden.
1143+
1144+
Arguments:
1145+
form_desc (FormDescription): The registration form description to modify
1146+
provider_overrides (dict): Field override configuration from provider
1147+
field_name (str): Name of the field to potentially override
1148+
1149+
Returns:
1150+
bool: True if the field was overridden, False otherwise
1151+
"""
1152+
if field_name not in provider_overrides:
1153+
return False
1154+
1155+
override_value = provider_overrides[field_name]
1156+
1157+
if override_value == "hidden":
1158+
# Hide the field completely
1159+
form_desc.override_field_properties(
1160+
field_name,
1161+
field_type="hidden",
1162+
required=False,
1163+
label="",
1164+
instructions="",
1165+
default=""
1166+
)
1167+
return True
1168+
1169+
elif override_value == "required":
1170+
# Make the field required
1171+
form_desc.override_field_properties(
1172+
field_name,
1173+
required=True
1174+
)
1175+
return True
1176+
1177+
elif override_value == "optional":
1178+
# Make the field optional (ensure it's not required)
1179+
form_desc.override_field_properties(
1180+
field_name,
1181+
required=False
1182+
)
1183+
return True
1184+
1185+
return False
1186+
11141187
def _apply_third_party_auth_overrides(self, request, form_desc):
11151188
"""Modify the registration form if the user has authenticated with a third-party provider.
11161189
If a user has successfully authenticated with a third-party provider,
@@ -1195,3 +1268,26 @@ def _apply_third_party_auth_overrides(self, request, form_desc):
11951268
default=current_provider.name if current_provider.name else "Third Party",
11961269
required=False,
11971270
)
1271+
1272+
# Apply provider-specific field visibility overrides
1273+
# This allows SAML providers to configure whether fields like
1274+
# marketing_emails_opt_in or research should be shown/required
1275+
provider_field_overrides = current_provider.get_registration_field_overrides()
1276+
1277+
if provider_field_overrides:
1278+
# List of fields that can be overridden by the provider
1279+
overrideable_fields = [
1280+
'marketing_emails_opt_in',
1281+
'research',
1282+
# Add more fields here as needed
1283+
]
1284+
1285+
for field_name in overrideable_fields:
1286+
# Only apply overrides for fields that are actually in the form
1287+
# This check prevents errors if a field isn't enabled platform-wide
1288+
if any(f['name'] == field_name for f in form_desc.fields):
1289+
self._apply_provider_field_overrides(
1290+
form_desc,
1291+
provider_field_overrides,
1292+
field_name
1293+
)

0 commit comments

Comments
 (0)