Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VerificationAPI #13

Merged
merged 49 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8646f81
feat(VerificationAPI): WiP
650elx Dec 6, 2023
31b75d9
feat(verification): WiP
650elx Dec 11, 2023
a9ee46c
feat(Verification): WiP
650elx Dec 19, 2023
5440396
feat(VerificationAPI): domain facade updated
650elx Dec 20, 2023
4ddccb6
feat(Verification): adapt exception structure
650elx Dec 20, 2023
f9fd637
feat(Verification): WiP
650elx Dec 20, 2023
1aa3683
feat(Signature): WiP
650elx Dec 20, 2023
b465ea4
feat(Verification): GET requests WiP
650elx Dec 21, 2023
53c9c07
feat(Verification): GET methods WiP
650elx Dec 21, 2023
d05360f
feat(Verification): GET methods finished
650elx Dec 21, 2023
e72af0e
feat(Verification): WiP
650elx Dec 22, 2023
0b38e27
feat(VerificationAPI): remaining API endpoints, models and tests added
650elx Jan 2, 2024
3789224
feat(Verification): validation exception added
650elx Jan 2, 2024
38cc672
refactor(Client): Client classes adapted
650elx Jan 2, 2024
0bfb679
feat(Verification): async tests added
650elx Jan 2, 2024
2f79350
feat(Verification): token refresh tests adopted
650elx Jan 2, 2024
fa5937a
Merge branch 'main' into DEVEXP-205-186-verification-API
650elx Jan 2, 2024
b074fc0
fix(pep8)
650elx Jan 2, 2024
7b5a205
CI:(Verification): map GH secrets
650elx Jan 3, 2024
67be035
ci(Verification): map GH secret
650elx Jan 3, 2024
5077a00
test(Verification): async client fixutre fixed
650elx Jan 3, 2024
f881811
fix(Endpoint): remove redundat method
650elx Jan 3, 2024
90fdaa7
refactor(Verification): ReportVerification model extracted
650elx Jan 5, 2024
fe2c438
Support for Python 3.8 removed, 3.12 added
650elx Jan 9, 2024
0aa0435
Revert "Support for Python 3.8 removed, 3.12 added"
650elx Jan 9, 2024
6e06e40
fix(Verification): unified method names added
650elx Jan 12, 2024
3bd60ba
refactor(Verification): main facade
650elx Jan 15, 2024
0ecbd2d
test(Verification): tests adapted
650elx Jan 15, 2024
143e4f1
fix(Verification): VerificationMethod enum added
650elx Jan 18, 2024
4c2df89
test(Verification): signature tests improved
650elx Jan 18, 2024
3317bf0
fix(Signature): naming fixed
650elx Jan 18, 2024
d52e838
fix(Singature): verification timestamp added to the constructor
650elx Jan 18, 2024
3c2a9cc
feat(Sinature): content type added to the init
650elx Jan 18, 2024
2de175f
fix(Verification): better naming
650elx Jan 18, 2024
a1fb0ca
fix(Verification): enum for method added
650elx Jan 18, 2024
86174a8
test(Verification): redunant lines removed
650elx Jan 18, 2024
edc92d4
feat(CI): Verification API specific env vars added
650elx Jan 19, 2024
6bbb1c3
Merge branch 'main' into DEVEXP-205-186-verification-API
650elx Jan 19, 2024
bd32d50
test(Logging): caplog usage fixed
650elx Jan 19, 2024
2a80584
test(Logging): simpfly caplog usage
650elx Jan 19, 2024
79da667
test(Logging): filter for only relevant log records
650elx Jan 19, 2024
3b1c765
feat(Verfication): DTO improved
650elx Jan 22, 2024
0ac9c0a
fix(Verification): simplyfy model usage
650elx Jan 22, 2024
97d53f2
feat(Verification): Start verification splitted into smaller DTOs
650elx Jan 22, 2024
f444e84
fix(Verification): identity type changed
650elx Jan 22, 2024
53691e1
fix(Verification): flashcall naming unified
650elx Jan 26, 2024
cc6a49e
fix(Verification): redundant transformation removed
650elx Jan 26, 2024
13ce08f
test(Verification): more specific DTOs assertions
650elx Jan 26, 2024
2d7a61e
fix(style): remove unused import
650elx Jan 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ env:
APP_ID: ${{ secrets.APP_ID }}
EMPTY_PROJECT_ID: ${{ secrets.EMPTY_PROJECT_ID }}
TEMPLATES_ORIGIN: ${{ secrets.TEMPLATES_ORIGIN }}

APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }}
APPLICATION_KEY: ${{ secrets.APPLICATION_KEY }}
VERIFICATION_ID: ${{ secrets.VERIFICATION_ID }}
VERIFICATION_ORIGIN: ${{ secrets.VERIFICATION_ORIGIN}}
VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP}}
VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE}}
VERIFICATION_REQUEST_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE}}

jobs:
build:
Expand Down
18 changes: 9 additions & 9 deletions sinch/core/clients/sinch_client_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sinch.domains.numbers import NumbersAsync
from sinch.domains.conversation import ConversationAsync
from sinch.domains.sms import SMSAsync
from sinch.domains.verification import Verification as VerificationAsync


class ClientAsync(ClientBase):
Expand All @@ -20,25 +21,24 @@ def __init__(
key_secret,
project_id,
logger_name=None,
logger=None
logger=None,
application_key: str = None,
application_secret: str = None
):
super().__init__(
key_id=key_id,
key_secret=key_secret,
project_id=project_id,
logger_name=logger_name,
logger=logger
)
self.configuration = Configuration(
key_id=key_id,
key_secret=key_secret,
project_id=project_id,
logger_name=logger_name,
logger=logger,
transport=HTTPTransportAioHTTP(self),
token_manager=TokenManagerAsync(self)
token_manager=TokenManagerAsync(self),
application_secret=application_secret,
application_key=application_key
)

self.authentication = AuthenticationAsync(self)
self.numbers = NumbersAsync(self)
self.conversation = ConversationAsync(self)
self.sms = SMSAsync(self)
self.verification = VerificationAsync(self)
32 changes: 13 additions & 19 deletions sinch/core/clients/sinch_client_base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from abc import ABC
from sinch.core.exceptions import ValidationException
from abc import ABC, abstractmethod
from sinch.core.clients.sinch_client_configuration import Configuration
from sinch.domains.authentication import AuthenticationBase
from sinch.domains.numbers import NumbersBase
Expand All @@ -10,32 +9,27 @@
class ClientBase(ABC):
"""
Sinch abstract base class for concrete Sinch Client implementations.
By default this SDK provides two implementations - sync and async.
By default, this SDK provides two implementations - sync and async.
Feel free to utilize any of them for you custom implementation.
"""
configuration = Configuration
authentication = AuthenticationBase
numbers = NumbersBase
conversation = ConversationBase
sms = SMSBase

asein-sinch marked this conversation as resolved.
Show resolved Hide resolved
@abstractmethod
def __init__(
self,
key_id,
key_secret,
project_id,
logger_name=None,
logger=None
logger=None,
application_key: str = None,
application_secret: str = None
):
if not key_id or not key_secret or not project_id:
raise ValidationException(
message=(
"key_id, key_secret and project_id are required by the Sinch Client. "
"Those credentials can be obtained from Sinch portal."
),
is_from_server=False,
response=None
)

self.configuration = Configuration
self.authentication = AuthenticationBase
self.numbers = NumbersBase
self.conversation = ConversationBase
self.sms = SMSBase
pass

def __repr__(self):
return f"Sinch SDK client for project_id: {self.configuration.project_id}"
7 changes: 6 additions & 1 deletion sinch/core/clients/sinch_client_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ def __init__(
logger=None,
logger_name: str = None,
disable_https=False,
connection_timeout=10
connection_timeout=10,
application_key: str = None,
application_secret: str = None
):
self.key_id = key_id
self.key_secret = key_secret
self.project_id = project_id
self.application_key = application_key
self.application_secret = application_secret
self.connection_timeout = connection_timeout
self.auth_origin = "auth.sinch.com"
self.numbers_origin = "numbers.api.sinch.com"
self.verification_origin = "verification.api.sinch.com"
self._conversation_region = "eu"
self._conversation_domain = ".conversation.api.sinch.com"
self._sms_region = "us"
Expand Down
18 changes: 9 additions & 9 deletions sinch/core/clients/sinch_client_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sinch.domains.numbers import Numbers
from sinch.domains.conversation import Conversation
from sinch.domains.sms import SMS
from sinch.domains.verification import Verification


class Client(ClientBase):
Expand All @@ -20,25 +21,24 @@ def __init__(
key_secret,
project_id,
logger_name=None,
logger=None
logger=None,
application_key: str = None,
application_secret: str = None
):
super().__init__(
key_id=key_id,
key_secret=key_secret,
project_id=project_id,
logger_name=logger_name,
logger=logger
)
self.configuration = Configuration(
key_id=key_id,
key_secret=key_secret,
project_id=project_id,
logger_name=logger_name,
logger=logger,
transport=HTTPTransportRequests(self),
token_manager=TokenManager(self)
token_manager=TokenManager(self),
application_key=application_key,
application_secret=application_secret
)

self.authentication = Authentication(self)
self.numbers = Numbers(self)
self.conversation = Conversation(self)
self.sms = SMS(self)
self.verification = Verification(self)
3 changes: 3 additions & 0 deletions sinch/core/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class HTTPEndpoint(ABC):
def __init__(self, project_id, request_data):
pass

def get_url_without_origin(self, sinch):
return '/' + '/'.join(self.build_url(sinch).split('/')[1:])

def build_url(self, sinch):
return

Expand Down
1 change: 1 addition & 0 deletions sinch/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ class HTTPMethods(Enum):
class HTTPAuthentication(Enum):
BASIC = "BASIC"
OAUTH = "OAUTH"
SIGNED = "SIGNED"
38 changes: 36 additions & 2 deletions sinch/core/ports/http_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from abc import ABC, abstractmethod
from platform import python_version
from sinch.core.endpoint import HTTPEndpoint
from sinch.core.signature import Signature
from sinch.core.models.http_request import HttpRequest
from sinch.core.models.http_response import HTTPResponse
from sinch.core.exceptions import ValidationException
from sinch.core.enums import HTTPAuthentication
from sinch.core.token_manager import TokenState
from sinch import __version__ as sdk_version
Expand All @@ -18,6 +20,21 @@ def request(self, endpoint: HTTPEndpoint) -> HTTPResponse:
pass

def authenticate(self, endpoint, request_data):
if endpoint.HTTP_AUTHENTICATION in (HTTPAuthentication.BASIC.value, HTTPAuthentication.OAUTH.value):
JPPortier marked this conversation as resolved.
Show resolved Hide resolved
if (
not self.sinch.configuration.key_id
or not self.sinch.configuration.key_secret
or not self.sinch.configuration.project_id
):
raise ValidationException(
message=(
"key_id, key_secret and project_id are required by this API. "
"Those credentials can be obtained from Sinch portal."
),
is_from_server=False,
response=None
)

if endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.BASIC.value:
request_data.auth = (self.sinch.configuration.key_id, self.sinch.configuration.key_secret)
else:
Expand All @@ -29,6 +46,23 @@ def authenticate(self, endpoint, request_data):
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
})
elif endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.SIGNED.value:
JPPortier marked this conversation as resolved.
Show resolved Hide resolved
if not self.sinch.configuration.application_key or not self.sinch.configuration.application_secret:
raise ValidationException(
message=(
"application key and application secret are required by this API. "
"Those credentials can be obtained from Sinch portal."
),
is_from_server=False,
response=None
)
signature = Signature(
self.sinch,
endpoint.HTTP_METHOD,
request_data.request_body,
endpoint.get_url_without_origin(self.sinch)
)
request_data.headers = signature.get_http_headers_with_signature()

return request_data

Expand All @@ -50,7 +84,7 @@ def prepare_request(self, endpoint: HTTPEndpoint) -> HttpRequest:
)

def handle_response(self, endpoint: HTTPEndpoint, http_response: HTTPResponse):
if http_response.status_code == 401:
if http_response.status_code == 401 and endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.OAUTH.value:
self.sinch.configuration.token_manager.handle_invalid_token(http_response)
if self.sinch.configuration.token_manager.token_state == TokenState.EXPIRED:
return self.request(endpoint=endpoint)
Expand Down Expand Up @@ -78,7 +112,7 @@ async def authenticate(self, endpoint, request_data):
return request_data

async def handle_response(self, endpoint: HTTPEndpoint, http_response: HTTPResponse):
if http_response.status_code == 401:
if http_response.status_code == 401 and endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.OAUTH.value:
self.sinch.configuration.token_manager.handle_invalid_token(http_response)
if self.sinch.configuration.token_manager.token_state == TokenState.EXPIRED:
return await self.request(endpoint=endpoint)
Expand Down
58 changes: 58 additions & 0 deletions sinch/core/signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import hashlib
import hmac
import base64
from datetime import datetime, timezone


class Signature:
def __init__(
self,
sinch,
http_method,
request_data,
request_uri,
content_type=None,
signature_timestamp=None
):
self.sinch = sinch
self.http_method = http_method
self.content_type = content_type or 'application/json; charset=UTF-8'
self.request_data = request_data
self.signature_timestamp = signature_timestamp or datetime.now(timezone.utc).isoformat()
self.request_uri = request_uri
self.authorization_signature = None

def get_http_headers_with_signature(self):
if not self.authorization_signature:
self.calculate()

return {
"Content-Type": self.content_type,
"Authorization": (
f"Application {self.sinch.configuration.application_key}:{self.authorization_signature}"
),
"x-timestamp": self.signature_timestamp
}

def calculate(self):
b64_decoded_application_secret = base64.b64decode(self.sinch.configuration.application_secret)
if self.request_data:
encoded_verification_request = hashlib.md5(self.request_data.encode())
encoded_verification_request = base64.b64encode(encoded_verification_request.digest())

else:
encoded_verification_request = ''.encode()

request_timestamp = "x-timestamp:" + self.signature_timestamp

string_to_sign = (
self.http_method + '\n'
+ encoded_verification_request.decode() + '\n'
+ self.content_type + '\n'
+ request_timestamp + '\n'
+ self.request_uri
)

self.authorization_signature = base64.b64encode(
hmac.new(b64_decoded_application_secret, string_to_sign.encode(), hashlib.sha256).digest()
).decode()
Loading
Loading