mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-20 21:46:04 -05:00
feat: RBAC + SAML support
This commit is contained in:
0
identity_providers/__init__.py
Normal file
0
identity_providers/__init__.py
Normal file
360
identity_providers/admin.py
Normal file
360
identity_providers/admin.py
Normal file
@@ -0,0 +1,360 @@
|
||||
import csv
|
||||
import logging
|
||||
|
||||
from allauth.socialaccount.admin import SocialAccountAdmin, SocialAppAdmin
|
||||
from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
|
||||
from identity_providers.forms import ImportCSVsForm
|
||||
from identity_providers.models import (
|
||||
IdentityProviderCategoryMapping,
|
||||
IdentityProviderGlobalRole,
|
||||
IdentityProviderGroupRole,
|
||||
IdentityProviderUserLog,
|
||||
LoginOption,
|
||||
)
|
||||
from rbac.models import RBACGroup
|
||||
from saml_auth.models import SAMLConfiguration
|
||||
|
||||
|
||||
class IdentityProviderUserLogAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'identity_provider',
|
||||
'user',
|
||||
'created_at',
|
||||
]
|
||||
|
||||
list_filter = ['identity_provider', 'created_at']
|
||||
|
||||
search_fields = ['identity_provider__name', 'user__username', 'user__email', 'logs']
|
||||
|
||||
readonly_fields = ['identity_provider', 'user', 'created_at', 'logs']
|
||||
|
||||
|
||||
class SAMLConfigurationInline(admin.StackedInline):
|
||||
model = SAMLConfiguration
|
||||
extra = 0
|
||||
can_delete = True
|
||||
max_num = 1
|
||||
|
||||
|
||||
class IdentityProviderCategoryMappingInlineForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = IdentityProviderCategoryMapping
|
||||
fields = ('name', 'map_to')
|
||||
|
||||
# custom field to track if the row should be deleted
|
||||
should_delete = forms.BooleanField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
|
||||
class IdentityProviderCategoryMappingInline(admin.TabularInline):
|
||||
model = IdentityProviderCategoryMapping
|
||||
form = IdentityProviderCategoryMappingInlineForm
|
||||
extra = 0
|
||||
can_delete = True
|
||||
show_change_link = True
|
||||
verbose_name = "Category Mapping"
|
||||
verbose_name_plural = "Category Mapping"
|
||||
template = 'admin/socialaccount/socialapp/custom_tabular_inline.html'
|
||||
autocomplete_fields = ['map_to']
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
formfield = super().formfield_for_dbfield(db_field, **kwargs)
|
||||
if db_field.name in ('name', 'map_to') and formfield:
|
||||
formfield.widget.attrs.update(
|
||||
{
|
||||
'data-help-text': db_field.help_text,
|
||||
'class': 'with-help-text',
|
||||
}
|
||||
)
|
||||
return formfield
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
formset = super().get_formset(request, obj, **kwargs)
|
||||
return formset
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return True
|
||||
|
||||
|
||||
class RBACGroupInlineForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = RBACGroup
|
||||
fields = ('uid', 'name')
|
||||
labels = {
|
||||
'uid': 'Group Attribute Value',
|
||||
'name': 'Name',
|
||||
}
|
||||
help_texts = {
|
||||
'uid': 'Identity Provider group attribute value',
|
||||
'name': 'MediaCMS Group name',
|
||||
}
|
||||
|
||||
# custom field to track if the row should be deleted
|
||||
should_delete = forms.BooleanField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
|
||||
class RBACGroupInline(admin.TabularInline):
|
||||
model = RBACGroup
|
||||
form = RBACGroupInlineForm
|
||||
extra = 0
|
||||
can_delete = True
|
||||
show_change_link = True
|
||||
verbose_name = "Group Mapping"
|
||||
verbose_name_plural = "Group Mapping"
|
||||
template = 'admin/socialaccount/socialapp/custom_tabular_inline_for_groups.html'
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
formfield = super().formfield_for_dbfield(db_field, **kwargs)
|
||||
if db_field.name in ('uid', 'name') and formfield:
|
||||
formfield.widget.attrs.update(
|
||||
{
|
||||
'data-help-text': db_field.help_text,
|
||||
'class': 'with-help-text',
|
||||
}
|
||||
)
|
||||
return formfield
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
formset = super().get_formset(request, obj, **kwargs)
|
||||
return formset
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return True
|
||||
|
||||
|
||||
class CustomSocialAppAdmin(SocialAppAdmin):
|
||||
# The default SocialAppAdmin has been overriden to achieve a number of changes.
|
||||
# If you need to add more fields (out of those that are hidden), or remove tabs, or
|
||||
# change the ordering of fields, or the place where fields appear, don't forget to
|
||||
# check the html template!
|
||||
|
||||
change_form_template = 'admin/socialaccount/socialapp/change_form.html'
|
||||
list_display = ('get_config_name', 'get_protocol')
|
||||
fields = ('provider', 'provider_id', 'name', 'client_id', 'sites', 'groups_csv', 'categories_csv')
|
||||
form = ImportCSVsForm
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.inlines = []
|
||||
|
||||
if getattr(settings, 'USE_SAML', False):
|
||||
self.inlines.append(SAMLConfigurationInline)
|
||||
self.inlines.append(IdentityProviderGlobalRoleInline)
|
||||
self.inlines.append(IdentityProviderGroupRoleInline)
|
||||
self.inlines.append(RBACGroupInline)
|
||||
self.inlines.append(IdentityProviderCategoryMappingInline)
|
||||
|
||||
def get_protocol(self, obj):
|
||||
return obj.provider
|
||||
|
||||
def get_config_name(self, obj):
|
||||
return obj.name
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
field = super().formfield_for_dbfield(db_field, **kwargs)
|
||||
if db_field.name == 'provider':
|
||||
field.label = 'Protocol'
|
||||
field.help_text = "The provider type, eg `google`. For SAML providers, make sure this is set to `saml` lowercase."
|
||||
elif db_field.name == 'name':
|
||||
field.label = 'IDP Config Name'
|
||||
field.help_text = "This should be a unique name for the provider."
|
||||
elif db_field.name == 'client_id':
|
||||
field.help_text = 'App ID, or consumer key. For SAML providers, this will be part of the default login URL /accounts/saml/{client_id}/login/'
|
||||
elif db_field.name == 'sites':
|
||||
field.required = True
|
||||
field.help_text = "Select at least one site where this social application is available. Required."
|
||||
elif db_field.name == 'provider_id':
|
||||
field.required = True
|
||||
field.help_text = "This should be a unique identifier for the provider."
|
||||
return field
|
||||
|
||||
get_config_name.short_description = 'IDP Config Name'
|
||||
get_protocol.short_description = 'Protocol'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super().save_model(request, obj, form, change)
|
||||
csv_file = form.cleaned_data.get('groups_csv')
|
||||
if csv_file:
|
||||
try:
|
||||
csv_file.seek(0)
|
||||
decoded_file = csv_file.read().decode('utf-8').splitlines()
|
||||
csv_reader = csv.DictReader(decoded_file)
|
||||
for row in csv_reader:
|
||||
group_id = row.get('group_id')
|
||||
name = row.get('name')
|
||||
|
||||
if group_id and name:
|
||||
if not (RBACGroup.objects.filter(identity_provider=obj, uid=group_id).exists() or RBACGroup.objects.filter(identity_provider=obj, name=name).exists()):
|
||||
try:
|
||||
group = RBACGroup.objects.create(identity_provider=obj, uid=group_id, name=name) # noqa
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
csv_file = form.cleaned_data.get('categories_csv')
|
||||
if csv_file:
|
||||
from files.models import Category
|
||||
|
||||
try:
|
||||
csv_file.seek(0)
|
||||
decoded_file = csv_file.read().decode('utf-8').splitlines()
|
||||
csv_reader = csv.DictReader(decoded_file)
|
||||
for row in csv_reader:
|
||||
group_id = row.get('group_id')
|
||||
category_id = row.get('category_id')
|
||||
if group_id and category_id:
|
||||
category = Category.objects.filter(uid=category_id).first()
|
||||
if category:
|
||||
if not IdentityProviderCategoryMapping.objects.filter(identity_provider=obj, name=group_id, map_to=category).exists():
|
||||
mapping = IdentityProviderCategoryMapping.objects.create(identity_provider=obj, name=group_id, map_to=category) # noqa
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
def save_formset(self, request, form, formset, change):
|
||||
instances = formset.save(commit=False)
|
||||
|
||||
for form in formset.forms:
|
||||
if form.cleaned_data.get('should_delete', False) and form.instance.pk:
|
||||
instances.remove(form.instance)
|
||||
form.instance.delete()
|
||||
|
||||
for instance in instances:
|
||||
instance.save()
|
||||
formset.save_m2m()
|
||||
|
||||
|
||||
class CustomSocialAccountAdmin(SocialAccountAdmin):
|
||||
list_display = ('user', 'uid', 'get_provider')
|
||||
|
||||
def get_provider(self, obj):
|
||||
return obj.provider
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
field = super().formfield_for_dbfield(db_field, **kwargs)
|
||||
if db_field.name == 'provider':
|
||||
field.label = 'Provider ID'
|
||||
return field
|
||||
|
||||
get_provider.short_description = 'Provider ID'
|
||||
|
||||
|
||||
class IdentityProviderGroupRoleInlineForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = IdentityProviderGroupRole
|
||||
fields = ('name', 'map_to')
|
||||
|
||||
# custom field to track if the row should be deleted
|
||||
should_delete = forms.BooleanField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
name = cleaned_data.get('name')
|
||||
identity_provider = getattr(self.instance, 'identity_provider', None)
|
||||
|
||||
if name and identity_provider:
|
||||
if IdentityProviderGroupRole.objects.filter(identity_provider=identity_provider, name=name).exclude(pk=self.instance.pk).exists():
|
||||
self.add_error('name', 'A group role mapping with this name already exists for this Identity provider.')
|
||||
|
||||
|
||||
class IdentityProviderGroupRoleInline(admin.TabularInline):
|
||||
model = IdentityProviderGroupRole
|
||||
form = IdentityProviderGroupRoleInlineForm
|
||||
extra = 0
|
||||
verbose_name = "Group Role Mapping"
|
||||
verbose_name_plural = "Group Role Mapping"
|
||||
template = 'admin/socialaccount/socialapp/custom_tabular_inline.html'
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
formfield = super().formfield_for_dbfield(db_field, **kwargs)
|
||||
if db_field.name in ('name',) and formfield:
|
||||
formfield.widget.attrs.update(
|
||||
{
|
||||
'data-help-text': db_field.help_text,
|
||||
'class': 'with-help-text',
|
||||
}
|
||||
)
|
||||
return formfield
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
formset = super().get_formset(request, obj, **kwargs)
|
||||
return formset
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return True
|
||||
|
||||
|
||||
class IdentityProviderGlobalRoleInlineForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = IdentityProviderGlobalRole
|
||||
fields = ('name', 'map_to')
|
||||
|
||||
# custom field to track if the row should be deleted
|
||||
should_delete = forms.BooleanField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
name = cleaned_data.get('name')
|
||||
identity_provider = getattr(self.instance, 'identity_provider', None)
|
||||
|
||||
if name and identity_provider:
|
||||
if IdentityProviderGlobalRole.objects.filter(identity_provider=identity_provider, name=name).exclude(pk=self.instance.pk).exists():
|
||||
self.add_error('name', 'A global role mapping with this name already exists for this Identity provider.')
|
||||
|
||||
|
||||
class IdentityProviderGlobalRoleInline(admin.TabularInline):
|
||||
model = IdentityProviderGlobalRole
|
||||
form = IdentityProviderGlobalRoleInlineForm
|
||||
extra = 0
|
||||
verbose_name = "Global Role Mapping"
|
||||
verbose_name_plural = "Global Role Mapping"
|
||||
template = 'admin/socialaccount/socialapp/custom_tabular_inline.html'
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
formfield = super().formfield_for_dbfield(db_field, **kwargs)
|
||||
if db_field.name in ('name',) and formfield:
|
||||
formfield.widget.attrs.update(
|
||||
{
|
||||
'data-help-text': db_field.help_text,
|
||||
'class': 'with-help-text',
|
||||
}
|
||||
)
|
||||
return formfield
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
formset = super().get_formset(request, obj, **kwargs)
|
||||
return formset
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return True
|
||||
|
||||
|
||||
class LoginOptionAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'url', 'ordering', 'active')
|
||||
list_editable = ('ordering', 'active')
|
||||
list_filter = ('active',)
|
||||
search_fields = ('title', 'url')
|
||||
|
||||
|
||||
if getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
|
||||
admin.site.register(IdentityProviderUserLog, IdentityProviderUserLogAdmin)
|
||||
admin.site.unregister(SocialToken)
|
||||
|
||||
# This is unregistering the default Social App and registers the custom one here,
|
||||
# with mostly name setting options
|
||||
IdentityProviderUserLog._meta.verbose_name = "User Logs"
|
||||
IdentityProviderUserLog._meta.verbose_name_plural = "User Logs"
|
||||
|
||||
SocialAccount._meta.verbose_name = "User Account"
|
||||
SocialAccount._meta.verbose_name_plural = "User Accounts"
|
||||
admin.site.unregister(SocialApp)
|
||||
admin.site.register(SocialApp, CustomSocialAppAdmin)
|
||||
admin.site.register(LoginOption, LoginOptionAdmin)
|
||||
admin.site.unregister(SocialAccount)
|
||||
admin.site.register(SocialAccount, CustomSocialAccountAdmin)
|
||||
SocialApp._meta.verbose_name = "ID Provider"
|
||||
SocialApp._meta.verbose_name_plural = "ID Providers"
|
||||
SocialAccount._meta.app_config.verbose_name = "Identity Providers"
|
||||
6
identity_providers/apps.py
Normal file
6
identity_providers/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IdentityProvidersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'identity_providers'
|
||||
69
identity_providers/forms.py
Normal file
69
identity_providers/forms.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import csv
|
||||
|
||||
from allauth.socialaccount.models import SocialApp
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
class ImportCSVsForm(forms.ModelForm):
|
||||
groups_csv = forms.FileField(
|
||||
required=False,
|
||||
label="CSV file",
|
||||
help_text=mark_safe("Optionally, upload a CSV file to add multiple group mappings at once. <a href='/static/templates/group_mapping.csv' class='download-template'>Download Template</a>"),
|
||||
)
|
||||
categories_csv = forms.FileField(
|
||||
required=False,
|
||||
label="CSV file",
|
||||
help_text=("Optionally, upload a CSV file to add multiple category mappings at once. <a href='/static/templates/category_mapping.csv' class='download-template'>Download Template</a>"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SocialApp
|
||||
fields = '__all__'
|
||||
|
||||
def clean_groups_csv(self):
|
||||
groups_csv = self.cleaned_data.get('groups_csv')
|
||||
|
||||
if not groups_csv:
|
||||
return groups_csv
|
||||
|
||||
if not groups_csv.name.endswith('.csv'):
|
||||
raise ValidationError("Uploaded file must be a CSV file.")
|
||||
|
||||
try:
|
||||
decoded_file = groups_csv.read().decode('utf-8').splitlines()
|
||||
csv_reader = csv.reader(decoded_file)
|
||||
headers = next(csv_reader, None)
|
||||
if not headers or 'group_id' not in headers or 'name' not in headers:
|
||||
raise ValidationError("CSV file must contain 'group_id' and 'name' headers. " f"Found headers: {', '.join(headers) if headers else 'none'}")
|
||||
groups_csv.seek(0)
|
||||
return groups_csv
|
||||
|
||||
except csv.Error:
|
||||
raise ValidationError("Invalid CSV file. Please ensure the file is properly formatted.")
|
||||
except UnicodeDecodeError:
|
||||
raise ValidationError("Invalid file encoding. Please upload a CSV file with UTF-8 encoding.")
|
||||
|
||||
def clean_categories_csv(self):
|
||||
categories_csv = self.cleaned_data.get('categories_csv')
|
||||
|
||||
if not categories_csv:
|
||||
return categories_csv
|
||||
|
||||
if not categories_csv.name.endswith('.csv'):
|
||||
raise ValidationError("Uploaded file must be a CSV file.")
|
||||
|
||||
try:
|
||||
decoded_file = categories_csv.read().decode('utf-8').splitlines()
|
||||
csv_reader = csv.reader(decoded_file)
|
||||
headers = next(csv_reader, None)
|
||||
if not headers or 'category_id' not in headers or 'group_id' not in headers:
|
||||
raise ValidationError("CSV file must contain 'group_id' and 'category_id' headers. " f"Found headers: {', '.join(headers) if headers else 'none'}")
|
||||
categories_csv.seek(0)
|
||||
return categories_csv
|
||||
|
||||
except csv.Error:
|
||||
raise ValidationError("Invalid CSV file. Please ensure the file is properly formatted.")
|
||||
except UnicodeDecodeError:
|
||||
raise ValidationError("Invalid file encoding. Please upload a CSV file with UTF-8 encoding.")
|
||||
87
identity_providers/migrations/0001_initial.py
Normal file
87
identity_providers/migrations/0001_initial.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-18 17:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('socialaccount', '0006_alter_socialaccount_extra_data'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IdentityProviderUserLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('logs', models.TextField(blank=True, null=True)),
|
||||
('identity_provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='saml_logs', to='socialaccount.socialapp')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='saml_logs', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Identity Provider User Log',
|
||||
'verbose_name_plural': 'Identity Provider User Logs',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IdentityProviderCategoryMapping',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Identity Provider group attribute value', max_length=100, verbose_name='Group Attribute Value')),
|
||||
('map_to', models.CharField(help_text='Category id', max_length=300)),
|
||||
('identity_provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='category_mapping', to='socialaccount.socialapp')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Identity Provider Category Mapping',
|
||||
'verbose_name_plural': 'Identity Provider Category Mappings',
|
||||
'unique_together': {('identity_provider', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IdentityProviderGlobalRole',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Identity Provider role attribute value', max_length=100, verbose_name='Global Role Mapping')),
|
||||
(
|
||||
'map_to',
|
||||
models.CharField(
|
||||
choices=[
|
||||
('user', 'Authenticated User'),
|
||||
('advancedUser', 'Advanced User'),
|
||||
('editor', 'MediaCMS Editor'),
|
||||
('manager', 'MediaCMS Manager'),
|
||||
('admin', 'MediaCMS Administrator'),
|
||||
],
|
||||
help_text='MediaCMS Global Role',
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
('identity_provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_roles', to='socialaccount.socialapp')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Identity Provider Global Role Mapping',
|
||||
'verbose_name_plural': 'Identity Provider Global Role Mappings',
|
||||
'unique_together': {('identity_provider', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IdentityProviderGroupRole',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Identity Provider role attribute value', max_length=100, verbose_name='Group Role Mapping')),
|
||||
('map_to', models.CharField(choices=[('member', 'Member'), ('contributor', 'Contributor'), ('manager', 'Manager')], help_text='MediaCMS Group Role', max_length=20)),
|
||||
('identity_provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_roles', to='socialaccount.socialapp')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Identity Provider Group Role Mapping',
|
||||
'verbose_name_plural': 'Identity Provider Group Role Mappings',
|
||||
'unique_together': {('identity_provider', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
||||
27
identity_providers/migrations/0002_loginoption.py
Normal file
27
identity_providers/migrations/0002_loginoption.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-20 18:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('identity_providers', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LoginOption',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(help_text='Display name for this login option (e.g. Login through DEIC)', max_length=100)),
|
||||
('url', models.CharField(help_text='URL or path for this login option', max_length=255)),
|
||||
('ordering', models.PositiveIntegerField(default=0, help_text='Display order (smaller numbers appear first)')),
|
||||
('active', models.BooleanField(default=True, help_text='Whether this login option is currently active')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Login Option',
|
||||
'verbose_name_plural': 'Login Options',
|
||||
'ordering': ['ordering'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-25 15:05
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('identity_providers', '0002_loginoption'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='identityprovidercategorymapping',
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-25 15:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('files', '0005_alter_category_uid'),
|
||||
('identity_providers', '0003_alter_identityprovidercategorymapping_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='identityprovidercategorymapping',
|
||||
name='map_to',
|
||||
field=models.ForeignKey(help_text='Category id', on_delete=django.db.models.deletion.CASCADE, to='files.category'),
|
||||
),
|
||||
]
|
||||
0
identity_providers/migrations/__init__.py
Normal file
0
identity_providers/migrations/__init__.py
Normal file
125
identity_providers/models.py
Normal file
125
identity_providers/models.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from allauth.socialaccount.models import SocialApp
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
|
||||
|
||||
class IdentityProviderUserLog(models.Model):
|
||||
identity_provider = models.ForeignKey(SocialApp, on_delete=models.CASCADE, related_name='saml_logs')
|
||||
user = models.ForeignKey("users.User", on_delete=models.CASCADE, related_name='saml_logs')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
logs = models.TextField(blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Identity Provider User Log'
|
||||
verbose_name_plural = 'Identity Provider User Logs'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f'SAML Log - {self.user.username} - {self.created_at}'
|
||||
|
||||
|
||||
class IdentityProviderGroupRole(models.Model):
|
||||
identity_provider = models.ForeignKey(SocialApp, on_delete=models.CASCADE, related_name='group_roles')
|
||||
name = models.CharField(verbose_name='Group Role Mapping', max_length=100, help_text='Identity Provider role attribute value')
|
||||
|
||||
map_to = models.CharField(max_length=20, choices=[('member', 'Member'), ('contributor', 'Contributor'), ('manager', 'Manager')], help_text='MediaCMS Group Role')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Identity Provider Group Role Mapping'
|
||||
verbose_name_plural = 'Identity Provider Group Role Mappings'
|
||||
unique_together = ('identity_provider', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return f'Identity Provider Group Role Mapping {self.name}'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.identity_provider:
|
||||
raise ValidationError({'identity_provider': 'Identity Provider is required.'})
|
||||
|
||||
if IdentityProviderGroupRole.objects.filter(identity_provider=self.identity_provider, name=self.name).exclude(pk=self.pk).exists():
|
||||
raise ValidationError({'name': 'A group role mapping for this Identity Provider with this name already exists.'})
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class IdentityProviderGlobalRole(models.Model):
|
||||
identity_provider = models.ForeignKey(SocialApp, on_delete=models.CASCADE, related_name='global_roles')
|
||||
name = models.CharField(verbose_name='Global Role Mapping', max_length=100, help_text='Identity Provider role attribute value')
|
||||
|
||||
map_to = models.CharField(
|
||||
max_length=20,
|
||||
choices=[('user', 'Authenticated User'), ('advancedUser', 'Advanced User'), ('editor', 'MediaCMS Editor'), ('manager', 'MediaCMS Manager'), ('admin', 'MediaCMS Administrator')],
|
||||
help_text='MediaCMS Global Role',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Identity Provider Global Role Mapping'
|
||||
verbose_name_plural = 'Identity Provider Global Role Mappings'
|
||||
unique_together = ('identity_provider', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return f'Identity Provider Global Role Mapping {self.name}'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.identity_provider:
|
||||
raise ValidationError({'identity_provider': 'Identity Provider is required.'})
|
||||
|
||||
if IdentityProviderGlobalRole.objects.filter(identity_provider=self.identity_provider, name=self.name).exclude(pk=self.pk).exists():
|
||||
raise ValidationError({'name': 'A global role mapping for this Identity Provider with this name already exists.'})
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class IdentityProviderCategoryMapping(models.Model):
|
||||
identity_provider = models.ForeignKey(SocialApp, on_delete=models.CASCADE, related_name='category_mapping')
|
||||
|
||||
name = models.CharField(verbose_name='Group Attribute Value', max_length=100, help_text='Identity Provider group attribute value')
|
||||
|
||||
map_to = models.ForeignKey('files.Category', on_delete=models.CASCADE, help_text='Category id')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Identity Provider Category Mapping'
|
||||
verbose_name_plural = 'Identity Provider Category Mappings'
|
||||
|
||||
def clean(self):
|
||||
if not self._state.adding and self.pk:
|
||||
original = IdentityProviderCategoryMapping.objects.get(pk=self.pk)
|
||||
if original.name != self.name:
|
||||
raise ValidationError("Cannot change the name once it is set. First delete this entry and then create a new one instead.")
|
||||
super().clean()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
from rbac.models import RBACGroup
|
||||
|
||||
group = RBACGroup.objects.filter(identity_provider=self.identity_provider, uid=self.name).first()
|
||||
if group:
|
||||
group.categories.add(self.map_to)
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return f'Identity Provider Category Mapping {self.name}'
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
from rbac.models import RBACGroup
|
||||
|
||||
group = RBACGroup.objects.filter(identity_provider=self.identity_provider, uid=self.name).first()
|
||||
if group:
|
||||
group.categories.remove(self.map_to)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
class LoginOption(models.Model):
|
||||
title = models.CharField(max_length=100, help_text="Display name for this login option (e.g. Login through DEIC)")
|
||||
url = models.CharField(max_length=255, help_text="URL or path for this login option")
|
||||
ordering = models.PositiveIntegerField(default=0, help_text="Display order (smaller numbers appear first)")
|
||||
active = models.BooleanField(default=True, help_text="Whether this login option is currently active")
|
||||
|
||||
class Meta:
|
||||
ordering = ['ordering']
|
||||
verbose_name = "Login Option"
|
||||
verbose_name_plural = "Login Options"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
0
identity_providers/tests.py
Normal file
0
identity_providers/tests.py
Normal file
0
identity_providers/views.py
Normal file
0
identity_providers/views.py
Normal file
Reference in New Issue
Block a user