diff --git a/twilio/base/client_base.py b/twilio/base/client_base.py index 5f17c75406..700e420ad8 100644 --- a/twilio/base/client_base.py +++ b/twilio/base/client_base.py @@ -69,6 +69,8 @@ def request( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, + domain: Optional[str] = None ) -> Response: """ Makes a request to the Twilio API using the configured http client @@ -85,9 +87,15 @@ def request( :returns: Response from the Twilio API """ - auth = self.get_auth(auth) + if not is_oauth: + auth = self.get_auth(auth) headers = self.get_headers(method, headers) uri = self.get_hostname(uri) + if is_oauth: + OauthTokenBase = dynamic_import("twilio.base.oauth_token_base", "OauthTokenBase") + token = OauthTokenBase().get_oauth_token(domain, "v1", self.username, self.password) + headers['Authorization'] = f'Bearer {token}' + headers.get('Authorization') return self.http_client.request( method, @@ -110,6 +118,7 @@ async def request_async( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Response: """ Asynchronously makes a request to the Twilio API using the configured http client @@ -131,10 +140,15 @@ async def request_async( raise RuntimeError( "http_client must be asynchronous to support async API requests" ) - - auth = self.get_auth(auth) + if not is_oauth: + auth = self.get_auth(auth) headers = self.get_headers(method, headers) uri = self.get_hostname(uri) + if is_oauth: + OauthTokenBase = dynamic_import("twilio.base.oauth_token_base", "OauthTokenBase") + token = OauthTokenBase().get_oauth_token(domain, "v1", self.username, self.password) + headers['Authorization'] = f'Bearer {token}' + headers.get('Authorization') return await self.http_client.request( method, @@ -232,3 +246,8 @@ def __repr__(self) -> str: :returns: Machine friendly representation """ return "".format(self.account_sid) + +def dynamic_import(module_name, class_name): + from importlib import import_module + module = import_module(module_name) + return getattr(module, class_name) diff --git a/twilio/base/domain.py b/twilio/base/domain.py index 4f8395ddf2..02f4986858 100644 --- a/twilio/base/domain.py +++ b/twilio/base/domain.py @@ -32,6 +32,7 @@ def request( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Response: """ Makes an HTTP request to this domain. @@ -55,6 +56,8 @@ def request( auth=auth, timeout=timeout, allow_redirects=allow_redirects, + is_oauth=is_oauth, + domain=self.base_url, ) async def request_async( @@ -67,6 +70,7 @@ async def request_async( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Response: """ Makes an asynchronous HTTP request to this domain. @@ -90,4 +94,5 @@ async def request_async( auth=auth, timeout=timeout, allow_redirects=allow_redirects, + is_oauth=is_oauth ) diff --git a/twilio/base/oauth_token_base.py b/twilio/base/oauth_token_base.py new file mode 100644 index 0000000000..91e6d5191f --- /dev/null +++ b/twilio/base/oauth_token_base.py @@ -0,0 +1,24 @@ +from twilio.http.token_manager_initializer import TokenManagerInitializer + +# Dynamic import utility function +def dynamic_import(module_name, class_name): + from importlib import import_module + module = import_module(module_name) + return getattr(module, class_name) + +class OauthTokenBase: + def get_oauth_token(self, domain: str, version: str, username: str, password: str): + Domain = dynamic_import("twilio.base.domain", "Domain") + Version = dynamic_import("twilio.base.version", "Version") + BearerTokenHTTPClient = dynamic_import("twilio.http.bearer_token_http_client", "BearerTokenHTTPClient") + OrgTokenManager = dynamic_import("twilio.http.orgs_token_manager", "OrgTokenManager") + Client = dynamic_import("twilio.rest", "Client") + try: + orgs_token_manager = TokenManagerInitializer.get_token_manager() + return BearerTokenHTTPClient(orgs_token_manager).get_access_token(Version(Domain(Client(username, password), domain), version)) + except Exception: + orgs_token_manager = OrgTokenManager(grant_type='client_credentials', + client_id=username, + client_secret=password) + TokenManagerInitializer().set_token_manager(orgs_token_manager) + return BearerTokenHTTPClient(orgs_token_manager).get_access_token(Version(Domain(Client(username, password), domain), version)) \ No newline at end of file diff --git a/twilio/base/version.py b/twilio/base/version.py index 64cc601faa..e7ddaff6c9 100644 --- a/twilio/base/version.py +++ b/twilio/base/version.py @@ -39,6 +39,7 @@ def request( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Response: """ Make an HTTP request. @@ -53,6 +54,7 @@ def request( auth=auth, timeout=timeout, allow_redirects=allow_redirects, + is_oauth=is_oauth ) async def request_async( @@ -65,6 +67,7 @@ async def request_async( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Response: """ Make an asynchronous HTTP request @@ -79,6 +82,7 @@ async def request_async( auth=auth, timeout=timeout, allow_redirects=allow_redirects, + is_oauth=is_oauth ) @classmethod @@ -123,6 +127,7 @@ def fetch( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Any: """ Fetch a resource instance. @@ -136,6 +141,7 @@ def fetch( auth=auth, timeout=timeout, allow_redirects=allow_redirects, + is_oauth=is_oauth ) return self._parse_fetch(method, uri, response) @@ -150,6 +156,7 @@ async def fetch_async( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Any: """ Asynchronously fetch a resource instance. @@ -163,6 +170,7 @@ async def fetch_async( auth=auth, timeout=timeout, allow_redirects=allow_redirects, + is_oauth=is_oauth ) return self._parse_fetch(method, uri, response) @@ -186,6 +194,7 @@ def update( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Any: """ Update a resource instance. @@ -213,6 +222,7 @@ async def update_async( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Any: """ Asynchronously update a resource instance. @@ -226,6 +236,7 @@ async def update_async( auth=auth, timeout=timeout, allow_redirects=allow_redirects, + is_oauth=is_oauth ) return self._parse_update(method, uri, response) @@ -249,6 +260,7 @@ def delete( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> bool: """ Delete a resource. @@ -276,6 +288,7 @@ async def delete_async( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> bool: """ Asynchronously delete a resource. @@ -289,6 +302,7 @@ async def delete_async( auth=auth, timeout=timeout, allow_redirects=allow_redirects, + is_oauth=is_oauth ) return self._parse_delete(method, uri, response) @@ -347,6 +361,7 @@ async def page_async( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Response: """ Makes an asynchronous HTTP request. @@ -360,6 +375,7 @@ async def page_async( auth=auth, timeout=timeout, allow_redirects=allow_redirects, + is_oauth=is_oauth ) def stream( @@ -447,6 +463,7 @@ def create( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Any: """ Create a resource instance. @@ -474,6 +491,7 @@ async def create_async( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Any: """ Asynchronously create a resource instance. @@ -487,6 +505,7 @@ async def create_async( auth=auth, timeout=timeout, allow_redirects=allow_redirects, + is_oauth=is_oauth ) return self._parse_create(method, uri, response) diff --git a/twilio/http/bearer_token_http_client.py b/twilio/http/bearer_token_http_client.py new file mode 100644 index 0000000000..eb5758db98 --- /dev/null +++ b/twilio/http/bearer_token_http_client.py @@ -0,0 +1,30 @@ +import datetime +import jwt + +from twilio.base.version import Version +from twilio.http.token_manager import TokenManager +from twilio.twilio_bearer_token_auth import TwilioBearerTokenAuth + + +class BearerTokenHTTPClient: + def __init__(self, orgs_token_manager: TokenManager): + self.orgs_token_manager = orgs_token_manager + + def get_access_token(self, version: Version): + if TwilioBearerTokenAuth.get_access_token() is None or self.is_token_expired( + TwilioBearerTokenAuth.get_access_token() + ): + access_token = self.orgs_token_manager.fetch_access_token(version) + TwilioBearerTokenAuth.init(access_token) + else: + access_token = TwilioBearerTokenAuth.get_access_token() + + return access_token + + def is_token_expired(self, token): + decoded_jwt = jwt.decode(token, options={"verify_signature": True}) + expires_at = decoded_jwt.get("exp") + # Add a buffer of 30 seconds + buffer_seconds = 30 + buffer_expires_at = expires_at - buffer_seconds + return buffer_expires_at < datetime.datetime.now().timestamp() diff --git a/twilio/http/http_client.py b/twilio/http/http_client.py index 9d329c6aa7..c97c6e830d 100644 --- a/twilio/http/http_client.py +++ b/twilio/http/http_client.py @@ -57,6 +57,7 @@ def request( auth: Optional[Tuple[str, str]] = None, timeout: Optional[float] = None, allow_redirects: bool = False, + is_oauth: bool = False, ) -> Response: """ Make an HTTP Request with parameters provided. diff --git a/twilio/http/no_auth_http_client.py b/twilio/http/no_auth_http_client.py new file mode 100644 index 0000000000..daa5e5cdcc --- /dev/null +++ b/twilio/http/no_auth_http_client.py @@ -0,0 +1,4 @@ +class NoAuthHTTPClient: + def get_headers(self): + headers = {} + return headers diff --git a/twilio/http/orgs_token_manager.py b/twilio/http/orgs_token_manager.py new file mode 100644 index 0000000000..fbde521a3e --- /dev/null +++ b/twilio/http/orgs_token_manager.py @@ -0,0 +1,42 @@ +from twilio.base.version import Version +from twilio.http.token_manager import TokenManager +from twilio.rest.preview_iam.organizations.token import TokenList + + +class OrgTokenManager(TokenManager): + """ + Orgs Token Manager + """ + + def __init__( + self, + grant_type: str, + client_id: str, + client_secret: str, + code: str = None, + redirect_uri: str = None, + audience: str = None, + refreshToken: str = None, + scope: str = None, + ): + self.grant_type = grant_type + self.client_id = client_id + self.client_secret = client_secret + self.code = code + self.redirect_uri = redirect_uri + self.audience = audience + self.refreshToken = refreshToken + self.scope = scope + + def fetch_access_token(self, version: Version): + token_list = TokenList(version) + token_instance = token_list.create( + grant_type=self.grant_type, + client_id=self.client_id, + client_secret=self.client_secret, + code=self.code, + redirect_uri=self.redirect_uri, + audience=self.audience, + scope=self.scope, + ) + return token_instance.access_token diff --git a/twilio/http/token_manager.py b/twilio/http/token_manager.py new file mode 100644 index 0000000000..28cc731019 --- /dev/null +++ b/twilio/http/token_manager.py @@ -0,0 +1,7 @@ +from twilio.base.version import Version + + +class TokenManager: + + def fetch_access_token(self, version: Version): + pass diff --git a/twilio/http/token_manager_initializer.py b/twilio/http/token_manager_initializer.py new file mode 100644 index 0000000000..d4836d68a4 --- /dev/null +++ b/twilio/http/token_manager_initializer.py @@ -0,0 +1,16 @@ +from twilio.http.token_manager import TokenManager + + +class TokenManagerInitializer: + + org_token_manager = None + + @classmethod + def set_token_manager(cls, token_manager: TokenManager): + cls.org_token_manager = token_manager + + @classmethod + def get_token_manager(cls): + if cls.org_token_manager is None: + raise Exception('Token Manager not initialized') + return cls.org_token_manager \ No newline at end of file diff --git a/twilio/rest/__init__.py b/twilio/rest/__init__.py index 31d114a6c0..6da8da4a17 100644 --- a/twilio/rest/__init__.py +++ b/twilio/rest/__init__.py @@ -145,6 +145,7 @@ def __init__( self._numbers: Optional["Numbers"] = None self._oauth: Optional["Oauth"] = None self._preview: Optional["Preview"] = None + self._preview_iam: Optional["PreviewIam"] = None self._pricing: Optional["Pricing"] = None self._proxy: Optional["Proxy"] = None self._routes: Optional["Routes"] = None @@ -446,6 +447,19 @@ def preview(self) -> "Preview": self._preview = Preview(self) return self._preview + @property + def preview_iam(self) -> "PreviewIam": + """ + Access the Preview Twilio Domain + + :returns: Preview Twilio Domain + """ + if self._preview_iam is None: + from twilio.rest.preview_iam import PreviewIam + + self._preview = PreviewIam(self) + return self._preview_iam + @property def pricing(self) -> "Pricing": """ diff --git a/twilio/rest/preview_iam/PreviewIamBase.py b/twilio/rest/preview_iam/PreviewIamBase.py new file mode 100644 index 0000000000..9cf1f3bac3 --- /dev/null +++ b/twilio/rest/preview_iam/PreviewIamBase.py @@ -0,0 +1,29 @@ +r""" + This code was generated by + ___ _ _ _ _ _ _ ____ ____ ____ _ ____ ____ _ _ ____ ____ ____ ___ __ __ + | | | | | | | | | __ | | |__| | __ | __ |___ |\ | |___ |__/ |__| | | | |__/ + | |_|_| | |___ | |__| |__| | | | |__] |___ | \| |___ | \ | | | |__| | \ + + NOTE: This class is auto generated by OpenAPI Generator. + https://openapi-generator.tech + Do not edit the class manually. +""" +from twilio.base.domain import Domain +from twilio.rest import Client +from twilio.rest.preview_iam.organizations_openapi.token import Token +from twilio.rest.preview_iam.organizations_openapi.account import Account +from twilio.rest.preview_iam.organizations_openapi.authorize import AuthorizeList +from twilio.rest.preview_iam.organizations_openapi.resource_type import ResourceTypeList +from twilio.rest.preview_iam.organizations_openapi.role_assignment import RoleAssignmentList + +class PreviewIamBase(Domain): + def __init__(self, twilio: Client): + """ + Initialize the PreviewIam Domain + + :returns: Domain for PreviewIam + """ + super().__init__(twilio, "https://preview.twilio.com/iam") + self._token: Optional[TokenList] = None + self._service_accounts: Optional[ServiceAccounts] = None + self._service_roles: Optional[ServiceRoles] = None \ No newline at end of file diff --git a/twilio/rest/preview_iam/organizations/token.py b/twilio/rest/preview_iam/organizations/token.py new file mode 100644 index 0000000000..fc28287e8f --- /dev/null +++ b/twilio/rest/preview_iam/organizations/token.py @@ -0,0 +1,160 @@ +r""" + This code was generated by + ___ _ _ _ _ _ _ ____ ____ ____ _ ____ ____ _ _ ____ ____ ____ ___ __ __ + | | | | | | | | | __ | | |__| | __ | __ |___ |\ | |___ |__/ |__| | | | |__/ + | |_|_| | |___ | |__| |__| | | | |__] |___ | \| |___ | \ | | | |__| | \ + + Organization Public API + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + NOTE: This class is auto generated by OpenAPI Generator. + https://openapi-generator.tech + Do not edit the class manually. +""" + + +from datetime import date, datetime +from decimal import Decimal +from typing import Any, Dict, List, Optional, Union, Iterator, AsyncIterator +from twilio.base import deserialize, serialize, values + +from twilio.base.instance_resource import InstanceResource +from twilio.base.list_resource import ListResource +from twilio.base.version import Version + + + +class TokenInstance(InstanceResource): + + """ + :ivar access_token: Token which carries the necessary information to access a Twilio resource directly. + :ivar refresh_token: Token which carries the information necessary to get a new access token. + :ivar id_token: Token which carries the information necessary of user profile. + :ivar token_type: Token type + :ivar expires_in: + """ + + def __init__(self, version: Version, payload: Dict[str, Any]): + super().__init__(version) + + + self.access_token: Optional[str] = payload.get("access_token") + self.refresh_token: Optional[str] = payload.get("refresh_token") + self.id_token: Optional[str] = payload.get("id_token") + self.token_type: Optional[str] = payload.get("token_type") + self.expires_in: Optional[int] = payload.get("expires_in") + + + + + def __repr__(self) -> str: + """ + Provide a friendly representation + + :returns: Machine friendly representation + """ + + return '' + + + + +class TokenList(ListResource): + + def __init__(self, version: Version): + """ + Initialize the TokenList + + :param version: Version that contains the resource + + """ + super().__init__(version) + + + self._uri = '/token' + + + + def create(self, grant_type: str, client_id: str, client_secret: Union[str, object]=values.unset, code: Union[str, object]=values.unset, redirect_uri: Union[str, object]=values.unset, audience: Union[str, object]=values.unset, refresh_token: Union[str, object]=values.unset, scope: Union[str, object]=values.unset) -> TokenInstance: + """ + Create the TokenInstance + + :param grant_type: Grant type is a credential representing resource owner's authorization which can be used by client to obtain access token. + :param client_id: A 34 character string that uniquely identifies this OAuth App. + :param client_secret: The credential for confidential OAuth App. + :param code: JWT token related to the authorization code grant type. + :param redirect_uri: The redirect uri + :param audience: The targeted audience uri + :param refresh_token: JWT token related to refresh access token. + :param scope: The scope of token + + :returns: The created TokenInstance + """ + + data = values.of({ + 'grant_type': grant_type, + 'client_id': client_id, + 'client_secret': client_secret, + 'code': code, + 'redirect_uri': redirect_uri, + 'audience': audience, + 'refresh_token': refresh_token, + 'scope': scope, + }) + headers = values.of({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) + + + + payload = self._version.create(method='POST', uri=self._uri, data=data, headers=headers) + + return TokenInstance(self._version, payload) + + async def create_async(self, grant_type: str, client_id: str, client_secret: Union[str, object]=values.unset, code: Union[str, object]=values.unset, redirect_uri: Union[str, object]=values.unset, audience: Union[str, object]=values.unset, refresh_token: Union[str, object]=values.unset, scope: Union[str, object]=values.unset) -> TokenInstance: + """ + Asynchronously create the TokenInstance + + :param grant_type: Grant type is a credential representing resource owner's authorization which can be used by client to obtain access token. + :param client_id: A 34 character string that uniquely identifies this OAuth App. + :param client_secret: The credential for confidential OAuth App. + :param code: JWT token related to the authorization code grant type. + :param redirect_uri: The redirect uri + :param audience: The targeted audience uri + :param refresh_token: JWT token related to refresh access token. + :param scope: The scope of token + + :returns: The created TokenInstance + """ + + data = values.of({ + 'grant_type': grant_type, + 'client_id': client_id, + 'client_secret': client_secret, + 'code': code, + 'redirect_uri': redirect_uri, + 'audience': audience, + 'refresh_token': refresh_token, + 'scope': scope, + }) + headers = values.of({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) + + + + payload = await self._version.create_async(method='POST', uri=self._uri, data=data, headers=headers) + + return TokenInstance(self._version, payload) + + + + + def __repr__(self) -> str: + """ + Provide a friendly representation + + :returns: Machine friendly representation + """ + return '' + diff --git a/twilio/twilio_bearer_token_auth.py b/twilio/twilio_bearer_token_auth.py new file mode 100644 index 0000000000..ffc8814cdc --- /dev/null +++ b/twilio/twilio_bearer_token_auth.py @@ -0,0 +1,33 @@ +from threading import Lock + + +class BearerTokenTwilioRestClient: + pass + + +class TwilioBearerTokenAuth: + _lock = Lock() + access_token = None + rest_client = None + user_agent_extensions = None + region = None + edge = None + + @classmethod + def init(cls, access_token): + with cls._lock: + if not access_token: + raise ValueError("Access Token cannot be null or Empty") + if access_token != cls.access_token: + cls.access_token = None + cls.access_token = access_token + + @classmethod + def get_access_token(cls): + with cls._lock: + return cls.access_token + + @classmethod + def get_header_param(cls): + with cls._lock: + return {"Authorization": "Bearer {token}".format(token=cls.access_token)}