mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-06-08 01:42:37 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e83b9f43a | |||
| 9da6a85ad8 | |||
| 51b1097509 | |||
| 95644dc961 |
@@ -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
@@ -1 +1 @@
|
|||||||
VERSION = "8.1.2"
|
VERSION = "8.2.1"
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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']}),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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"] = {
|
||||||
|
|||||||
Reference in New Issue
Block a user