from django.conf import settings from django.core.validators import MinLengthValidator from django.db import models from django.db.models import JSONField from apps.base.utils import LiveSettingValidator from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length def generate_public_primary_key_for_live_setting(): prefix = "L" new_public_primary_key = generate_public_primary_key(prefix) failure_counter = 0 while LiveSetting.objects.filter(public_primary_key=new_public_primary_key).exists(): new_public_primary_key = increase_public_primary_key_length( failure_counter=failure_counter, prefix=prefix, model_name="LiveSetting" ) failure_counter += 1 return new_public_primary_key class LiveSetting(models.Model): public_primary_key = models.CharField( max_length=20, validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)], unique=True, default=generate_public_primary_key_for_live_setting, ) name = models.CharField(max_length=50, unique=True) value = JSONField(null=True, default=None) error = models.TextField(null=True, default=None) AVAILABLE_NAMES = ( "EMAIL_HOST", "EMAIL_PORT", "EMAIL_HOST_USER", "EMAIL_HOST_PASSWORD", "EMAIL_USE_TLS", "EMAIL_FROM_ADDRESS", "INBOUND_EMAIL_ESP", "INBOUND_EMAIL_DOMAIN", "INBOUND_EMAIL_WEBHOOK_SECRET", "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_API_KEY_SID", "TWILIO_API_KEY_SECRET", "TWILIO_NUMBER", "TWILIO_VERIFY_SERVICE_SID", "TELEGRAM_TOKEN", "TELEGRAM_WEBHOOK_HOST", "SLACK_CLIENT_OAUTH_ID", "SLACK_CLIENT_OAUTH_SECRET", "SLACK_SIGNING_SECRET", "SLACK_INSTALL_RETURN_REDIRECT_HOST", "SEND_ANONYMOUS_USAGE_STATS", "GRAFANA_CLOUD_ONCALL_TOKEN", "GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED", "GRAFANA_CLOUD_NOTIFICATIONS_ENABLED", "DANGEROUS_WEBHOOKS_ENABLED", ) DESCRIPTIONS = { "EMAIL_HOST": "SMTP server host. This email server will be used to notify users via email.", "EMAIL_PORT": "SMTP server port", "EMAIL_HOST_USER": "SMTP server user", "EMAIL_HOST_PASSWORD": "SMTP server password", "EMAIL_USE_TLS": "SMTP enable/disable TLS", "EMAIL_FROM_ADDRESS": "Email address used to send emails. If not specified, EMAIL_HOST_USER will be used.", "INBOUND_EMAIL_DOMAIN": "Inbound email domain", "INBOUND_EMAIL_ESP": ( "Inbound email ESP name. " "Available options: amazon_ses, mailgun, mailjet, mandrill, postal, postmark, sendgrid, sparkpost" ), "INBOUND_EMAIL_WEBHOOK_SECRET": "Inbound email webhook secret", "SLACK_SIGNING_SECRET": ( "Check instruction for details how to set up Slack. " "Slack secrets can't be verified on the backend, please try installing the Slack Bot " "after you update them." ), "SLACK_CLIENT_OAUTH_SECRET": ( "Check instruction for details how to set up Slack. " "Slack secrets can't be verified on the backend, please try installing the Slack Bot " "after you update them." ), "SLACK_CLIENT_OAUTH_ID": ( "Check instruction for details how to set up Slack. " "Slack secrets can't be verified on the backend, please try installing the Slack Bot " "after you update them." ), "SLACK_INSTALL_RETURN_REDIRECT_HOST": ( "Check instruction for details how to set up Slack. " "Slack secrets can't be verified on the backend, please try installing the Slack Bot " "after you update them." ), "TWILIO_ACCOUNT_SID": ( "Twilio account SID/username to allow OnCall to send SMSes and make phone calls, see " "" "here for more info. Required." ), "TWILIO_API_KEY_SID": ( "Twilio API key SID/username to allow OnCall to send SMSes and make phone calls, see " "" "here for more info. Either (TWILIO_API_KEY_SID + TWILIO_API_KEY_SECRET) or TWILIO_AUTH_TOKEN is required." ), "TWILIO_API_KEY_SECRET": ( "Twilio API key secret/password to allow OnCall to send SMSes and make phone calls, see " "" "here for more info. Either (TWILIO_API_KEY_SID + TWILIO_API_KEY_SECRET) or TWILIO_AUTH_TOKEN is required." ), "TWILIO_AUTH_TOKEN": ( "Twilio password to allow OnCall to send SMSes and make calls, see " "" "here for more info. Either (TWILIO_API_KEY_SID + TWILIO_API_KEY_SECRET) or TWILIO_AUTH_TOKEN is required." ), "TWILIO_NUMBER": ( "Number from which you will receive calls and SMSes, " "more info." ), "TWILIO_VERIFY_SERVICE_SID": ( "SID of Twilio service for number verification. " "You can create a service in Twilio web interface. " "twilio.com -> verify -> create new service." ), "TELEGRAM_TOKEN": ( "Secret token for Telegram bot, you can get one via BotFather." ), "TELEGRAM_WEBHOOK_HOST": ( "Externally available URL for Telegram to make requests. Must use https and ports 80, 88, 443, 8443." ), "SEND_ANONYMOUS_USAGE_STATS": ( "Grafana OnCall will send anonymous, but uniquely-identifiable usage analytics to Grafana Labs." " These statistics are sent to https://stats.grafana.org/. For more information on what's sent, look at the " " source code." ), "GRAFANA_CLOUD_ONCALL_TOKEN": "Secret token for Grafana Cloud OnCall instance.", "GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED": "Enable heartbeat integration with Grafana Cloud OnCall.", "GRAFANA_CLOUD_NOTIFICATIONS_ENABLED": "Enable SMS/call notifications via Grafana Cloud OnCall", "DANGEROUS_WEBHOOKS_ENABLED": "Enable outgoing webhooks to private networks", } SECRET_SETTING_NAMES = ( "EMAIL_HOST_PASSWORD", "INBOUND_EMAIL_WEBHOOK_SECRET", "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_API_KEY_SID", "TWILIO_API_KEY_SECRET", "TWILIO_VERIFY_SERVICE_SID", "SLACK_CLIENT_OAUTH_ID", "SLACK_CLIENT_OAUTH_SECRET", "SLACK_SIGNING_SECRET", "TELEGRAM_TOKEN", "GRAFANA_CLOUD_ONCALL_TOKEN", ) def __str__(self): return self.name @property def description(self): return self.DESCRIPTIONS.get(self.name) @property def default_value(self): return self._get_setting_from_setting_file(self.name) @property def is_secret(self): return self.name in self.SECRET_SETTING_NAMES @classmethod def get_setting(cls, setting_name): if not settings.FEATURE_LIVE_SETTINGS_ENABLED: return cls._get_setting_from_setting_file(setting_name) if setting_name not in cls.AVAILABLE_NAMES: raise ValueError( f"Setting with name '{setting_name}' is not in list of available names {cls.AVAILABLE_NAMES}" ) live_setting = cls.objects.filter(name=setting_name).first() if live_setting is not None: return live_setting.value else: return cls._get_setting_from_setting_file(setting_name) @classmethod def populate_settings_if_needed(cls): settings_in_db = cls.objects.filter(name__in=cls.AVAILABLE_NAMES).values_list("name", flat=True) setting_names_to_populate = set(cls.AVAILABLE_NAMES) - set(settings_in_db) if len(setting_names_to_populate) == 0: return for setting_name in setting_names_to_populate: cls.objects.create(name=setting_name, value=cls._get_setting_from_setting_file(setting_name)) cls.validate_settings() @classmethod def validate_settings(cls): settings_to_validate = cls.objects.all() for setting in settings_to_validate: setting.save(update_fields=["error"]) @staticmethod def _get_setting_from_setting_file(setting_name): return getattr(settings, setting_name) def save(self, *args, **kwargs): if self.name not in self.AVAILABLE_NAMES: raise ValueError( f"Setting with name '{self.name}' is not in list of available names {self.AVAILABLE_NAMES}" ) self.error = LiveSettingValidator(live_setting=self).get_error() super().save(*args, **kwargs)