Initial commit: Email alerts application

This commit is contained in:
Iyeoluwa Akinrinola
2025-07-25 11:31:36 +01:00
commit adfb625ae9
6322 changed files with 2882826 additions and 0 deletions
@@ -0,0 +1,161 @@
import jwt as jwt_lib
import time
__all__ = ["Jwt", "JwtDecodeError"]
class JwtDecodeError(Exception):
pass
class Jwt(object):
"""Base class for building a Json Web Token"""
GENERATE = object()
ALGORITHM = "HS256"
def __init__(
self,
secret_key,
issuer,
subject=None,
algorithm=None,
nbf=GENERATE,
ttl=3600,
valid_until=None,
):
self.secret_key = secret_key
""":type str: The secret used to encode the JWT"""
self.issuer = issuer
""":type str: The issuer of this JWT"""
self.subject = subject
""":type str: The subject of this JWT, omitted from payload by default"""
self.algorithm = algorithm or self.ALGORITHM
""":type str: The algorithm used to encode the JWT, defaults to 'HS256'"""
self.nbf = nbf
""":type int: Time in secs since epoch before which this JWT is invalid. Defaults to now."""
self.ttl = ttl
""":type int: Time to live of the JWT in seconds, defaults to 1 hour"""
self.valid_until = valid_until
""":type int: Time in secs since epoch this JWT is valid for. Overrides ttl if provided."""
self.__decoded_payload = None
self.__decoded_headers = None
def _generate_payload(self):
""":rtype: dict the payload of the JWT to send"""
raise NotImplementedError("Subclass must provide a payload.")
def _generate_headers(self):
""":rtype dict: Additional headers to include in the JWT, defaults to an empty dict"""
return {}
@classmethod
def _from_jwt(cls, headers, payload, key=None):
"""
Class specific implementation of from_jwt which should take jwt components and return
and instance of this Class with jwt information loaded.
:return: Jwt object containing the headers, payload and key
"""
jwt = Jwt(
secret_key=key,
issuer=payload.get("iss", None),
subject=payload.get("sub", None),
algorithm=headers.get("alg", None),
valid_until=payload.get("exp", None),
nbf=payload.get("nbf", None),
)
jwt.__decoded_payload = payload
jwt.__decoded_headers = headers
return jwt
@property
def payload(self):
if self.__decoded_payload:
return self.__decoded_payload
payload = self._generate_payload().copy()
payload["iss"] = self.issuer
payload["exp"] = int(time.time()) + self.ttl
if self.nbf is not None:
if self.nbf == self.GENERATE:
payload["nbf"] = int(time.time())
else:
payload["nbf"] = self.nbf
if self.valid_until:
payload["exp"] = self.valid_until
if self.subject:
payload["sub"] = self.subject
return payload
@property
def headers(self):
if self.__decoded_headers:
return self.__decoded_headers
headers = self._generate_headers().copy()
headers["typ"] = "JWT"
headers["alg"] = self.algorithm
return headers
def to_jwt(self, ttl=None):
"""
Encode this JWT object into a JWT string
:param int ttl: override the ttl configured in the constructor
:rtype: str The JWT string
"""
if not self.secret_key:
raise ValueError("JWT does not have a signing key configured.")
headers = self.headers.copy()
payload = self.payload.copy()
if ttl:
payload["exp"] = int(time.time()) + ttl
return jwt_lib.encode(
payload, self.secret_key, algorithm=self.algorithm, headers=headers
)
@classmethod
def from_jwt(cls, jwt, key=""):
"""
Decode a JWT string into a Jwt object
:param str jwt: JWT string
:param Optional[str] key: key used to verify JWT signature, if not provided then validation
is skipped.
:raises JwtDecodeError if decoding JWT fails for any reason.
:return: A DecodedJwt object containing the jwt information.
"""
verify = True if key else False
try:
headers = jwt_lib.get_unverified_header(jwt)
alg = headers.get("alg")
if alg != cls.ALGORITHM:
raise ValueError(
f"Incorrect decoding algorithm {alg}, "
f"expecting {cls.ALGORITHM}."
)
payload = jwt_lib.decode(
jwt,
key,
algorithms=[cls.ALGORITHM],
options={
"verify_signature": verify,
"verify_exp": True,
"verify_nbf": True,
},
)
except Exception as e:
raise JwtDecodeError(getattr(e, "message", str(e)))
return cls._from_jwt(headers, payload, key)
def __str__(self):
return "<JWT {}>".format(self.to_jwt())
@@ -0,0 +1,81 @@
import time
from twilio.jwt import Jwt
class AccessTokenGrant(object):
"""A Grant giving access to a Twilio Resource"""
@property
def key(self):
""":rtype str Grant's twilio specific key"""
raise NotImplementedError("Grant must have a key property.")
def to_payload(self):
""":return: dict something"""
raise NotImplementedError("Grant must implement to_payload.")
def __str__(self):
return "<{} {}>".format(self.__class__.__name__, self.to_payload())
class AccessToken(Jwt):
"""Access Token containing one or more AccessTokenGrants used to access Twilio Resources"""
ALGORITHM = "HS256"
def __init__(
self,
account_sid,
signing_key_sid,
secret,
grants=None,
identity=None,
nbf=Jwt.GENERATE,
ttl=3600,
valid_until=None,
region=None,
):
grants = grants or []
if any(not isinstance(g, AccessTokenGrant) for g in grants):
raise ValueError("Grants must be instances of AccessTokenGrant.")
self.account_sid = account_sid
self.signing_key_sid = signing_key_sid
self.identity = identity
self.region = region
self.grants = grants
super(AccessToken, self).__init__(
secret_key=secret,
algorithm=self.ALGORITHM,
issuer=signing_key_sid,
subject=self.account_sid,
nbf=nbf,
ttl=ttl,
valid_until=valid_until,
)
def add_grant(self, grant):
"""Add a grant to this AccessToken"""
if not isinstance(grant, AccessTokenGrant):
raise ValueError("Grant must be an instance of AccessTokenGrant.")
self.grants.append(grant)
def _generate_headers(self):
headers = {"cty": "twilio-fpa;v=1"}
if self.region and isinstance(self.region, str):
headers["twr"] = self.region
return headers
def _generate_payload(self):
now = int(time.time())
payload = {
"jti": "{}-{}".format(self.signing_key_sid, now),
"grants": {grant.key: grant.to_payload() for grant in self.grants},
}
if self.identity:
payload["grants"]["identity"] = self.identity
return payload
def __str__(self):
return "<{} {}>".format(self.__class__.__name__, self.to_jwt())
@@ -0,0 +1,183 @@
from twilio.jwt.access_token import AccessTokenGrant
import warnings
import functools
def deprecated(func):
"""This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
when the function is used."""
@functools.wraps(func)
def new_func(*args, **kwargs):
warnings.simplefilter("always", DeprecationWarning)
warnings.warn(
"Call to deprecated function {}.".format(func.__name__),
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter("default", DeprecationWarning)
return func(*args, **kwargs)
return new_func
class ChatGrant(AccessTokenGrant):
"""Grant to access Twilio Chat"""
def __init__(
self,
service_sid=None,
endpoint_id=None,
deployment_role_sid=None,
push_credential_sid=None,
):
self.service_sid = service_sid
self.endpoint_id = endpoint_id
self.deployment_role_sid = deployment_role_sid
self.push_credential_sid = push_credential_sid
@property
def key(self):
return "chat"
def to_payload(self):
grant = {}
if self.service_sid:
grant["service_sid"] = self.service_sid
if self.endpoint_id:
grant["endpoint_id"] = self.endpoint_id
if self.deployment_role_sid:
grant["deployment_role_sid"] = self.deployment_role_sid
if self.push_credential_sid:
grant["push_credential_sid"] = self.push_credential_sid
return grant
class SyncGrant(AccessTokenGrant):
"""Grant to access Twilio Sync"""
def __init__(self, service_sid=None, endpoint_id=None):
self.service_sid = service_sid
self.endpoint_id = endpoint_id
@property
def key(self):
return "data_sync"
def to_payload(self):
grant = {}
if self.service_sid:
grant["service_sid"] = self.service_sid
if self.endpoint_id:
grant["endpoint_id"] = self.endpoint_id
return grant
class VoiceGrant(AccessTokenGrant):
"""Grant to access Twilio Programmable Voice"""
def __init__(
self,
incoming_allow=None,
outgoing_application_sid=None,
outgoing_application_params=None,
push_credential_sid=None,
endpoint_id=None,
):
self.incoming_allow = incoming_allow
""" :type : bool """
self.outgoing_application_sid = outgoing_application_sid
""" :type : str """
self.outgoing_application_params = outgoing_application_params
""" :type : dict """
self.push_credential_sid = push_credential_sid
""" :type : str """
self.endpoint_id = endpoint_id
""" :type : str """
@property
def key(self):
return "voice"
def to_payload(self):
grant = {}
if self.incoming_allow is True:
grant["incoming"] = {}
grant["incoming"]["allow"] = True
if self.outgoing_application_sid:
grant["outgoing"] = {}
grant["outgoing"]["application_sid"] = self.outgoing_application_sid
if self.outgoing_application_params:
grant["outgoing"]["params"] = self.outgoing_application_params
if self.push_credential_sid:
grant["push_credential_sid"] = self.push_credential_sid
if self.endpoint_id:
grant["endpoint_id"] = self.endpoint_id
return grant
class VideoGrant(AccessTokenGrant):
"""Grant to access Twilio Video"""
def __init__(self, room=None):
self.room = room
@property
def key(self):
return "video"
def to_payload(self):
grant = {}
if self.room:
grant["room"] = self.room
return grant
class TaskRouterGrant(AccessTokenGrant):
"""Grant to access Twilio TaskRouter"""
def __init__(self, workspace_sid=None, worker_sid=None, role=None):
self.workspace_sid = workspace_sid
self.worker_sid = worker_sid
self.role = role
@property
def key(self):
return "task_router"
def to_payload(self):
grant = {}
if self.workspace_sid:
grant["workspace_sid"] = self.workspace_sid
if self.worker_sid:
grant["worker_sid"] = self.worker_sid
if self.role:
grant["role"] = self.role
return grant
class PlaybackGrant(AccessTokenGrant):
"""Grant to access Twilio Live stream"""
def __init__(self, grant=None):
"""Initialize a PlaybackGrant with a grant retrieved from the Twilio API."""
self.grant = grant
@property
def key(self):
"""Return the grant's key."""
return "player"
def to_payload(self):
"""Return the grant."""
return self.grant
@@ -0,0 +1,117 @@
from twilio.jwt import Jwt
from urllib.parse import urlencode
class ClientCapabilityToken(Jwt):
"""A token to control permissions with Twilio Client"""
ALGORITHM = "HS256"
def __init__(
self,
account_sid,
auth_token,
nbf=Jwt.GENERATE,
ttl=3600,
valid_until=None,
**kwargs
):
"""
:param str account_sid: The account sid to which this token is granted access.
:param str auth_token: The secret key used to sign the token. Note, this auth token is not
visible to the user of the token.
:param int nbf: Time in secs from epic before which this token is considered invalid.
:param int ttl: the amount of time in seconds from generation that this token is valid for.
:param kwargs:
:returns: A new CapabilityToken with zero permissions
"""
super(ClientCapabilityToken, self).__init__(
algorithm=self.ALGORITHM,
secret_key=auth_token,
issuer=account_sid,
nbf=nbf,
ttl=ttl,
valid_until=None,
)
self.account_sid = account_sid
self.auth_token = auth_token
self.client_name = None
self.capabilities = {}
if "allow_client_outgoing" in kwargs:
self.allow_client_outgoing(**kwargs["allow_client_outgoing"])
if "allow_client_incoming" in kwargs:
self.allow_client_incoming(**kwargs["allow_client_incoming"])
if "allow_event_stream" in kwargs:
self.allow_event_stream(**kwargs["allow_event_stream"])
def allow_client_outgoing(self, application_sid, **kwargs):
"""
Allow the user of this token to make outgoing connections. Keyword arguments are passed
to the application.
:param str application_sid: Application to contact
"""
scope = ScopeURI("client", "outgoing", {"appSid": application_sid})
if kwargs:
scope.add_param("appParams", urlencode(kwargs, doseq=True))
self.capabilities["outgoing"] = scope
def allow_client_incoming(self, client_name):
"""
Allow the user of this token to accept incoming connections.
:param str client_name: Client name to accept calls from
"""
self.client_name = client_name
self.capabilities["incoming"] = ScopeURI(
"client", "incoming", {"clientName": client_name}
)
def allow_event_stream(self, **kwargs):
"""
Allow the user of this token to access their event stream.
"""
scope = ScopeURI("stream", "subscribe", {"path": "/2010-04-01/Events"})
if kwargs:
scope.add_param("params", urlencode(kwargs, doseq=True))
self.capabilities["events"] = scope
def _generate_payload(self):
if "outgoing" in self.capabilities and self.client_name is not None:
self.capabilities["outgoing"].add_param("clientName", self.client_name)
scope_uris = [
scope_uri.to_payload() for scope_uri in self.capabilities.values()
]
return {"scope": " ".join(scope_uris)}
class ScopeURI(object):
"""A single capability granted to Twilio Client and scoped to a service"""
def __init__(self, service, privilege, params=None):
self.service = service
self.privilege = privilege
self.params = params or {}
def add_param(self, key, value):
self.params[key] = value
def to_payload(self):
if self.params:
sorted_params = sorted([(k, v) for k, v in self.params.items()])
encoded_params = urlencode(sorted_params)
param_string = "?{}".format(encoded_params)
else:
param_string = ""
return "scope:{}:{}{}".format(self.service, self.privilege, param_string)
def __str__(self):
return "<ScopeURI {}>".format(self.to_payload())
@@ -0,0 +1,142 @@
from twilio.jwt import Jwt
class TaskRouterCapabilityToken(Jwt):
VERSION = "v1"
DOMAIN = "https://taskrouter.twilio.com"
EVENTS_BASE_URL = "https://event-bridge.twilio.com/v1/wschannels"
ALGORITHM = "HS256"
def __init__(self, account_sid, auth_token, workspace_sid, channel_id, **kwargs):
"""
:param str account_sid: Twilio account sid
:param str auth_token: Twilio auth token used to sign the JWT
:param str workspace_sid: TaskRouter workspace sid
:param str channel_id: TaskRouter channel sid
:param kwargs:
:param bool allow_web_sockets: shortcut to calling allow_web_sockets, defaults to True
:param bool allow_fetch_self: shortcut to calling allow_fetch_self, defaults to True
:param bool allow_update_self: shortcut to calling allow_update_self, defaults to False
:param bool allow_delete_self: shortcut to calling allow_delete_self, defaults to False
:param bool allow_fetch_subresources: shortcut to calling allow_fetch_subresources,
defaults to False
:param bool allow_update_subresources: shortcut to calling allow_update_subresources,
defaults to False
:param bool allow_delete_subresources: shortcut to calling allow_delete_subresources,
defaults to False
:returns a new TaskRouterCapabilityToken with capabilities set depending on kwargs.
"""
super(TaskRouterCapabilityToken, self).__init__(
secret_key=auth_token,
issuer=account_sid,
algorithm=self.ALGORITHM,
nbf=kwargs.get("nbf", Jwt.GENERATE),
ttl=kwargs.get("ttl", 3600),
valid_until=kwargs.get("valid_until", None),
)
self._validate_inputs(account_sid, workspace_sid, channel_id)
self.account_sid = account_sid
self.auth_token = auth_token
self.workspace_sid = workspace_sid
self.channel_id = channel_id
self.policies = []
if kwargs.get("allow_web_sockets", True):
self.allow_web_sockets()
if kwargs.get("allow_fetch_self", True):
self.allow_fetch_self()
if kwargs.get("allow_update_self", False):
self.allow_update_self()
if kwargs.get("allow_delete_self", False):
self.allow_delete_self()
if kwargs.get("allow_fetch_subresources", False):
self.allow_fetch_subresources()
if kwargs.get("allow_delete_subresources", False):
self.allow_delete_subresources()
if kwargs.get("allow_update_subresources", False):
self.allow_update_subresources()
@property
def workspace_url(self):
return "{}/{}/Workspaces/{}".format(
self.DOMAIN, self.VERSION, self.workspace_sid
)
@property
def resource_url(self):
raise NotImplementedError("Subclass must set its specific resource_url.")
@property
def channel_prefix(self):
raise NotImplementedError(
"Subclass must set its specific channel_id sid prefix."
)
def allow_fetch_self(self):
self._make_policy(self.resource_url, "GET", True)
def allow_update_self(self):
self._make_policy(self.resource_url, "POST", True)
def allow_delete_self(self):
self._make_policy(self.resource_url, "DELETE", True)
def allow_fetch_subresources(self):
self._make_policy(self.resource_url + "/**", "GET", True)
def allow_update_subresources(self):
self._make_policy(self.resource_url + "/**", "POST", True)
def allow_delete_subresources(self):
self._make_policy(self.resource_url + "/**", "DELETE", True)
def allow_web_sockets(self, channel_id=None):
channel_id = channel_id or self.channel_id
web_socket_url = "{}/{}/{}".format(
self.EVENTS_BASE_URL, self.account_sid, channel_id
)
self._make_policy(web_socket_url, "GET", True)
self._make_policy(web_socket_url, "POST", True)
def _generate_payload(self):
payload = {
"account_sid": self.account_sid,
"workspace_sid": self.workspace_sid,
"channel": self.channel_id,
"version": self.VERSION,
"friendly_name": self.channel_id,
"policies": self.policies,
}
if self.channel_id.startswith("WK"):
payload["worker_sid"] = self.channel_id
elif self.channel_id.startswith("WQ"):
payload["taskqueue_sid"] = self.channel_id
return payload
def _make_policy(self, url, method, allowed, query_filter=None, post_filter=None):
self.policies.append(
{
"url": url,
"method": method.upper(),
"allow": allowed,
"query_filter": query_filter or {},
"post_filter": post_filter or {},
}
)
def _validate_inputs(self, account_sid, workspace_sid, channel_id):
if not account_sid or not account_sid.startswith("AC"):
raise ValueError("Invalid account sid provided {}".format(account_sid))
if not workspace_sid or not workspace_sid.startswith("WS"):
raise ValueError("Invalid workspace sid provided {}".format(workspace_sid))
if not channel_id or not channel_id.startswith(self.channel_prefix):
raise ValueError("Invalid channel id provided {}".format(channel_id))
def __str__(self):
return "<TaskRouterCapabilityToken {}>".format(self.to_jwt())
@@ -0,0 +1,116 @@
from twilio.jwt.taskrouter import TaskRouterCapabilityToken
class WorkerCapabilityToken(TaskRouterCapabilityToken):
def __init__(
self, account_sid, auth_token, workspace_sid, worker_sid, ttl=3600, **kwargs
):
"""
:param kwargs:
All kwarg parameters supported by TaskRouterCapabilityToken
:param bool allow_fetch_activities: shortcut to calling allow_fetch_activities,
defaults to True
:param bool allow_fetch_reservations: shortcut to calling allow_fetch_reservations,
defaults to True
:param bool allow_fetch_worker_reservations: shortcut to calling allow_fetch_worker_reservations,
defaults to True
:param bool allow_update_activities: shortcut to calling allow_update_activities,
defaults to False
:param bool allow_update_reservations: shortcut to calling allow_update_reservations,
defaults to False
"""
super(WorkerCapabilityToken, self).__init__(
account_sid=account_sid,
auth_token=auth_token,
workspace_sid=workspace_sid,
channel_id=worker_sid,
ttl=ttl,
**kwargs
)
if kwargs.get("allow_fetch_activities", True):
self.allow_fetch_activities()
if kwargs.get("allow_fetch_reservations", True):
self.allow_fetch_reservations()
if kwargs.get("allow_fetch_worker_reservations", True):
self.allow_fetch_worker_reservations()
if kwargs.get("allow_update_activities", False):
self.allow_update_activities()
if kwargs.get("allow_update_reservations", False):
self.allow_update_reservations()
@property
def resource_url(self):
return "{}/Workers/{}".format(self.workspace_url, self.channel_id)
@property
def channel_prefix(self):
return "WK"
def allow_fetch_activities(self):
self._make_policy(self.workspace_url + "/Activities", "GET", True)
def allow_fetch_reservations(self):
self._make_policy(self.workspace_url + "/Tasks/**", "GET", True)
def allow_fetch_worker_reservations(self):
self._make_policy(self.resource_url + "/Reservations/**", "GET", True)
def allow_update_activities(self):
post_filter = {"ActivitySid": {"required": True}}
self._make_policy(self.resource_url, "POST", True, post_filter=post_filter)
def allow_update_reservations(self):
self._make_policy(self.workspace_url + "/Tasks/**", "POST", True)
self._make_policy(self.resource_url + "/Reservations/**", "POST", True)
def __str__(self):
return "<WorkerCapabilityToken {}>".format(self.to_jwt())
class TaskQueueCapabilityToken(TaskRouterCapabilityToken):
def __init__(
self, account_sid, auth_token, workspace_sid, task_queue_sid, ttl=3600, **kwargs
):
super(TaskQueueCapabilityToken, self).__init__(
account_sid=account_sid,
auth_token=auth_token,
workspace_sid=workspace_sid,
channel_id=task_queue_sid,
ttl=ttl,
**kwargs
)
@property
def resource_url(self):
return "{}/TaskQueues/{}".format(self.workspace_url, self.channel_id)
@property
def channel_prefix(self):
return "WQ"
def __str__(self):
return "<TaskQueueCapabilityToken {}>".format(self.to_jwt())
class WorkspaceCapabilityToken(TaskRouterCapabilityToken):
def __init__(self, account_sid, auth_token, workspace_sid, ttl=3600, **kwargs):
super(WorkspaceCapabilityToken, self).__init__(
account_sid=account_sid,
auth_token=auth_token,
workspace_sid=workspace_sid,
channel_id=workspace_sid,
ttl=ttl,
**kwargs
)
@property
def resource_url(self):
return self.workspace_url
@property
def channel_prefix(self):
return "WS"
def __str__(self):
return "<WorkspaceCapabilityToken {}>".format(self.to_jwt())
@@ -0,0 +1,92 @@
from hashlib import sha256
from twilio.jwt import Jwt
class ClientValidationJwt(Jwt):
"""A JWT included on requests so that Twilio can verify request authenticity"""
__CTY = "twilio-pkrv;v=1"
ALGORITHM = "RS256"
def __init__(
self, account_sid, api_key_sid, credential_sid, private_key, validation_payload
):
"""
Create a new ClientValidationJwt
:param str account_sid: A Twilio Account Sid starting with 'AC'
:param str api_key_sid: A Twilio API Key Sid starting with 'SK'
:param str credential_sid: A Credential Sid starting with 'CR',
public key Twilio will use to verify the JWT.
:param str private_key: The private key used to sign the JWT.
:param ValidationPayload validation_payload: information from the request to sign
"""
super(ClientValidationJwt, self).__init__(
secret_key=private_key,
issuer=api_key_sid,
subject=account_sid,
algorithm=self.ALGORITHM,
ttl=300, # 5 minute ttl
)
self.credential_sid = credential_sid
self.validation_payload = validation_payload
def _generate_headers(self):
return {"cty": ClientValidationJwt.__CTY, "kid": self.credential_sid}
def _generate_payload(self):
# Lowercase header keys, combine and sort headers with list values
all_headers = {
k.lower(): self._sort_and_join(v, ",")
for k, v in self.validation_payload.all_headers.items()
}
# Names of headers we are signing in the jwt
signed_headers = sorted(self.validation_payload.signed_headers)
# Stringify headers, only include headers in signed_headers
headers_str = [
"{}:{}".format(h, all_headers[h])
for h in signed_headers
if h in all_headers
]
headers_str = "\n".join(headers_str)
# Sort query string parameters
query_string = self.validation_payload.query_string.split("&")
query_string = self._sort_and_join(query_string, "&")
req_body_hash = self._hash(self.validation_payload.body) or ""
signed_headers_str = ";".join(signed_headers)
signed_payload = [
self.validation_payload.method,
self.validation_payload.path,
query_string,
]
if headers_str:
signed_payload.append(headers_str)
signed_payload.append("")
signed_payload.append(signed_headers_str)
signed_payload.append(req_body_hash)
signed_payload = "\n".join(signed_payload)
return {"hrh": signed_headers_str, "rqh": self._hash(signed_payload)}
@classmethod
def _sort_and_join(cls, values, joiner):
if isinstance(values, str):
return values
return joiner.join(sorted(values))
@classmethod
def _hash(cls, input_str):
if not input_str:
return input_str
if not isinstance(input_str, bytes):
input_str = input_str.encode("utf-8")
return sha256(input_str).hexdigest()