"""Models concerning the Trustpoint settings."""
from typing import ClassVar
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from notifications.models import WeakECCCurve, WeakSignatureAlgorithm
from pki.util.keys import AutoGenPkiKeyAlgorithm
[docs]
class SecurityConfig(models.Model):
"""Security Configuration model."""
[docs]
class SecurityModeChoices(models.TextChoices):
"""Types of security modes."""
[docs]
DEV = '0', _('Testing env')
[docs]
MEDIUM = '2', _('Medium')
[docs]
HIGHEST = '4', _('Highest')
[docs]
security_mode = models.CharField(max_length=6, choices=SecurityModeChoices, default=SecurityModeChoices.LOW)
[docs]
auto_gen_pki = models.BooleanField(default=False)
[docs]
auto_gen_pki_key_algorithm = models.CharField(
max_length=24, choices=AutoGenPkiKeyAlgorithm, default=AutoGenPkiKeyAlgorithm.RSA2048
)
[docs]
NOTIFICATION_CONFIGURATIONS: ClassVar[dict] = {
SecurityModeChoices.DEV: {
'cert_expiry_warning_days': 10,
'issuing_ca_expiry_warning_days': 10,
'rsa_minimum_key_size': 1024,
'weak_ecc_curves': [],
'weak_signature_algorithms': [],
},
SecurityModeChoices.LOW: {
'cert_expiry_warning_days': 15,
'issuing_ca_expiry_warning_days': 15,
'rsa_minimum_key_size': 1024,
'weak_ecc_curves': [],
'weak_signature_algorithms': [],
},
SecurityModeChoices.MEDIUM: {
'cert_expiry_warning_days': 20,
'issuing_ca_expiry_warning_days': 20,
'rsa_minimum_key_size': 2048,
'weak_ecc_curves': [
WeakECCCurve.ECCCurveChoices.SECP160R1,
WeakECCCurve.ECCCurveChoices.SECT163K1,
WeakECCCurve.ECCCurveChoices.SECT163R2,
],
'weak_signature_algorithms': [
WeakSignatureAlgorithm.SignatureChoices.MD5,
WeakSignatureAlgorithm.SignatureChoices.SHA1,
],
},
SecurityModeChoices.HIGH: {
'cert_expiry_warning_days': 25,
'issuing_ca_expiry_warning_days': 25,
'rsa_minimum_key_size': 3072,
'weak_ecc_curves': [
WeakECCCurve.ECCCurveChoices.SECP160R1,
WeakECCCurve.ECCCurveChoices.SECT163K1,
WeakECCCurve.ECCCurveChoices.SECT163R2,
],
'weak_signature_algorithms': [
WeakSignatureAlgorithm.SignatureChoices.MD5,
WeakSignatureAlgorithm.SignatureChoices.SHA1,
WeakSignatureAlgorithm.SignatureChoices.SHA224,
],
},
SecurityModeChoices.HIGHEST: {
'cert_expiry_warning_days': 30,
'issuing_ca_expiry_warning_days': 30,
'rsa_minimum_key_size': 4096,
'weak_ecc_curves': [
WeakECCCurve.ECCCurveChoices.SECP160R1,
WeakECCCurve.ECCCurveChoices.SECT163K1,
WeakECCCurve.ECCCurveChoices.SECT163R2,
WeakECCCurve.ECCCurveChoices.SECP192R1,
WeakECCCurve.ECCCurveChoices.SECP224R1,
],
'weak_signature_algorithms': [
WeakSignatureAlgorithm.SignatureChoices.MD5,
WeakSignatureAlgorithm.SignatureChoices.SHA1,
WeakSignatureAlgorithm.SignatureChoices.SHA224,
],
},
}
[docs]
notification_config = models.OneToOneField(
'notifications.NotificationConfig',
on_delete=models.CASCADE,
related_name='security_config',
null=True,
blank=False,
help_text=_('Notification configuration associated with this security level.'),
)
[docs]
def __str__(self) -> str:
"""Output as string."""
return f'{self.security_mode}'
[docs]
def apply_security_settings(self) -> None:
"""Apply appropriate configuration values based on the security mode."""
if self.security_mode and self.notification_config:
# Get the default configuration for the selected security level
config_values = self.NOTIFICATION_CONFIGURATIONS.get(self.security_mode, {})
# Apply values to the NotificationConfig
self.notification_config.cert_expiry_warning_days = config_values.get(
'cert_expiry_warning_days', self.notification_config.cert_expiry_warning_days
)
self.notification_config.issuing_ca_expiry_warning_days = config_values.get(
'issuing_ca_expiry_warning_days', self.notification_config.issuing_ca_expiry_warning_days
)
self.notification_config.rsa_minimum_key_size = config_values.get(
'rsa_minimum_key_size', self.notification_config.rsa_minimum_key_size
)
# Update WeakECCCurve and WeakSignatureAlgorithm relationships
weak_ecc_curve_oids = config_values.get('weak_ecc_curves', [])
weak_signature_algorithm_oids = config_values.get('weak_signature_algorithms', [])
weak_ecc_curves = WeakECCCurve.objects.filter(oid__in=weak_ecc_curve_oids)
weak_signature_algorithms = WeakSignatureAlgorithm.objects.filter(oid__in=weak_signature_algorithm_oids)
self.notification_config.weak_ecc_curves.set(weak_ecc_curves)
self.notification_config.weak_signature_algorithms.set(weak_signature_algorithms)
self.notification_config.save()
[docs]
class TlsSettings(models.Model):
"""TLS settings model"""
[docs]
ipv4_address = models.GenericIPAddressField(protocol="IPv4", null=True, blank=True)
@classmethod
[docs]
def get_first_ipv4_address(cls) -> str:
"""Get the first IPv4 address or a default value."""
try:
network_settings = cls.objects.get(id=1)
ipv4_address = network_settings.ipv4_address
except cls.DoesNotExist:
ipv4_address = '127.0.0.1'
return ipv4_address
[docs]
class AppVersion(models.Model):
[docs]
objects: models.Manager['AppVersion']
[docs]
version = models.CharField(max_length=17)
[docs]
last_updated = models.DateTimeField(auto_now=True)
[docs]
def __str__(self) -> str:
return f'{self.version} @ {self.last_updated.isoformat()}'
[docs]
class BackupOptions(models.Model):
"""A singleton model (we always operate with pk=1) for backup settings.
We store host/port/user/local_storage, plus either a password or an SSH key.
"""
[docs]
class AuthMethod(models.TextChoices):
[docs]
PASSWORD = 'password', 'Password'
[docs]
SSH_KEY = 'ssh_key', 'SSH Key'
[docs]
local_storage = models.BooleanField(default=True, verbose_name=_('Use local storage'))
[docs]
sftp_storage = models.BooleanField(default=False, verbose_name=_('Use SFTP storage'))
[docs]
host = models.CharField(max_length=255, verbose_name=_('Host'), blank=True)
[docs]
port = models.PositiveIntegerField(default=2222, verbose_name=_('Port'), blank=True)
[docs]
user = models.CharField(max_length=128, verbose_name=_('Username'), blank=True)
[docs]
auth_method = models.CharField(
max_length=10,
choices=AuthMethod.choices,
default=AuthMethod.PASSWORD,
verbose_name=_('Authentication Method')
)
# TODO (Dome): Storing passwords in plain text
[docs]
password = models.CharField(
max_length=128,
blank=True,
verbose_name=_('Password'),
help_text=_('Plainβtext password for SFTP.')
)
[docs]
private_key = models.TextField(
blank=True,
verbose_name=_('SSH Private Key (PEM format)'),
help_text=_('Paste the private key here (PEM).')
)
[docs]
key_passphrase = models.CharField(
max_length=128,
blank=True,
verbose_name=_('Key Passphrase'),
help_text=_('Passphrase for the private key, if any.')
)
[docs]
remote_directory = models.CharField(
max_length=512,
blank=True,
default='/upload/trustpoint/',
verbose_name=_('Remote Directory'),
help_text=_('Remote directory (e.g. /backups/) where files should be uploaded. '
'Trailing slash is optional.'),
)
[docs]
def save(self, *args, **kwargs):
"""Ensure only one instance exists (singleton pattern)."""
self.full_clean()
super().save(*args, **kwargs)
[docs]
def clean(self):
"""Prevent the creation of more than one instance."""
if BackupOptions.objects.exists() and not self.pk:
raise ValidationError("Only one BackupOptions instance is allowed.")
return super().clean()
[docs]
def __str__(self) -> str:
return f'{self.user}@{self.host}:{self.port} ({self.auth_method})'
[docs]
class LoggingConfig(models.Model):
"""Logging Configuration model."""
[docs]
class LogLevelChoices(models.TextChoices):
"""Types of log levels."""
[docs]
DEBUG = '0', _('Debug')
[docs]
WARNING = '2', _('Warning')
[docs]
ERROR = '3', _('Error')
[docs]
CRITICAL = '4', _('Critical')
[docs]
log_level = models.CharField(max_length=8, choices=LogLevelChoices, default=LogLevelChoices.INFO)
[docs]
last_updated = models.DateTimeField(auto_now=True)
[docs]
def __str__(self) -> str:
"""Output as string."""
return f'{self.log_level}'