diff --git a/src/ansibleguy-webui/aw/api_endpoints/alert.py b/src/ansibleguy-webui/aw/api_endpoints/alert.py index bbfc44a..7502b7c 100644 --- a/src/ansibleguy-webui/aw/api_endpoints/alert.py +++ b/src/ansibleguy-webui/aw/api_endpoints/alert.py @@ -6,11 +6,22 @@ from drf_spectacular.utils import extend_schema, OpenApiResponse from aw.api_endpoints.base import API_PERMISSION, GenericResponse, get_api_user, api_docs_put, api_docs_delete, \ - api_docs_post + api_docs_post, BaseResponse from aw.utils.permission import has_manager_privileges from aw.model.alert import AlertPlugin, AlertGlobal, AlertGroup, AlertUser +class BaseAlertWriteRequest(BaseResponse): + # NOTE: not using modelserializer because issues with DRF and PUT unique constraints + name = serializers.CharField(required=True) + alert_type = serializers.IntegerField() + condition = serializers.IntegerField() + jobs = serializers.ListSerializer(child=serializers.IntegerField(), required=False) + jobs_all = serializers.BooleanField() + # todo: require alert to be provided if alert-type is plugin + plugin = serializers.IntegerField(required=False) + + class AlertPluginReadWrite(serializers.ModelSerializer): class Meta: model = AlertPlugin @@ -170,10 +181,8 @@ class Meta: condition_name = serializers.CharField() -class AlertUserWriteRequest(serializers.ModelSerializer): - class Meta: - model = AlertUser - fields = AlertUser.api_fields_write +class AlertUserWriteRequest(BaseAlertWriteRequest): + user = serializers.IntegerField(required=True) class APIAlertUser(GenericAPIView): @@ -324,12 +333,6 @@ class Meta: condition_name = serializers.CharField() -class AlertGlobalWriteRequest(serializers.ModelSerializer): - class Meta: - model = AlertGlobal - fields = AlertGlobal.api_fields_write - - class APIAlertGlobal(GenericAPIView): http_method_names = ['get', 'post'] serializer_class = AlertGlobalReadResponse @@ -360,7 +363,7 @@ def post(self, request): status=403, ) - serializer = AlertGlobalWriteRequest(data=request.data) + serializer = BaseAlertWriteRequest(data=request.data) if not serializer.is_valid(): return Response( @@ -408,7 +411,7 @@ def get(request, alert_id: int): return Response(data={'msg': f"Alert with ID {alert_id} does not exist"}, status=404) @extend_schema( - request=AlertGlobalWriteRequest, + request=BaseAlertWriteRequest, responses=api_docs_put('Alert'), summary='Modify an Alert.', operation_id='alert_global_edit', @@ -421,7 +424,7 @@ def put(self, request, alert_id: int): status=403, ) - serializer = AlertGlobalWriteRequest(data=request.data) + serializer = BaseAlertWriteRequest(data=request.data) if not serializer.is_valid(): return Response( @@ -484,10 +487,8 @@ class Meta: group_name = serializers.CharField() -class AlertGroupWriteRequest(serializers.ModelSerializer): - class Meta: - model = AlertGroup - fields = AlertGroup.api_fields_write +class AlertGroupWriteRequest(BaseAlertWriteRequest): + group = serializers.IntegerField(required=True) class APIAlertGroup(GenericAPIView): diff --git a/src/ansibleguy-webui/aw/api_endpoints/job_util.py b/src/ansibleguy-webui/aw/api_endpoints/job_util.py index 2ea2b17..b9f224c 100644 --- a/src/ansibleguy-webui/aw/api_endpoints/job_util.py +++ b/src/ansibleguy-webui/aw/api_endpoints/job_util.py @@ -49,7 +49,7 @@ def get_job_execution_serialized(execution: JobExecution) -> dict: serialized['job_name'] = execution.job.name serialized['job_comment'] = execution.job.comment serialized['user'] = execution.user.id if execution.user is not None else None - serialized['user_name'] = execution.user.username if execution.user is not None else 'Scheduled' + serialized['user_name'] = execution.user_name serialized['time_start'] = execution.time_created_str serialized['time_fin'] = None serialized['failed'] = None diff --git a/src/ansibleguy-webui/aw/api_endpoints/system.py b/src/ansibleguy-webui/aw/api_endpoints/system.py index f2ff605..bef5571 100644 --- a/src/ansibleguy-webui/aw/api_endpoints/system.py +++ b/src/ansibleguy-webui/aw/api_endpoints/system.py @@ -16,7 +16,7 @@ class SystemConfigReadResponse(BaseResponse): # todo: fix static fields.. duplicate logic in model - # SystemConfig.form_fields + # SystemConfig.api_fields_read path_run = serializers.CharField() path_play = serializers.CharField() path_log = serializers.CharField() @@ -38,6 +38,8 @@ class SystemConfigReadResponse(BaseResponse): mail_server = serializers.CharField() mail_transport = serializers.IntegerField() mail_user = serializers.CharField() + mail_sender = serializers.CharField() + mail_ssl_verify = serializers.BooleanField() class SystemConfigWriteRequest(serializers.ModelSerializer): @@ -64,7 +66,7 @@ def get(request): del request merged_config = {'read_only_settings': SystemConfig.api_fields_read_only} - for field in SystemConfig.form_fields + merged_config['read_only_settings']: + for field in SystemConfig.api_fields_read + merged_config['read_only_settings']: merged_config[field] = config[field] merged_config['read_only_settings'] += SystemConfig.get_set_env_vars() diff --git a/src/ansibleguy-webui/aw/config/form_metadata.py b/src/ansibleguy-webui/aw/config/form_metadata.py index 8580e97..bba50cf 100644 --- a/src/ansibleguy-webui/aw/config/form_metadata.py +++ b/src/ansibleguy-webui/aw/config/form_metadata.py @@ -94,6 +94,8 @@ 'ssl_file_key': 'SSL Private-Key', 'mail_server': 'Mail Server', 'mail_transport': 'Mail Transport', + 'mail_ssl_verify': 'Mail SSL Verification', + 'mail_sender': 'Mail Sender Address', 'mail_user': 'Mail Login Username', 'mail_pass': 'Mail Login Password', } @@ -211,7 +213,12 @@ 'Documentation - Integrations', 'global_environment_vars': 'Set environmental variables that will be added to every job execution. ' 'Comma-separated list of key-value pairs. (VAR1=TEST1,VAR2=0)', - 'mail_server': 'Mail Server to use for Alert Mails', + 'mail_server': 'Mail Server to use for Alert Mails. Combination of server and port (default 25)', + 'mail_ssl_verify': 'En- or disable SSL certificate verification. ' + 'If enabled - the certificate SAN has to contain the mail-server FQDN ' + 'and must be issued from a trusted CA', + 'mail_sender': 'Mail Sender Address to use for Alert Mails', + 'mail_transport': 'The default port mapping is: 25 = Unencrypted, 465 = SSL, 587 = StartTLS', } } } diff --git a/src/ansibleguy-webui/aw/execute/alert.py b/src/ansibleguy-webui/aw/execute/alert.py new file mode 100644 index 0000000..7c2c0f1 --- /dev/null +++ b/src/ansibleguy-webui/aw/execute/alert.py @@ -0,0 +1,70 @@ +from django.db.models import Q + +from aw.base import USERS +from aw.model.base import JOB_EXEC_STATUS_SUCCESS +from aw.model.job import Job, JobExecution, JobExecutionResultHost +from aw.utils.permission import has_job_permission, CHOICE_PERMISSION_READ +from aw.model.alert import BaseAlert, AlertUser, AlertGroup, AlertGlobal, \ + ALERT_CONDITION_FAIL, ALERT_CONDITION_SUCCESS, ALERT_CONDITION_ALWAYS, \ + ALERT_TYPE_PLUGIN +from aw.execute.alert_plugin.plugin_email import alert_plugin_email +from aw.execute.alert_plugin.plugin_wrapper import alert_plugin_wrapper + + +class Alert: + def __init__(self, job: Job, execution: JobExecution): + self.job = job + self.execution = execution + self.failed = execution.status != JOB_EXEC_STATUS_SUCCESS + self.privileged_users = [] + for user in USERS.objects.all(): + if has_job_permission(user=user, job=job, permission_needed=CHOICE_PERMISSION_READ): + self.privileged_users.append(user) + + self.stats = {} + if execution.result is None: + for stats in JobExecutionResultHost.objects.filter(result=execution.result): + self.stats[stats['hostname']] = { + attr: getattr(stats, attr) for attr in JobExecutionResultHost.STATS + } + + def _job_filter(self, model: type): + return model.objects.filter(Q(jobs=self.job) | Q(jobs_all=True)) + + def _condition_filter(self, alerts: list[BaseAlert]): + matching = [] + for alert in alerts: + if alert.condition == ALERT_CONDITION_ALWAYS or \ + (self.failed and alert.condition == ALERT_CONDITION_FAIL) or \ + (not self.failed and alert.condition == ALERT_CONDITION_SUCCESS): + matching.append(alert) + + return matching + + def _route(self, alert: BaseAlert, user: USERS): + if alert.alert_type == ALERT_TYPE_PLUGIN: + alert_plugin_wrapper(alert=alert, user=user, stats=self.stats, execution=self.execution) + + else: + alert_plugin_email(user=user, stats=self.stats, execution=self.execution) + + def _global(self): + for alert in self._condition_filter(self._job_filter(AlertGlobal)): + for user in self.privileged_users: + self._route(alert=alert, user=user) + + def _group(self): + for alert in self._condition_filter(self._job_filter(AlertGroup)): + for user in self.privileged_users: + if user.groups.filter(name=alert.group).exists(): + self._route(alert=alert, user=user) + + def _user(self): + for user in self.privileged_users: + for alert in self._condition_filter(self._job_filter(AlertUser).filter(user=user)): + self._route(alert=alert, user=user) + + def go(self): + self._global() + self._group() + self._user() diff --git a/src/ansibleguy-webui/aw/execute/alert_plugin/plugin_email.py b/src/ansibleguy-webui/aw/execute/alert_plugin/plugin_email.py new file mode 100644 index 0000000..59da52e --- /dev/null +++ b/src/ansibleguy-webui/aw/execute/alert_plugin/plugin_email.py @@ -0,0 +1,104 @@ +import ssl +from pathlib import Path +from smtplib import SMTP, SMTP_SSL, SMTPResponseException +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from json import dumps as json_dumps + +from aw.base import USERS +from aw.utils.util import valid_email +from aw.utils.debug import log +from aw.config.main import config +from aw.model.job import JobExecution +from aw.settings import get_main_web_address +from aw.model.system import MAIL_TRANSPORT_TYPE_SSL, MAIL_TRANSPORT_TYPE_STARTTLS + + +def _email_send(server: SMTP, user: USERS, stats: list[dict], execution: JobExecution): + server.login(user=config['mail_user'], password=config['mail_pass']) + msg = MIMEMultipart('alternative') + msg['Subject'] = f"Ansible WebUI Alert - Job '{execution.job.name}' - {execution.status_name}" + msg['From'] = config['mail_sender'] + msg['To'] = user.email + + text = f""" +Job: {execution.job.name} +Status: {execution.status_name} + +Executed by: {execution.user_name} +Start time: {execution.time_created_str} +""" + + if execution.result is not None: + text += f""" +Finish time: {execution.result.time_fin_str} +Duration: {execution.result.time_duration_str} +""" + + if execution.result.error is not None: + text += f""" +Short error message: '{execution.result.error.short}' +Long error message: '{execution.result.error.med}' +""" + + for log_attr in JobExecution.log_file_fields: + file = getattr(execution, log_attr) + if Path(file).is_file(): + text += f""" +{log_attr.replace('_', ' ').capitalize()}: {get_main_web_address()}{getattr(execution, log_attr + '_url')} +""" + + if len(stats) > 0: + text += f""" + +Raw stats: +{json_dumps(stats)} +""" + + msg.attach(MIMEText(text, 'plain')) + # msg.attach(MIMEText(html, 'html')) + + server.sendmail( + from_addr=config['mail_sender'], + to_addrs=user.email, + msg=msg.as_string() + ) + + +def alert_plugin_email(user: USERS, stats: list[dict], execution: JobExecution): + if user.email.endswith('@localhost') or not valid_email(user.email): + log(msg=f"User has an invalid email address configured: {user.username} ({user.email})", level=3) + return + + try: + server, port = config['mail_server'].split(':', 1) + + except ValueError: + server = config['mail_server'] + port = 25 + + try: + print(f"Alert user {user.username} via email ({server}:{port} => {user.email})") + ssl_context = ssl.create_default_context() + if config['mail_ssl_verify']: + ssl_context.check_hostname = True + ssl_context.verify_mode = ssl.CERT_REQUIRED + + else: + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + if config['mail_transport'] == MAIL_TRANSPORT_TYPE_SSL: + with SMTP_SSL(server, port, context=ssl_context) as server: + server.login(config['mail_user'], config['mail_pass']) + _email_send(server=server, user=user, stats=stats, execution=execution) + + else: + with SMTP(server, port) as server: + if config['mail_transport'] == MAIL_TRANSPORT_TYPE_STARTTLS: + server.starttls(context=ssl_context) + + _email_send(server=server, user=user, stats=stats, execution=execution) + + except (SMTPResponseException, OSError) as e: + log(msg=f"Got error sending alert mail: {e}", level=2) diff --git a/src/ansibleguy-webui/aw/execute/alert_plugin/plugin_wrapper.py b/src/ansibleguy-webui/aw/execute/alert_plugin/plugin_wrapper.py new file mode 100644 index 0000000..921ffb0 --- /dev/null +++ b/src/ansibleguy-webui/aw/execute/alert_plugin/plugin_wrapper.py @@ -0,0 +1,8 @@ +from aw.base import USERS +from aw.model.job import JobExecution, JobExecutionResultHost +from aw.model.alert import BaseAlert, AlertPlugin + + +def alert_plugin_wrapper(alert: BaseAlert, user: USERS, stats: [JobExecutionResultHost], execution: JobExecution): + # implement plugin interface + print(f"Alert user {user.username} via plugin {alert.plugin.executable}") diff --git a/src/ansibleguy-webui/aw/execute/play.py b/src/ansibleguy-webui/aw/execute/play.py index 534fe9b..2bb87d8 100644 --- a/src/ansibleguy-webui/aw/execute/play.py +++ b/src/ansibleguy-webui/aw/execute/play.py @@ -7,6 +7,7 @@ from aw.execute.play_util import runner_cleanup, runner_prep, parse_run_result, failure, runner_logs from aw.execute.util import get_path_run, is_execution_status, job_logs from aw.execute.repository import ExecuteRepository +from aw.execute.alert import Alert from aw.utils.util import datetime_w_tz, is_null, timed_lru_cache # get_ansible_versions from aw.utils.handlers import AnsibleConfigError from aw.utils.debug import log @@ -63,6 +64,7 @@ def _cancel_job() -> bool: del runner runner_cleanup(execution=execution, path_run=path_run, exec_repo=exec_repo) + Alert(job=job, execution=execution).go() except (OSError, AnsibleConfigError) as err: tb = traceback.format_exc(limit=1024) @@ -70,4 +72,5 @@ def _cancel_job() -> bool: execution=execution, exec_repo=exec_repo, path_run=path_run, result=result, error_s=str(err), error_m=tb, ) + Alert(job=job, execution=execution).go() raise diff --git a/src/ansibleguy-webui/aw/execute/play_util.py b/src/ansibleguy-webui/aw/execute/play_util.py index 0f3c3c9..cbec06a 100644 --- a/src/ansibleguy-webui/aw/execute/play_util.py +++ b/src/ansibleguy-webui/aw/execute/play_util.py @@ -274,6 +274,6 @@ def failure( result.failed = True result.error = job_error result.save() - execution.save() - runner_cleanup(execution=JobExecution ,path_run=path_run, exec_repo=exec_repo) + + runner_cleanup(execution=execution, path_run=path_run, exec_repo=exec_repo) diff --git a/src/ansibleguy-webui/aw/model/base.py b/src/ansibleguy-webui/aw/model/base.py index c4912f5..9fb559e 100644 --- a/src/ansibleguy-webui/aw/model/base.py +++ b/src/ansibleguy-webui/aw/model/base.py @@ -5,12 +5,13 @@ (False, 'No') ) DEFAULT_NONE = {'null': True, 'default': None, 'blank': True} +JOB_EXEC_STATUS_SUCCESS = 4 CHOICES_JOB_EXEC_STATUS = [ (0, 'Waiting'), (1, 'Starting'), (2, 'Running'), (3, 'Failed'), - (4, 'Finished'), + (JOB_EXEC_STATUS_SUCCESS, 'Finished'), (5, 'Stopping'), (6, 'Stopped'), ] diff --git a/src/ansibleguy-webui/aw/model/job.py b/src/ansibleguy-webui/aw/model/job.py index 7151fdd..6b6290a 100644 --- a/src/ansibleguy-webui/aw/model/job.py +++ b/src/ansibleguy-webui/aw/model/job.py @@ -255,6 +255,10 @@ def log_stdout_repo_url(self) -> str: def log_stderr_repo_url(self) -> str: return f"/api/job/{self.job.id}/{self.id}/log?type=stderr_repo" + @property + def user_name(self) -> str: + return self.user.username if self.user is not None else 'Scheduled' + class JobQueue(BareModel): job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='jobqueue_fk_job') diff --git a/src/ansibleguy-webui/aw/model/system.py b/src/ansibleguy-webui/aw/model/system.py index ec9393b..68e2456 100644 --- a/src/ansibleguy-webui/aw/model/system.py +++ b/src/ansibleguy-webui/aw/model/system.py @@ -22,15 +22,16 @@ # NOTE: add default-values to config.defaults.CONFIG_DEFAULTS class SystemConfig(BaseModel): SECRET_ATTRS = ['mail_pass'] - form_fields = [ + api_fields_read = [ 'path_run', 'path_play', 'path_log', 'timezone', 'run_timeout', 'session_timeout', 'path_ansible_config', 'path_ssh_known_hosts', 'debug', 'logo_url', 'ara_server', 'global_environment_vars', - 'mail_server', 'mail_transport', 'mail_user', + 'mail_server', 'mail_transport', 'mail_ssl_verify', 'mail_sender', 'mail_user', ] # NOTE: 'AW_DB' is needed to get this config from DB and 'AW_SECRET' cannot be saved because of security breach - api_fields_write = form_fields.copy() + api_fields_write = api_fields_read.copy() api_fields_write.extend(SECRET_ATTRS) + form_fields = api_fields_read.copy() api_fields_read_only = ['db', 'db_migrate', 'serve_static', 'deployment', 'version'] path_run = models.CharField(max_length=500, default='/tmp/ansible-webui') @@ -49,6 +50,8 @@ class SystemConfig(BaseModel): mail_transport = models.PositiveSmallIntegerField( choices=MAIL_TRANSPORT_TYPE_CHOICES, default=MAIL_TRANSPORT_TYPE_PLAIN, ) + mail_ssl_verify = models.BooleanField(default=True, choices=CHOICES_BOOL) + mail_sender = models.CharField(max_length=300, **DEFAULT_NONE) mail_user = models.CharField(max_length=300, **DEFAULT_NONE) _enc_mail_pass = models.CharField(max_length=500, **DEFAULT_NONE) diff --git a/src/ansibleguy-webui/aw/settings.py b/src/ansibleguy-webui/aw/settings.py index 948e000..b034958 100644 --- a/src/ansibleguy-webui/aw/settings.py +++ b/src/ansibleguy-webui/aw/settings.py @@ -151,6 +151,15 @@ def debug_mode() -> bool: f'https://{LISTEN_ADDRESS}:{PORT_WEB}' ]) + +def get_main_web_address() -> str: + if 'AW_HOSTNAMES' not in environ: + return f'http://localhost:{PORT_WEB}' + + _hostname = environ['AW_HOSTNAMES'].split(',', 1)[0] + return f'https://{_hostname}:{PORT_WEB}' + + if 'AW_PROXY' in environ: SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') USE_X_FORWARDED_HOST = True diff --git a/src/ansibleguy-webui/aw/templates/settings/alert.html b/src/ansibleguy-webui/aw/templates/settings/alert.html index 3212ff9..9ca2199 100644 --- a/src/ansibleguy-webui/aw/templates/settings/alert.html +++ b/src/ansibleguy-webui/aw/templates/settings/alert.html @@ -42,12 +42,13 @@

User Alerts


Group Alerts

-

Alert all members of a specific group

+

Alert all members of a specific group that are privileged for the job

+ @@ -75,7 +76,7 @@

Group Alerts

Global Alerts

-

Alert all existing users

+

Alert all existing users that are privileged for the job

Name Type Condition Actions
diff --git a/src/ansibleguy-webui/aw/templatetags/form_util.py b/src/ansibleguy-webui/aw/templatetags/form_util.py index c72584b..b63efc4 100644 --- a/src/ansibleguy-webui/aw/templatetags/form_util.py +++ b/src/ansibleguy-webui/aw/templatetags/form_util.py @@ -46,6 +46,12 @@ def form_field_is_dropdown(bf: BoundField) -> bool: return isinstance(bf.field.widget, Select) +# todo: change boolean fields to checkboxes +# @register.filter +# def form_field_is_checkbox(bf: BoundField) -> bool: +# return isinstance(bf.field.widget, CheckboxInput) + + def get_form_required(bf: BoundField) -> str: return ' required' if bf.field.required else '' @@ -56,7 +62,7 @@ def get_form_field_value(bf: BoundField, existing: dict) -> (str, None): return None if bf.name in FORM_SECRET_FIELDS: - enc_field = '_enc_' + bf.name + enc_field = f'_enc_{bf.name}' if enc_field in existing and not is_set(existing[enc_field]): return None @@ -137,7 +143,7 @@ def get_form_field_input(bf: BoundField, existing: dict) -> str: elif bf.field.initial is not None: field_value = f'value="{bf.field.initial}"' - if bf.name.find('_pass') != -1 or bf.name.find('_key') != -1: + if bf.name in FORM_SECRET_FIELDS or bf.name.find('_pass') != -1 or bf.name.find('_key') != -1: field_attrs += ' type="password"' elif bf.name in AW_VALIDATIONS['file_system_browse']: diff --git a/src/ansibleguy-webui/aw/utils/alert.py b/src/ansibleguy-webui/aw/utils/alert.py deleted file mode 100644 index 544029b..0000000 --- a/src/ansibleguy-webui/aw/utils/alert.py +++ /dev/null @@ -1,11 +0,0 @@ -# from aw.model.alert import AlertUser - -# check if there is any alert for the current job (matching the condition) -# get all users privileged to view the current job -# filter list to those that should be notified -# execute all alerts (global/group/user) - -# implement email -# https://realpython.com/python-send-email/ - -# implement plugin interface diff --git a/src/ansibleguy-webui/aw/utils/util.py b/src/ansibleguy-webui/aw/utils/util.py index c1a40b3..4f94fc5 100644 --- a/src/ansibleguy-webui/aw/utils/util.py +++ b/src/ansibleguy-webui/aw/utils/util.py @@ -5,6 +5,8 @@ from pathlib import Path from functools import lru_cache, wraps from math import ceil +from re import compile as regex_compile +from re import IGNORECASE from pkg_resources import get_distribution from crontab import CronTab @@ -146,3 +148,44 @@ def pretty_timedelta_str(sec: (int, float)) -> str: return f'{minutes}m {sec}s' return f'{sec}s' + +# source: https://validators.readthedocs.io/en/latest/_modules/validators/email.html +EMAIL_REGEX_USER = regex_compile( + # dot-atom + r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+" + r"(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$" + # quoted-string + r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|' + r"""\\[\001-\011\013\014\016-\177])*"$)""", + IGNORECASE +) +EMAIL_REGEX_DOMAIN = regex_compile( + # domain + r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' + r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)' + # literal form, ipv4 address (SMTP 4.1.3) + r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)' + r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', + IGNORECASE +) + + +def valid_email(email: str) -> bool: + if not email or '@' not in email: + return False + + user_part, domain_part = email.rsplit('@', 1) + + if not EMAIL_REGEX_USER.match(user_part): + return False + + if not EMAIL_REGEX_DOMAIN.match(domain_part): + # Try for possible IDN domain-part + try: + domain_part = domain_part.encode('idna').decode('ascii') + return EMAIL_REGEX_DOMAIN.match(domain_part) + + except UnicodeError: + return False + + return True diff --git a/src/ansibleguy-webui/aw/views/forms/settings.py b/src/ansibleguy-webui/aw/views/forms/settings.py index 023cb32..d83008d 100644 --- a/src/ansibleguy-webui/aw/views/forms/settings.py +++ b/src/ansibleguy-webui/aw/views/forms/settings.py @@ -117,11 +117,8 @@ def setting_alert_plugin_edit(request, plugin_id: int = None) -> HttpResponse: ) -def _choices_alert_user_plugins() -> list: - return [ - (plugin.id, plugin.name) - for plugin in AlertUser.objects.all() - ] +def _choices_alert_plugins() -> list: + return [(plugin.id, plugin.name) for plugin in AlertPlugin.objects.all()] class SettingAlertUserForm(forms.ModelForm): @@ -140,7 +137,7 @@ class Meta: plugin = forms.ChoiceField( required=False, widget=forms.Select, - choices=_choices_alert_user_plugins, + choices=_choices_alert_plugins, ) @@ -171,13 +168,6 @@ def setting_alert_user_edit(request, alert_id: int = None) -> HttpResponse: ) -def _choices_alert_group_plugins() -> list: - return [ - (plugin.id, plugin.name) - for plugin in AlertGroup.objects.all() - ] - - class SettingAlertGroupForm(forms.ModelForm): class Meta: model = AlertGroup @@ -194,7 +184,7 @@ class Meta: plugin = forms.ChoiceField( required=False, widget=forms.Select, - choices=_choices_alert_group_plugins, + choices=_choices_alert_plugins, ) group = forms.ChoiceField( required=True, @@ -230,13 +220,6 @@ def setting_alert_group_edit(request, alert_id: int = None) -> HttpResponse: ) -def _choices_alert_global_plugins() -> list: - return [ - (plugin.id, plugin.name) - for plugin in AlertGlobal.objects.all() - ] - - class SettingAlertGlobalForm(forms.ModelForm): class Meta: model = AlertGlobal @@ -253,7 +236,7 @@ class Meta: plugin = forms.ChoiceField( required=False, widget=forms.Select, - choices=_choices_alert_global_plugins, + choices=_choices_alert_plugins, ) diff --git a/src/ansibleguy-webui/aw/views/forms/system.py b/src/ansibleguy-webui/aw/views/forms/system.py index ce46b49..aad884a 100644 --- a/src/ansibleguy-webui/aw/views/forms/system.py +++ b/src/ansibleguy-webui/aw/views/forms/system.py @@ -9,8 +9,9 @@ from aw.utils.http import ui_endpoint_wrapper from aw.config.form_metadata import FORM_LABEL, FORM_HELP from aw.config.environment import AW_ENV_VARS, AW_ENV_VARS_SECRET -from aw.model.system import SystemConfig +from aw.model.system import SystemConfig, get_config_from_db from aw.utils.deployment import deployment_dev +from aw.model.base import CHOICES_BOOL class SystemConfigForm(forms.ModelForm): @@ -36,7 +37,9 @@ class Meta: choices=[(tz, tz) for tz in sorted(all_timezones)], label=FORM_LABEL['system']['config']['timezone'], ) - debug = forms.BooleanField(initial=CONFIG_DEFAULTS['debug'] or deployment_dev()) + debug = forms.ChoiceField( + initial=CONFIG_DEFAULTS['debug'] or deployment_dev(), choices=CHOICES_BOOL, + ) mail_pass = forms.CharField( max_length=100, required=False, label=Meta.labels['mail_pass'], ) @@ -49,11 +52,11 @@ def system_config(request) -> HttpResponse: form_method = 'put' form_api = 'config' + existing = {key: config[key] for key in SystemConfig.form_fields} + existing['_enc_mail_pass'] = get_config_from_db()._enc_mail_pass config_form_html = config_form.render( template_name='forms/snippet.html', - context={'form': config_form, 'existing': { - key: config[key] for key in SystemConfig.form_fields - }}, + context={'form': config_form, 'existing': existing}, ) return render( request, status=200, template_name='system/config.html',
Name