From bc16795fa8357033dfaaa224777ae909f6ff5c7f Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 11 Oct 2022 14:47:16 -0600 Subject: [PATCH 1/8] Allow use of API keys as alternative to account auth token for Twilio credentials --- engine/apps/base/models/live_setting.py | 20 ++++++++--- engine/apps/base/utils.py | 45 +++++++++++++++++++++++-- engine/apps/twilioapp/twilio_client.py | 7 +++- engine/settings/base.py | 2 ++ 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index abd5cf1e..129d7e94 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -35,6 +35,8 @@ class LiveSetting(models.Model): AVAILABLE_NAMES = ( "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", + "TWILIO_API_KEY_SID", + "TWILIO_API_KEY_SECRET", "TWILIO_NUMBER", "TWILIO_VERIFY_SERVICE_SID", "TELEGRAM_TOKEN", @@ -80,14 +82,24 @@ class LiveSetting(models.Model): "after you update them." ), "TWILIO_ACCOUNT_SID": ( - "Twilio username to allow amixr send sms and make phone calls, " + "Twilio account SID/username to allow OnCall send sms and make phone calls, " "" - "more info." + "more info. Required." + ), + "TWILIO_API_KEY_SID": ( + "Twilio API key SID/username to allow OnCall send sms and make phone calls, " + "" + "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 send sms and make phone calls, " + "" + "more info. Either (TWILIO_API_KEY_SID + TWILIO_API_KEY_SECRET) or TWILIO_AUTH_TOKEN is required." ), "TWILIO_AUTH_TOKEN": ( - "Twilio password to allow amixr send sms and make calls, " + "Twilio password to allow OnCall send sms and make calls, " "" - "more info." + "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 SMS, " diff --git a/engine/apps/base/utils.py b/engine/apps/base/utils.py index 0fe0d8a9..e57a78d7 100644 --- a/engine/apps/base/utils.py +++ b/engine/apps/base/utils.py @@ -50,24 +50,63 @@ class LiveSettingValidator: check_fn = getattr(self, check_fn_name) return check_fn(self.live_setting.value) + @classmethod + def _check_twilio_api_key_sid(cls, twilio_api_key_sid): + if live_settings.TWILIO_AUTH_TOKEN: + return + + try: + Client( + twilio_api_key_sid, live_settings.TWILIO_API_KEY_SECRET, live_settings.TWILIO_ACCOUNT_SID + ).api.applications.list(limit=1) + except Exception as e: + return cls._prettify_twilio_error(e) + + @classmethod + def _check_twilio_api_key_secret(cls, twilio_api_key_secret): + if live_settings.TWILIO_AUTH_TOKEN: + return + + try: + Client( + live_settings.TWILIO_API_KEY_SID, twilio_api_key_secret, live_settings.TWILIO_ACCOUNT_SID + ).api.applications.list(limit=1) + except Exception as e: + return cls._prettify_twilio_error(e) + @classmethod def _check_twilio_account_sid(cls, twilio_account_sid): try: - Client(twilio_account_sid, live_settings.TWILIO_AUTH_TOKEN).api.accounts.list(limit=1) + if live_settings.TWILIO_API_KEY_SID and live_settings.TWILIO_API_KEY_SECRET: + Client( + live_settings.TWILIO_API_KEY_SID, live_settings.TWILIO_API_KEY_SECRET, twilio_account_sid + ).api.applications.list(limit=1) + else: + Client(twilio_account_sid, live_settings.TWILIO_AUTH_TOKEN).api.applications.list(limit=1) except Exception as e: return cls._prettify_twilio_error(e) @classmethod def _check_twilio_auth_token(cls, twilio_auth_token): + if live_settings.TWILIO_API_KEY_SID and live_settings.TWILIO_API_KEY_SECRET: + return + try: - Client(live_settings.TWILIO_ACCOUNT_SID, twilio_auth_token).api.accounts.list(limit=1) + Client(live_settings.TWILIO_ACCOUNT_SID, twilio_auth_token).api.applications.list(limit=1) except Exception as e: return cls._prettify_twilio_error(e) @classmethod def _check_twilio_verify_service_sid(cls, twilio_verify_service_sid): try: - twilio_client = Client(live_settings.TWILIO_ACCOUNT_SID, live_settings.TWILIO_AUTH_TOKEN) + if live_settings.TWILIO_API_KEY_SID and live_settings.TWILIO_API_KEY_SECRET: + twilio_client = Client( + live_settings.TWILIO_API_KEY_SID, + live_settings.TWILIO_API_KEY_SECRET, + live_settings.TWILIO_ACCOUNT_SID, + ) + else: + twilio_client = Client(live_settings.TWILIO_ACCOUNT_SID, live_settings.TWILIO_AUTH_TOKEN) twilio_client.verify.services(twilio_verify_service_sid).rate_limits.list(limit=1) except Exception as e: return cls._prettify_twilio_error(e) diff --git a/engine/apps/twilioapp/twilio_client.py b/engine/apps/twilioapp/twilio_client.py index 007d9e72..75d06403 100644 --- a/engine/apps/twilioapp/twilio_client.py +++ b/engine/apps/twilioapp/twilio_client.py @@ -17,7 +17,12 @@ logger = logging.getLogger(__name__) class TwilioClient: @property def twilio_api_client(self): - return Client(live_settings.TWILIO_ACCOUNT_SID, live_settings.TWILIO_AUTH_TOKEN) + if live_settings.TWILIO_API_KEY_SID and live_settings.TWILIO_API_KEY_SECRET: + return Client( + live_settings.TWILIO_API_KEY_SID, live_settings.TWILIO_API_KEY_SECRET, live_settings.TWILIO_ACCOUNT_SID + ) + else: + return Client(live_settings.TWILIO_ACCOUNT_SID, live_settings.TWILIO_AUTH_TOKEN) @property def twilio_number(self): diff --git a/engine/settings/base.py b/engine/settings/base.py index d9ec9f36..9a5dd09d 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -56,6 +56,8 @@ FEATURE_WEB_SCHEDULES_ENABLED = getenv_boolean("FEATURE_WEB_SCHEDULES_ENABLED", GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED = getenv_boolean("GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED", default=True) GRAFANA_CLOUD_NOTIFICATIONS_ENABLED = getenv_boolean("GRAFANA_CLOUD_NOTIFICATIONS_ENABLED", default=True) +TWILIO_API_KEY_SID = os.environ.get("TWILIO_API_KEY_SID") +TWILIO_API_KEY_SECRET = os.environ.get("TWILIO_API_KEY_SECRET") TWILIO_ACCOUNT_SID = os.environ.get("TWILIO_ACCOUNT_SID") TWILIO_AUTH_TOKEN = os.environ.get("TWILIO_AUTH_TOKEN") TWILIO_NUMBER = os.environ.get("TWILIO_NUMBER") From d0b851299267a8e1e091ee9205bfcb88fb222ec0 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Wed, 12 Oct 2022 11:01:17 +0200 Subject: [PATCH 2/8] Grammar --- engine/apps/base/models/live_setting.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index 129d7e94..770e3eb4 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -82,24 +82,24 @@ class LiveSetting(models.Model): "after you update them." ), "TWILIO_ACCOUNT_SID": ( - "Twilio account SID/username to allow OnCall send sms and make phone calls, " + "Twilio account SID/username to allow OnCall to send SMSes and make phone calls, see " "" - "more info. Required." + "here for more info. Required." ), "TWILIO_API_KEY_SID": ( - "Twilio API key SID/username to allow OnCall send sms and make phone calls, " + "Twilio API key SID/username to allow OnCall to send SMSes and make phone calls, see" "" - "more info. Either (TWILIO_API_KEY_SID + TWILIO_API_KEY_SECRET) or TWILIO_AUTH_TOKEN is required." + "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 send sms and make phone calls, " + "Twilio API key secret/password to allow OnCall to send SMSes and make phone calls, see" "" - "more info. Either (TWILIO_API_KEY_SID + TWILIO_API_KEY_SECRET) or TWILIO_AUTH_TOKEN is required." + "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 send sms and make calls, " + "Twilio password to allow OnCall to send SMSes and make calls, " "" - "more info. Either (TWILIO_API_KEY_SID + TWILIO_API_KEY_SECRET) or TWILIO_AUTH_TOKEN is required." + "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 SMS, " From 0152c27f5e77495f3428a58398cb94227fecf5d4 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 18 Oct 2022 16:05:49 -0600 Subject: [PATCH 3/8] Re-validate all twilio settings when any change as they depend on each other --- engine/apps/api/views/live_setting.py | 18 +++++++++--------- engine/apps/base/models/live_setting.py | 25 ++++++++++++++++++++----- engine/apps/base/utils.py | 9 ++++++++- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/engine/apps/api/views/live_setting.py b/engine/apps/api/views/live_setting.py index d2c77ab0..8040ee48 100644 --- a/engine/apps/api/views/live_setting.py +++ b/engine/apps/api/views/live_setting.py @@ -38,40 +38,40 @@ class LiveSettingViewSet(PublicPrimaryKeyMixin, viewsets.ModelViewSet): return queryset def perform_update(self, serializer): + name = serializer.instance.name old_value = serializer.instance.value new_value = serializer.validated_data["value"] super().perform_update(serializer) if new_value != old_value: - self._post_update_hook(old_value) + self._post_update_hook(name, old_value) def perform_destroy(self, instance): + name = instance.name old_value = instance.value new_value = instance.default_value super().perform_destroy(instance) if new_value != old_value: - self._post_update_hook(old_value) + self._post_update_hook(name, old_value) - def _post_update_hook(self, old_value): - instance = self.get_object() - - if instance.name == "TELEGRAM_TOKEN": + def _post_update_hook(self, name, old_value): + if name == "TELEGRAM_TOKEN": self._reset_telegram_integration(old_token=old_value) register_telegram_webhook.delay() - if instance.name == "TELEGRAM_WEBHOOK_HOST": + if name == "TELEGRAM_WEBHOOK_HOST": register_telegram_webhook.delay() - if instance.name in ["SLACK_CLIENT_OAUTH_ID", "SLACK_CLIENT_OAUTH_SECRET"]: + if name in ["SLACK_CLIENT_OAUTH_ID", "SLACK_CLIENT_OAUTH_SECRET"]: organization = self.request.auth.organization slack_team_identity = organization.slack_team_identity if slack_team_identity is not None: unpopulate_slack_user_identities.delay(organization_pk=organization.pk, force=True) - if instance.name == "GRAFANA_CLOUD_ONCALL_TOKEN": + if name == "GRAFANA_CLOUD_ONCALL_TOKEN": from apps.oss_installation.models import CloudConnector CloudConnector.remove_sync() diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index 770e3eb4..f69c25a3 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -87,22 +87,22 @@ class LiveSetting(models.Model): "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" + "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" + "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, " + "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 SMS, " + "Number from which you will receive calls and SMSes, " "more info." ), "TWILIO_VERIFY_SERVICE_SID": ( @@ -140,6 +140,8 @@ class LiveSetting(models.Model): SECRET_SETTING_NAMES = ( "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", + "TWILIO_API_KEY_SID", + "TWILIO_API_KEY_SECRET", "TWILIO_VERIFY_SERVICE_SID", "SENDGRID_API_KEY", "SENDGRID_SECRET_KEY", @@ -186,8 +188,21 @@ class LiveSetting(models.Model): 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) + revalidate_twilio = False for setting_name in setting_names_to_populate: - cls.objects.create(name=setting_name, value=cls._get_setting_from_setting_file(setting_name)) + setting = cls.objects.create(name=setting_name, value=cls._get_setting_from_setting_file(setting_name)) + if setting.name.startswith("TWILIO"): + revalidate_twilio = True + + if revalidate_twilio: + cls.revalidate_twilio() + + @classmethod + def revalidate_twilio(cls): + twilio_settings = cls.objects.filter(name__startswith="TWILIO") + for setting in twilio_settings: + setting.error = LiveSettingValidator(live_setting=setting).get_error() + setting.save(update_fields=["error"]) @staticmethod def _get_setting_from_setting_file(setting_name): diff --git a/engine/apps/base/utils.py b/engine/apps/base/utils.py index e57a78d7..377dd443 100644 --- a/engine/apps/base/utils.py +++ b/engine/apps/base/utils.py @@ -34,13 +34,20 @@ live_settings = LiveSettingProxy() class LiveSettingValidator: + + EMPTY_VALID_NAMES = ( + "TWILIO_AUTH_TOKEN", + "TWILIO_API_KEY_SID", + "TWILIO_API_KEY_SECRET", + ) + def __init__(self, live_setting): self.live_setting = live_setting def get_error(self): check_fn_name = f"_check_{self.live_setting.name.lower()}" - if self.live_setting.value is None: + if self.live_setting.value is None and self.live_setting.name not in self.EMPTY_VALID_NAMES: return "Empty" # skip validation if there's no handler for it From 363f01935604ba65177feeef7974a42f07a5b04e Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 18 Oct 2022 16:12:57 -0600 Subject: [PATCH 4/8] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e27157..b2e3d12d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## v1.0.41 (TBD) +- Allow use of API keys as alternative to account auth token for Twilio + ## v1.0.40 (2022-10-05) - Improved database and celery backends support - Added script to import PagerDuty users to Grafana From ef37e6a6fd85b4ff62aa8e351d76b788fb94471f Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Thu, 20 Oct 2022 10:11:36 -0600 Subject: [PATCH 5/8] Revalidate all when any settings change instead of making a special case for twilio --- engine/apps/base/models/live_setting.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index f69c25a3..e07886f8 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -187,20 +187,18 @@ class LiveSetting(models.Model): 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 - revalidate_twilio = False for setting_name in setting_names_to_populate: - setting = cls.objects.create(name=setting_name, value=cls._get_setting_from_setting_file(setting_name)) - if setting.name.startswith("TWILIO"): - revalidate_twilio = True + cls.objects.create(name=setting_name, value=cls._get_setting_from_setting_file(setting_name)) - if revalidate_twilio: - cls.revalidate_twilio() + cls.revalidate_settings() @classmethod - def revalidate_twilio(cls): - twilio_settings = cls.objects.filter(name__startswith="TWILIO") - for setting in twilio_settings: + def revalidate_settings(cls): + settings_to_validate = cls.objects.all() + for setting in settings_to_validate: setting.error = LiveSettingValidator(live_setting=setting).get_error() setting.save(update_fields=["error"]) From e019c70f671c9a6562a84f86787d1e95b7a3dc51 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 1 Nov 2022 18:22:50 -0600 Subject: [PATCH 6/8] Handle validation on update, remove redundant get_error call --- engine/apps/api/views/live_setting.py | 1 + engine/apps/base/models/live_setting.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/apps/api/views/live_setting.py b/engine/apps/api/views/live_setting.py index 8040ee48..23830fe4 100644 --- a/engine/apps/api/views/live_setting.py +++ b/engine/apps/api/views/live_setting.py @@ -46,6 +46,7 @@ class LiveSettingViewSet(PublicPrimaryKeyMixin, viewsets.ModelViewSet): if new_value != old_value: self._post_update_hook(name, old_value) + LiveSetting.revalidate_settings() def perform_destroy(self, instance): name = instance.name diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index c6ac28f0..0b41e47b 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -198,7 +198,6 @@ class LiveSetting(models.Model): def revalidate_settings(cls): settings_to_validate = cls.objects.all() for setting in settings_to_validate: - setting.error = LiveSettingValidator(live_setting=setting).get_error() setting.save(update_fields=["error"]) @staticmethod From a331a3f392de5cb6042bccd057172ef09a1ea570 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 1 Nov 2022 18:24:44 -0600 Subject: [PATCH 7/8] Rename to validate_settings --- engine/apps/api/views/live_setting.py | 2 +- engine/apps/base/models/live_setting.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/engine/apps/api/views/live_setting.py b/engine/apps/api/views/live_setting.py index 23830fe4..57566f52 100644 --- a/engine/apps/api/views/live_setting.py +++ b/engine/apps/api/views/live_setting.py @@ -46,7 +46,7 @@ class LiveSettingViewSet(PublicPrimaryKeyMixin, viewsets.ModelViewSet): if new_value != old_value: self._post_update_hook(name, old_value) - LiveSetting.revalidate_settings() + LiveSetting.validate_settings() def perform_destroy(self, instance): name = instance.name diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index 0b41e47b..75219a00 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -192,10 +192,10 @@ class LiveSetting(models.Model): for setting_name in setting_names_to_populate: cls.objects.create(name=setting_name, value=cls._get_setting_from_setting_file(setting_name)) - cls.revalidate_settings() + cls.validate_settings() @classmethod - def revalidate_settings(cls): + def validate_settings(cls): settings_to_validate = cls.objects.all() for setting in settings_to_validate: setting.save(update_fields=["error"]) From 71c8d17c78572cb346a461cb12359339811c528f Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 1 Nov 2022 18:29:28 -0600 Subject: [PATCH 8/8] Changelog cleanup --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2092d0a6..6dbcecb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,6 @@ - Add personal email notifications - Bug fixes ->>>>>>> dev ## v1.0.40 (2022-10-05)