Initial commit: Email alerts application
This commit is contained in:
@@ -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())
|
||||
Binary file not shown.
@@ -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())
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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())
|
||||
BIN
Binary file not shown.
@@ -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())
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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()
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user