diff --git a/CHANGELOG.md b/CHANGELOG.md
index 35a2d0d2..efd7a181 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Change Log
+## v1.0.52 (TBD)
+
+- Allow use of API keys as alternative to account auth token for Twilio
+
## v1.0.51 (2022-11-05)
- Bug Fixes
diff --git a/engine/apps/api/views/live_setting.py b/engine/apps/api/views/live_setting.py
index d2c77ab0..57566f52 100644
--- a/engine/apps/api/views/live_setting.py
+++ b/engine/apps/api/views/live_setting.py
@@ -38,40 +38,41 @@ 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)
+ LiveSetting.validate_settings()
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 09845e6f..b5409ddc 100644
--- a/engine/apps/base/models/live_setting.py
+++ b/engine/apps/base/models/live_setting.py
@@ -41,6 +41,8 @@ class LiveSetting(models.Model):
"EMAIL_FROM_ADDRESS",
"TWILIO_ACCOUNT_SID",
"TWILIO_AUTH_TOKEN",
+ "TWILIO_API_KEY_SID",
+ "TWILIO_API_KEY_SECRET",
"TWILIO_NUMBER",
"TWILIO_VERIFY_SERVICE_SID",
"TELEGRAM_TOKEN",
@@ -92,17 +94,27 @@ 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 to send SMSes and make phone calls, see "
""
- "more info."
+ "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 amixr send sms and make calls, "
+ "Twilio password to allow OnCall to send SMSes and make calls, see "
""
- "more info."
+ "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": (
@@ -131,6 +143,8 @@ class LiveSetting(models.Model):
"EMAIL_HOST_PASSWORD",
"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",
@@ -174,10 +188,20 @@ 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
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)
diff --git a/engine/apps/base/utils.py b/engine/apps/base/utils.py
index 8dc1f5fa..98f593bf 100644
--- a/engine/apps/base/utils.py
+++ b/engine/apps/base/utils.py
@@ -32,13 +32,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 in (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
@@ -48,24 +55,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 2164ea80..8fa8e214 100644
--- a/engine/settings/base.py
+++ b/engine/settings/base.py
@@ -57,6 +57,8 @@ FEATURE_MULTIREGION_ENABLED = getenv_boolean("FEATURE_MULTIREGION_ENABLED", defa
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")