[FR] Enable restrictions on allowed domains for signup (#4172)

* [FR] Enable restrictions on allowed domains for signup
Fixes #4168

* raise permission errors

* add setting to page

* move checks to clean_email

* remove unneeded check

* simplify

* log error to database

* factor settings fnc call out

* Add validation before setting save

* add before_save to accepted tokens
This commit is contained in:
Matthias Mair 2023-01-08 22:22:50 +01:00 committed by GitHub
parent 41318e4056
commit be859183a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 4 deletions

View File

@ -215,9 +215,35 @@ class RegistratonMixin:
return super().is_open_for_signup(request, *args, **kwargs) return super().is_open_for_signup(request, *args, **kwargs)
return False return False
def clean_email(self, email):
"""Check if the mail is valid to the pattern in LOGIN_SIGNUP_MAIL_RESTRICTION (if enabled in settings)."""
mail_restriction = InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_RESTRICTION', None)
if not mail_restriction:
return super().clean_email(email)
split_email = email.split('@')
if len(split_email) != 2:
logger.error(f'The user {email} has an invalid email address')
raise forms.ValidationError(_('The provided primary email address is not valid.'))
mailoptions = mail_restriction.split(',')
for option in mailoptions:
if not option.startswith('@'):
log_error('LOGIN_SIGNUP_MAIL_RESTRICTION is not configured correctly')
raise forms.ValidationError(_('The provided primary email address is not valid.'))
else:
if split_email[1] == option[1:]:
return super().clean_email(email)
logger.info(f'The provided email domain for {email} is not approved')
raise forms.ValidationError(_('The provided email domain is not approved.'))
def save_user(self, request, user, form, commit=True): def save_user(self, request, user, form, commit=True):
"""Check if a default group is set in settings.""" """Check if a default group is set in settings."""
# Create the user
user = super().save_user(request, user, form) user = super().save_user(request, user, form)
# Check if a default group is set in settings
start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP') start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP')
if start_group: if start_group:
try: try:

View File

@ -11,6 +11,7 @@ import json
import logging import logging
import math import math
import os import os
import re
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import Enum from enum import Enum
@ -81,19 +82,33 @@ class BaseInvenTreeSetting(models.Model):
self.clean(**kwargs) self.clean(**kwargs)
self.validate_unique(**kwargs) self.validate_unique(**kwargs)
# Execute before_save action
self._call_settings_function('before_save', args, kwargs)
# Update this setting in the cache # Update this setting in the cache
if do_cache: if do_cache:
self.save_to_cache() self.save_to_cache()
super().save() super().save()
# Get after_save action # Execute after_save action
self._call_settings_function('after_save', args, kwargs)
def _call_settings_function(self, reference: str, args, kwargs):
"""Call a function associated with a particular setting.
Args:
reference (str): The name of the function to call
args: Positional arguments to pass to the function
kwargs: Keyword arguments to pass to the function
"""
# Get action
setting = self.get_setting_definition(self.key, *args, **kwargs) setting = self.get_setting_definition(self.key, *args, **kwargs)
after_save = setting.get('after_save', None) settings_fnc = setting.get(reference, None)
# Execute if callable # Execute if callable
if callable(after_save): if callable(settings_fnc):
after_save(self) settings_fnc(self)
@property @property
def cache_key(self): def cache_key(self):
@ -771,6 +786,19 @@ def update_instance_name(setting):
site_obj.save() site_obj.save()
def validate_email_domains(setting):
"""Validate the email domains setting."""
if not setting.value:
return
domains = setting.value.split(',')
for domain in domains:
if not domain:
raise ValidationError(_('An empty domain is not allowed.'))
if not re.match(r'^@[a-zA-Z0-9\.\-_]+$', domain):
raise ValidationError(_(f'Invalid domain name: {domain}'))
class InvenTreeSetting(BaseInvenTreeSetting): class InvenTreeSetting(BaseInvenTreeSetting):
"""An InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values). """An InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values).
@ -1375,6 +1403,13 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
}, },
'LOGIN_SIGNUP_MAIL_RESTRICTION': {
'name': _('Allowed domains'),
'description': _('Restrict signup to certain domains (comma-separated, strarting with @)'),
'default': '',
'before_save': validate_email_domains,
},
'SIGNUP_GROUP': { 'SIGNUP_GROUP': {
'name': _('Group on signup'), 'name': _('Group on signup'),
'description': _('Group to which new users are assigned on registration'), 'description': _('Group to which new users are assigned on registration'),

View File

@ -134,6 +134,7 @@ class SettingsTest(InvenTreeTestCase):
'units', 'units',
'requires_restart', 'requires_restart',
'after_save', 'after_save',
'before_save',
] ]
for k in setting.keys(): for k in setting.keys():

View File

@ -30,6 +30,7 @@
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_MAIL_TWICE" icon="fa-at" %} {% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_MAIL_TWICE" icon="fa-at" %}
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_PWD_TWICE" icon="fa-user-lock" %} {% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_PWD_TWICE" icon="fa-user-lock" %}
{% include "InvenTree/settings/setting.html" with key="SIGNUP_GROUP" icon="fa-users" %} {% include "InvenTree/settings/setting.html" with key="SIGNUP_GROUP" icon="fa-users" %}
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_MAIL_RESTRICTION" icon="fa-sitemap" %}
<tr> <tr>
<th><h5>{% trans 'Single Sign On' %}</h5></th> <th><h5>{% trans 'Single Sign On' %}</h5></th>
<td colspan='4'></td> <td colspan='4'></td>