Compare commits

..

4 Commits

Author SHA1 Message Date
semantic-release-bot 5e83b9f43a chore(release): 8.2.1 [skip ci]
## [8.2.1](https://github.com/mediacms-io/mediacms/compare/v8.2.0...v8.2.1) (2026-06-07)

### Bug Fixes

* SAML provider add guard to skip empty mappings before iterating ([#1536](https://github.com/mediacms-io/mediacms/issues/1536)) ([9da6a85](https://github.com/mediacms-io/mediacms/commit/9da6a85ad86f5092edb96495eeb1cca22d5334bf))
2026-06-07 14:55:57 +00:00
Markos Gogoulos 9da6a85ad8 fix: SAML provider add guard to skip empty mappings before iterating (#1536) 2026-06-07 17:55:32 +03:00
semantic-release-bot 51b1097509 chore(release): 8.2.0 [skip ci]
## [8.2.0](https://github.com/mediacms-io/mediacms/compare/v8.1.3...v8.2.0) (2026-05-31)

### Features

* configure SP certificate and private key via SAMLConfiguration ([#1531](https://github.com/mediacms-io/mediacms/issues/1531)) ([95644dc](https://github.com/mediacms-io/mediacms/commit/95644dc9615f428191d9fda0847c1b91a0b094a5))
2026-05-31 13:17:18 +00:00
Markos Gogoulos 95644dc961 feat: configure SP certificate and private key via SAMLConfiguration (#1531) 2026-05-31 16:16:46 +03:00
11 changed files with 81 additions and 18 deletions
+12
View File
@@ -1,5 +1,17 @@
# Changelog # Changelog
## [8.2.1](https://github.com/mediacms-io/mediacms/compare/v8.2.0...v8.2.1) (2026-06-07)
### Bug Fixes
* SAML provider add guard to skip empty mappings before iterating ([#1536](https://github.com/mediacms-io/mediacms/issues/1536)) ([9da6a85](https://github.com/mediacms-io/mediacms/commit/9da6a85ad86f5092edb96495eeb1cca22d5334bf))
## [8.2.0](https://github.com/mediacms-io/mediacms/compare/v8.1.3...v8.2.0) (2026-05-31)
### Features
* configure SP certificate and private key via SAMLConfiguration ([#1531](https://github.com/mediacms-io/mediacms/issues/1531)) ([95644dc](https://github.com/mediacms-io/mediacms/commit/95644dc9615f428191d9fda0847c1b91a0b094a5))
## [8.1.3](https://github.com/mediacms-io/mediacms/compare/v8.1.2...v8.1.3) (2026-05-19) ## [8.1.3](https://github.com/mediacms-io/mediacms/compare/v8.1.2...v8.1.3) (2026-05-19)
### Bug Fixes ### Bug Fixes
+1 -1
View File
@@ -1 +1 @@
VERSION = "8.1.2" VERSION = "8.2.1"
+2
View File
@@ -947,6 +947,8 @@ Select the SAML Configurations tab, create a new one and set:
3. **SSO URL**: 3. **SSO URL**:
4. **SLO URL**: 4. **SLO URL**:
5. **SP Metadata URL**: The metadata URL that the IDP will utilize. This can be https://{portal}/saml/metadata and is autogenerated by MediaCMS 5. **SP Metadata URL**: The metadata URL that the IDP will utilize. This can be https://{portal}/saml/metadata and is autogenerated by MediaCMS
6. **SP Certificate** (optional): SP x509 certificate (PEM). Enables encrypted/signed SAML communication. If set, the SP Private Key must also be provided, and the certificate is published in the SP metadata so the IDP can encrypt assertions to MediaCMS.
7. **SP Private Key** (optional): SP private key (PEM). Used to sign AuthnRequests/LogoutRequests and to decrypt assertions encrypted by the IDP. Required if SP Certificate is provided.
- Step 3: Set other Options - Step 3: Set other Options
1. **Email Settings**: 1. **Email Settings**:
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mediacms", "name": "mediacms",
"version": "8.1.3", "version": "8.2.1",
"devDependencies": { "devDependencies": {
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
+7 -4
View File
@@ -73,7 +73,7 @@ def perform_user_actions(user, social_account, common_fields=None):
if social_app: if social_app:
saml_configuration = social_app.saml_configurations.first() saml_configuration = social_app.saml_configurations.first()
add_user_logo(user, extra_data) add_user_logo(user, extra_data, saml_configuration)
handle_role_mapping(user, extra_data, social_app, saml_configuration) handle_role_mapping(user, extra_data, social_app, saml_configuration)
if saml_configuration and saml_configuration.save_saml_response_logs: if saml_configuration and saml_configuration.save_saml_response_logs:
handle_saml_logs_save(user, extra_data, social_app) handle_saml_logs_save(user, extra_data, social_app)
@@ -81,10 +81,13 @@ def perform_user_actions(user, social_account, common_fields=None):
return user return user
def add_user_logo(user, extra_data): def add_user_logo(user, extra_data, saml_configuration=None):
# use the attribute name configured in the SAML Configuration, falling
# back to "jpegPhoto" when it is left empty
logo_key = (saml_configuration.user_logo if saml_configuration and saml_configuration.user_logo else None) or "jpegPhoto"
try: try:
if extra_data.get("jpegPhoto") and user.logo.name in ["userlogos/user.jpg", "", None]: if extra_data.get(logo_key) and user.logo.name in ["userlogos/user.jpg", "", None]:
base64_string = extra_data.get("jpegPhoto")[0] base64_string = extra_data.get(logo_key)[0]
image_data = base64.b64decode(base64_string) image_data = base64.b64decode(base64_string)
image_content = ContentFile(image_data) image_content = ContentFile(image_data)
user.logo.save('user.jpg', image_content, save=True) user.logo.save('user.jpg', image_content, save=True)
+1 -1
View File
@@ -51,7 +51,7 @@ class SAMLConfigurationAdmin(admin.ModelAdmin):
search_fields = ['social_app__name', 'idp_id', 'sp_metadata_url'] search_fields = ['social_app__name', 'idp_id', 'sp_metadata_url']
fieldsets = [ fieldsets = [
('Provider Settings', {'fields': ['social_app', 'idp_id', 'idp_cert']}), ('Provider Settings', {'fields': ['social_app', 'idp_id', 'idp_cert', 'sp_cert', 'sp_private_key']}),
('URLs', {'fields': ['sso_url', 'slo_url', 'sp_metadata_url']}), ('URLs', {'fields': ['sso_url', 'slo_url', 'sp_metadata_url']}),
('Group Management', {'fields': ['remove_from_groups', 'save_saml_response_logs']}), ('Group Management', {'fields': ['remove_from_groups', 'save_saml_response_logs']}),
('Attribute Mapping', {'fields': ['uid', 'name', 'email', 'groups', 'first_name', 'last_name', 'user_logo', 'role']}), ('Attribute Mapping', {'fields': ['uid', 'name', 'email', 'groups', 'first_name', 'last_name', 'user_logo', 'role']}),
+15 -1
View File
@@ -18,14 +18,28 @@ class CustomSAMLProvider(SAMLProvider):
provider_config = self.app.settings provider_config = self.app.settings
raw_attributes = data.get_attributes() raw_attributes = data.get_attributes()
# get_attributes() keys attributes by their full Name. Some IdPs send
# certain attributes only under their FriendlyName, so fall back to the
# FriendlyName-keyed attributes when a Name lookup misses. The Name
# lookup is always preferred, so attributes that already resolve are
# unaffected.
try:
friendly_attributes = data.get_friendlyname_attributes()
except AttributeError:
friendly_attributes = {}
attributes = {} attributes = {}
attribute_mapping = provider_config.get("attribute_mapping", self.default_attribute_mapping) attribute_mapping = provider_config.get("attribute_mapping", self.default_attribute_mapping)
# map configured provider attributes # map configured provider attributes
for key, provider_keys in attribute_mapping.items(): for key, provider_keys in attribute_mapping.items():
# skip mappings left empty/None in the SAML Configuration
if not provider_keys:
continue
if isinstance(provider_keys, str): if isinstance(provider_keys, str):
provider_keys = [provider_keys] provider_keys = [provider_keys]
for provider_key in provider_keys: for provider_key in provider_keys:
attribute_list = raw_attributes.get(provider_key, None) attribute_list = raw_attributes.get(provider_key)
if attribute_list is None:
attribute_list = friendly_attributes.get(provider_key)
# if more than one keys, get them all comma separated # if more than one keys, get them all comma separated
if attribute_list is not None and len(attribute_list) > 1: if attribute_list is not None and len(attribute_list) > 1:
attributes[key] = ",".join(attribute_list) attributes[key] = ",".join(attribute_list)
+5 -9
View File
@@ -53,16 +53,12 @@ def build_sp_config(request, provider_config, org):
"binding": OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, "binding": OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
}, },
} }
if _sp_config.get("x509cert"):
sp_config["x509cert"] = _sp_config["x509cert"]
if _sp_config.get("private_key"):
sp_config["privateKey"] = _sp_config["private_key"]
avd = provider_config.get("advanced", {}) avd = provider_config.get("advanced", {})
if avd.get("x509cert") is not None:
sp_config["x509cert"] = avd["x509cert"]
if avd.get("x509cert_new"):
sp_config["x509certNew"] = avd["x509cert_new"]
if avd.get("private_key") is not None:
sp_config["privateKey"] = avd["private_key"]
if avd.get("name_id_format") is not None: if avd.get("name_id_format") is not None:
sp_config["NameIDFormat"] = avd["name_id_format"] sp_config["NameIDFormat"] = avd["name_id_format"]
+3 -1
View File
@@ -154,7 +154,9 @@ sls = SLSView.as_view()
class MetadataView(SAMLViewMixin, View): class MetadataView(SAMLViewMixin, View):
def dispatch(self, request, organization_slug): def dispatch(self, request, organization_slug):
provider = self.get_provider(organization_slug) provider = self.get_provider(organization_slug)
config = build_saml_config(self.request, provider.app.settings, organization_slug) custom_configuration = provider.app.saml_configurations.first()
provider_config = custom_configuration.saml_provider_settings if custom_configuration else provider.app.settings
config = build_saml_config(self.request, provider_config, organization_slug)
saml_settings = OneLogin_Saml2_Settings(settings=config, sp_validation_only=True) saml_settings = OneLogin_Saml2_Settings(settings=config, sp_validation_only=True)
metadata = saml_settings.get_sp_metadata() metadata = saml_settings.get_sp_metadata()
errors = saml_settings.validate_metadata(metadata) errors = saml_settings.validate_metadata(metadata)
@@ -0,0 +1,23 @@
# Generated by Django 5.2.6 on 2026-05-31 12:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('saml_auth', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='samlconfiguration',
name='sp_cert',
field=models.TextField(blank=True, help_text='SP x509cert (PEM). Optional; required if SP private key is set.', null=True),
),
migrations.AddField(
model_name='samlconfiguration',
name='sp_private_key',
field=models.TextField(blank=True, help_text='SP private key (PEM). Optional; required if SP certificate is set.', null=True),
),
]
+11
View File
@@ -14,6 +14,8 @@ class SAMLConfiguration(models.Model):
# Certificates # Certificates
idp_cert = models.TextField(help_text='x509cert') idp_cert = models.TextField(help_text='x509cert')
sp_cert = models.TextField(blank=True, null=True, help_text='SP x509cert (PEM). Optional; required if SP private key is set.')
sp_private_key = models.TextField(blank=True, null=True, help_text='SP private key (PEM). Optional; required if SP certificate is set.')
# Attribute Mapping Fields # Attribute Mapping Fields
uid = models.CharField(max_length=100, help_text='eg eduPersonPrincipalName') uid = models.CharField(max_length=100, help_text='eg eduPersonPrincipalName')
@@ -49,6 +51,11 @@ class SAMLConfiguration(models.Model):
if existing_conf.exists(): if existing_conf.exists():
raise ValidationError({'social_app': 'Cannot create configuration for the same social app because one configuration already exists.'}) raise ValidationError({'social_app': 'Cannot create configuration for the same social app because one configuration already exists.'})
if self.sp_cert and not self.sp_private_key:
raise ValidationError({'sp_private_key': 'Required when SP certificate is provided.'})
if self.sp_private_key and not self.sp_cert:
raise ValidationError({'sp_cert': 'Required when SP private key is provided.'})
super().clean() super().clean()
@property @property
@@ -56,6 +63,10 @@ class SAMLConfiguration(models.Model):
# provide settings in a way for Social App SAML provider # provide settings in a way for Social App SAML provider
provider_settings = {} provider_settings = {}
provider_settings["sp"] = {"entity_id": self.sp_metadata_url} provider_settings["sp"] = {"entity_id": self.sp_metadata_url}
if self.sp_cert:
provider_settings["sp"]["x509cert"] = self.sp_cert
if self.sp_private_key:
provider_settings["sp"]["private_key"] = self.sp_private_key
provider_settings["idp"] = {"slo_url": self.slo_url, "sso_url": self.sso_url, "x509cert": self.idp_cert, "entity_id": self.idp_id} provider_settings["idp"] = {"slo_url": self.slo_url, "sso_url": self.sso_url, "x509cert": self.idp_cert, "entity_id": self.idp_id}
provider_settings["attribute_mapping"] = { provider_settings["attribute_mapping"] = {