Skip to content

Commit

Permalink
models for alerting (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
ansibleguy committed Mar 25, 2024
1 parent 888e603 commit 34ff551
Show file tree
Hide file tree
Showing 14 changed files with 170 additions and 15 deletions.
7 changes: 4 additions & 3 deletions src/ansibleguy-webui/aw/api_endpoints/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from aw.model.permission import CHOICE_PERMISSION_READ, CHOICE_PERMISSION_WRITE, CHOICE_PERMISSION_DELETE
from aw.api_endpoints.base import API_PERMISSION, get_api_user, GenericResponse, BaseResponse
from aw.utils.permission import has_credentials_permission, has_manager_privileges
from aw.config.hardcoded import SECRET_HIDDEN
from aw.utils.util import is_null
from aw.base import USERS

Expand Down Expand Up @@ -186,7 +187,7 @@ def post(self, request):
for field in BaseJobCredentials.SECRET_ATTRS:
value = serializer.validated_data[field]
if field in BaseJobCredentials.SECRET_ATTRS:
if is_null(value) or value == BaseJobCredentials.SECRET_HIDDEN:
if is_null(value) or value == SECRET_HIDDEN:
serializer.validated_data[field] = None

elif field == 'ssh_key':
Expand Down Expand Up @@ -376,9 +377,9 @@ def put(self, request, credentials_id: int):

try:
# not working with password properties: 'Job.objects.filter(id=job_id).update(**serializer.data)'
for field, value in serializer.data.items():
for field, value in serializer.validated_data.items():
if field in BaseJobCredentials.SECRET_ATTRS:
if is_null(value) or value == BaseJobCredentials.SECRET_HIDDEN:
if is_null(value) or value == SECRET_HIDDEN:
value = getattr(credentials, field)

elif field == 'ssh_key':
Expand Down
2 changes: 1 addition & 1 deletion src/ansibleguy-webui/aw/api_endpoints/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ def put(self, request, job_id: int):
)

try:
Job.objects.filter(id=job.id).update(**serializer.data)
Job.objects.filter(id=job.id).update(**serializer.validated_data)

except IntegrityError as err:
return Response(
Expand Down
2 changes: 1 addition & 1 deletion src/ansibleguy-webui/aw/api_endpoints/permission.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def post(self, request):
status=400,
)

return Response({'msg': f"Permission '{serializer.data['name']}' created successfully"})
return Response({'msg': f"Permission '{serializer.validated_data['name']}' created successfully"})


class APIPermissionItem(GenericAPIView):
Expand Down
4 changes: 2 additions & 2 deletions src/ansibleguy-webui/aw/api_endpoints/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def post(self, request):
except IntegrityError as err:
return Response(data={'msg': f"Provided repository data is not valid: '{err}'"}, status=400)

return Response({'msg': f"Repository '{serializer.data['name']}' created successfully"})
return Response({'msg': f"Repository '{serializer.validated_data['name']}' created successfully"})


class APIRepositoryItem(GenericAPIView):
Expand Down Expand Up @@ -204,7 +204,7 @@ def put(self, request, repo_id: int):
)

try:
Repository.objects.filter(id=repo_id).update(**serializer.data)
Repository.objects.filter(id=repo_id).update(**serializer.validated_data)
return Response(data={'msg': f"Repository '{repository.name}' updated"}, status=200)

except IntegrityError as err:
Expand Down
14 changes: 12 additions & 2 deletions src/ansibleguy-webui/aw/api_endpoints/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
from aw.config.main import config
from aw.model.system import SystemConfig, get_config_from_db
from aw.api_endpoints.base import API_PERMISSION, get_api_user, GenericResponse, BaseResponse
from aw.utils.util_no_config import is_set
from aw.utils.util_no_config import is_set, is_null
from aw.utils.debug import log
from aw.utils.permission import has_manager_privileges
from aw.config.hardcoded import SECRET_HIDDEN


class SystemConfigReadResponse(BaseResponse):
Expand All @@ -34,13 +35,18 @@ class SystemConfigReadResponse(BaseResponse):
logo_url = serializers.CharField()
ara_server = serializers.CharField()
global_environment_vars = serializers.CharField()
mail_server = serializers.CharField()
mail_transport = serializers.IntegerField()
mail_user = serializers.CharField()


class SystemConfigWriteRequest(serializers.ModelSerializer):
class Meta:
model = SystemConfig
fields = SystemConfig.api_fields_write

mail_pass = serializers.CharField(max_length=100, required=False, default=None, allow_blank=True)


class APISystemConfig(APIView):
http_method_names = ['put', 'get']
Expand Down Expand Up @@ -95,12 +101,16 @@ def put(self, request):
try:
changed = False
for setting, value in serializer.validated_data.items():
if setting in SystemConfig.SECRET_ATTRS:
if is_null(value) or value == SECRET_HIDDEN:
value = getattr(config_db, setting)

if is_set(value) and str(config[setting]) != str(value):
setattr(config_db, setting, value)
changed = True

if changed:
log(msg='System config change - updating', level=5)
log(msg='System config changed - updating', level=5)
config_db.save()

return Response(data={'msg': "System config updated"}, status=200)
Expand Down
5 changes: 5 additions & 0 deletions src/ansibleguy-webui/aw/config/form_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
'port': 'Listen Port',
'ssl_file_crt': 'SSL Certificate',
'ssl_file_key': 'SSL Private-Key',
'mail_server': 'Mail Server',
'mail_transport': 'Mail Transport',
'mail_user': 'Mail Login Username',
'mail_pass': 'Mail Login Password',
}
}
}
Expand Down Expand Up @@ -197,6 +201,7 @@
'Documentation - Integrations</a>',
'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',
}
}
}
1 change: 1 addition & 0 deletions src/ansibleguy-webui/aw/config/hardcoded.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
REPO_CLONE_TIMEOUT = 300
ENV_KEY_CONFIG = 'AW_CONFIG'
ENV_KEY_SAML = 'AW_SAML'
SECRET_HIDDEN = '⬤' * 15
83 changes: 83 additions & 0 deletions src/ansibleguy-webui/aw/model/alert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from django.db import models

from aw.model.base import BaseModel, BareModel, CHOICES_BOOL
from aw.model.job import Job

from aw.base import USERS, GROUPS

ALERT_TYPE_EMAIL = 0
ALERT_TYPE_PLUGIN = 1

ALERT_TYPE_CHOICES = [
(ALERT_TYPE_EMAIL, 'E-Mail'),
(ALERT_TYPE_PLUGIN, 'Plugin'),
]

ALERT_CONDITION_FAIL = 0
ALERT_CONDITION_SUCCESS = 1
ALERT_CONDITION_ALWAYS = 2

ALERT_CONDITION_CHOICES = [
(ALERT_CONDITION_FAIL, 'Failure'),
(ALERT_CONDITION_SUCCESS, 'Success'),
(ALERT_CONDITION_ALWAYS, 'Always'),
]


class AlertPlugin(BaseModel):
name = models.CharField(max_length=100)
executable = models.CharField(max_length=300)


class BaseAlert(BaseModel):
name = models.CharField(max_length=100)
alert_type = models.PositiveSmallIntegerField(choices=ALERT_TYPE_CHOICES, default=ALERT_TYPE_EMAIL)
plugin = models.ForeignKey(
AlertPlugin, on_delete=models.CASCADE, null=True, related_name='alert_fk_plugin',
)
jobs_all = models.BooleanField(choices=CHOICES_BOOL, default=False)
jobs = models.ManyToManyField(
Job,
through='AlertJobMapping',
through_fields=('alert', 'job'),
)
condition = models.PositiveSmallIntegerField(choices=ALERT_CONDITION_CHOICES, default=ALERT_CONDITION_FAIL)

class Meta:
abstract = True


class AlertGlobal(BaseAlert):
def __str__(self) -> str:
return f"Global Alert '{self.name}'"


class AlertGroup(BaseAlert):
group = models.ForeignKey(
GROUPS, on_delete=models.SET_NULL, null=True, related_name='alert_fk_group',
)

def __str__(self) -> str:
return f"Alert '{self.name}' of group '{self.group.name}'"


class AlertUser(BaseAlert):
user = models.ForeignKey(
USERS, on_delete=models.SET_NULL, null=True, related_name='alert_fk_user', editable=False,
)

def __str__(self) -> str:
return f"Alert '{self.name}' of user '{self.user.username}'"


class AlertUserJobMapping(BareModel):
job = models.ForeignKey(Job, on_delete=models.CASCADE)
alert = models.ForeignKey(AlertUser, on_delete=models.CASCADE)

def __str__(self) -> str:
return f"{self.alert} linked to job '{self.job.name}'"

class Meta:
constraints = [
models.UniqueConstraint(fields=['job', 'alert'], name='alertjobmmap_unique')
]
1 change: 0 additions & 1 deletion src/ansibleguy-webui/aw/model/job_credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ class BaseJobCredentials(BaseModel):
'vault_file': '--vault-password-file',
'vault_id': '--vault-id',
}
SECRET_HIDDEN = '⬤' * 15

name = models.CharField(max_length=100, null=False, blank=False)
connect_user = models.CharField(max_length=100, **DEFAULT_NONE)
Expand Down
39 changes: 38 additions & 1 deletion src/ansibleguy-webui/aw/model/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,32 @@
from aw.config.defaults import CONFIG_DEFAULTS
from aw.config.environment import check_aw_env_var_is_set
from aw.config.main import VERSION
from aw.utils.util import is_null
from aw.utils.crypto import decrypt, encrypt

MAIL_TRANSPORT_TYPE_PLAIN = 0
MAIL_TRANSPORT_TYPE_SSL = 1
MAIL_TRANSPORT_TYPE_STARTTLS = 2

MAIL_TRANSPORT_TYPE_CHOICES = [
(MAIL_TRANSPORT_TYPE_PLAIN, 'Unencrypted'),
(MAIL_TRANSPORT_TYPE_SSL, 'SSL'),
(MAIL_TRANSPORT_TYPE_STARTTLS, 'StartTLS'),
]


# NOTE: add default-values to config.defaults.CONFIG_DEFAULTS
class SystemConfig(BaseModel):
SECRET_ATTRS = ['mail_pass']
form_fields = [
'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',
]

# 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
api_fields_write = form_fields.copy()
api_fields_write.extend(SECRET_ATTRS)
api_fields_read_only = ['db', 'db_migrate', 'serve_static', 'deployment', 'version']

path_run = models.CharField(max_length=500, default='/tmp/ansible-webui')
Expand All @@ -29,12 +45,33 @@ class SystemConfig(BaseModel):
logo_url = models.CharField(max_length=500, **DEFAULT_NONE)
ara_server = models.CharField(max_length=300, **DEFAULT_NONE)
global_environment_vars = models.CharField(max_length=1000, **DEFAULT_NONE)
mail_server = models.CharField(max_length=300, default='127.0.0.1:25', blank=True, null=True)
mail_transport = models.PositiveSmallIntegerField(
choices=MAIL_TRANSPORT_TYPE_CHOICES, default=MAIL_TRANSPORT_TYPE_PLAIN,
)
mail_user = models.CharField(max_length=300, **DEFAULT_NONE)
_enc_mail_pass = models.CharField(max_length=500, **DEFAULT_NONE)

@classmethod
def get_set_env_vars(cls) -> list:
# grey-out settings in web-ui
return [field for field in cls.form_fields if check_aw_env_var_is_set(field)]

@property
def mail_pass(self) -> str:
if is_null(self._enc_mail_pass):
return ''

return decrypt(self._enc_mail_pass)

@mail_pass.setter
def mail_pass(self, value: str):
if is_null(value):
self._enc_mail_pass = None
return

self._enc_mail_pass = encrypt(value)

def __str__(self) -> str:
return 'Ansible-WebUI System Config'

Expand Down
11 changes: 8 additions & 3 deletions src/ansibleguy-webui/aw/templatetags/form_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
from django.core.validators import RegexValidator

from aw.model.job_credential import BaseJobCredentials
from aw.model.system import SystemConfig
from aw.utils.util import is_set
from aw.views.validation import AW_VALIDATIONS
from aw.config.hardcoded import SECRET_HIDDEN

register = template.Library()

FORM_SECRET_FIELDS = BaseJobCredentials.SECRET_ATTRS.copy()
FORM_SECRET_FIELDS.extend(SystemConfig.SECRET_ATTRS)


@register.filter
def get_form_field_attributes(bf: BoundField) -> str:
Expand Down Expand Up @@ -47,10 +52,10 @@ def get_form_required(bf: BoundField) -> str:

def get_form_field_value(bf: BoundField, existing: dict) -> (str, None):
# SECRET_ATTRS are not exposed here
if bf.name not in existing and bf.name not in BaseJobCredentials.SECRET_ATTRS:
if bf.name not in existing and bf.name not in FORM_SECRET_FIELDS:
return None

if bf.name in BaseJobCredentials.SECRET_ATTRS:
if bf.name in FORM_SECRET_FIELDS:
enc_field = '_enc_' + bf.name
if enc_field in existing and not is_set(existing[enc_field]):
return None
Expand All @@ -59,7 +64,7 @@ def get_form_field_value(bf: BoundField, existing: dict) -> (str, None):
value = None

else:
value = BaseJobCredentials.SECRET_HIDDEN
value = SECRET_HIDDEN

else:
if existing[bf.name] is None:
Expand Down
11 changes: 11 additions & 0 deletions src/ansibleguy-webui/aw/utils/alert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 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
3 changes: 3 additions & 0 deletions src/ansibleguy-webui/aw/views/forms/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class Meta:
label=FORM_LABEL['system']['config']['timezone'],
)
debug = forms.BooleanField(initial=CONFIG_DEFAULTS['debug'] or deployment_dev())
mail_pass = forms.CharField(
max_length=100, required=False, label=Meta.labels['mail_pass'],
)


@login_required
Expand Down
2 changes: 1 addition & 1 deletion test/integration/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def test_modify():
'repository': 2,
}},

# perms
# perms; todo: fix
{'l': 'permission/1', 'd': {'name': 'perm1'}},
])

Expand Down

0 comments on commit 34ff551

Please sign in to comment.