Skip to content

Commit

Permalink
feat(multiple-auth): improvements in multiple auth implementation (#48)
Browse files Browse the repository at this point in the history
This commit adds improvements in the implementation of multiple authentication schemes. The improvements include the addition of auth-validation exception type and early returning in case of OR authentication group.

closes #47
  • Loading branch information
sufyankhanrao authored Dec 8, 2023
1 parent b6a84b8 commit 9343356
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 53 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ pip install apimatic-core
|--------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`LazyProperty`](apimatic_core/decorators/lazy_property.py) | A decorator class for lazy instantiation |

## Exceptions
| Name | Description |
|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`OneOfValidationException`](apimatic_core/exceptions/oneof_validation_exception.py) | An exception class for the failed validation of oneOf (union-type) cases |
| [`AnyOfValidationException`](apimatic_core/exceptions/anyof_validation_exception.py) | An exception class for the failed validation of anyOf (union-type) cases |
| [`AuthValidationException`](apimatic_core/exceptions/auth_validation_exception.py) | An exception class for the failed validation of authentication schemes |

## Factories
| Name | Description |
|---------------------------------------------------------------------------|-----------------------------------------------------------------------------|
Expand Down
12 changes: 7 additions & 5 deletions apimatic_core/authentication/multiple/or_auth_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ def is_valid(self):
return False

for participant in self.mapped_group:
if participant.is_valid():
self._is_valid_group = True
else:
self.error_messages.append(participant.error_message)
self._is_valid_group = participant.is_valid()
# returning as we encounter the first participant as valid
if self._is_valid_group:
return self._is_valid_group

return self.is_valid_group
self.error_messages.append(participant.error_message)

return self._is_valid_group

6 changes: 3 additions & 3 deletions apimatic_core/authentication/multiple/single_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def __init__(self, auth_participant):
self._auth_participant = auth_participant
self._mapped_auth = None
self._error_message = None
self._is_valid = True
self._is_valid = False

def with_auth_managers(self, auth_managers):
if not auth_managers.get(self._auth_participant):
Expand All @@ -23,9 +23,9 @@ def with_auth_managers(self, auth_managers):
return self

def is_valid(self):
if not self._mapped_auth.is_valid():
self._is_valid = self._mapped_auth.is_valid()
if not self._is_valid:
self._error_message = self._mapped_auth.error_message
self._is_valid = False

return self._is_valid

Expand Down
3 changes: 2 additions & 1 deletion apimatic_core/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
__all__ = [
'oneof_validation_exception',
'anyof_validation_exception'
'anyof_validation_exception',
'auth_validation_exception'
]
5 changes: 5 additions & 0 deletions apimatic_core/exceptions/anyof_validation_exception.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
"""
This is an exception class which will be raised when union type validation fails.
"""


class AnyOfValidationException(Exception):

def __init__(self, message):
self.message = message
super().__init__(self.message)
10 changes: 10 additions & 0 deletions apimatic_core/exceptions/auth_validation_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
This is an exception class which will be raised when auth validation fails.
"""


class AuthValidationException(Exception):

def __init__(self, message):
self.message = message
super().__init__(self.message)
5 changes: 5 additions & 0 deletions apimatic_core/exceptions/oneof_validation_exception.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
"""
This is an exception class which will be raised when union type validation fails.
"""


class OneOfValidationException(Exception):

def __init__(self, message):
self.message = message
super().__init__(self.message)
3 changes: 2 additions & 1 deletion apimatic_core/request_builder.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from apimatic_core.exceptions.auth_validation_exception import AuthValidationException
from apimatic_core.http.request.http_request import HttpRequest
from apimatic_core.types.array_serialization_format import SerializationFormats
from apimatic_core.utilities.api_helper import ApiHelper
Expand Down Expand Up @@ -229,4 +230,4 @@ def apply_auth(self, auth_managers, http_request):
if self._auth.with_auth_managers(auth_managers).is_valid():
self._auth.apply(http_request)
else:
raise PermissionError(self._auth.error_message)
raise AuthValidationException(self._auth.error_message)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

setup(
name='apimatic-core',
version='0.2.5',
version='0.2.6',
description='A library that contains core logic and utilities for '
'consuming REST APIs using Python SDKs generated by APIMatic.',
long_description=long_description,
Expand Down
6 changes: 3 additions & 3 deletions tests/apimatic_core/api_logger_tests/test_api_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ def test_end_to_end_success_case(self):
'Calling the on_before_request method of http_call_back for end-to-end-test.',
'Raw request for end-to-end-test is: {\'http_method\': \'POST\', \'query_url\': '
'\'http://localhost:3000/body/model\', \'headers\': {\'Content-Type\': '
'\'application/json\', \'accept\': \'application/json\', \'Authorization\': '
'\'application/json\', \'accept\': \'application/json\', \'Basic-Authorization\': '
'\'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk\'}, \'query_parameters\': None, '
'\'parameters\': \'{"Key": "Value"}\', \'files\': {}}',
'Raw response for end-to-end-test is: {\'status_code\': 200, \'reason_phrase\': None, '
'\'headers\': {\'Content-Type\': \'application/json\', \'accept\': \'application/json\', '
'\'Authorization\': \'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk\'}, \'text\': '
'\'Basic-Authorization\': \'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk\'}, \'text\': '
'\'{"Key": "Value"}\', \'request\': {\'http_method\': \'POST\', \'query_url\': '
'\'http://localhost:3000/body/model\', \'headers\': {\'Content-Type\': '
'\'application/json\', \'accept\': \'application/json\', \'Authorization\': '
'\'application/json\', \'accept\': \'application/json\', \'Basic-Authorization\': '
'\'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk\'}, \'query_parameters\': None, '
'\'parameters\': \'{"Key": "Value"}\', \'files\': {}}}',
'Calling on_after_response method of http_call_back for end-to-end-test.',
Expand Down
2 changes: 1 addition & 1 deletion tests/apimatic_core/mocks/authentications/basic_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def error_message(self):
def __init__(self, basic_auth_user_name, basic_auth_password):
auth_params = {}
if basic_auth_user_name and basic_auth_password:
auth_params = {'Authorization': "Basic {}".format(
auth_params = {'Basic-Authorization': "Basic {}".format(
AuthHelper.get_base64_encoded_value(basic_auth_user_name, basic_auth_password))}
super().__init__(auth_params=auth_params)
self._basic_auth_user_name = basic_auth_user_name
Expand Down
2 changes: 1 addition & 1 deletion tests/apimatic_core/mocks/authentications/bearer_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ def error_message(self):
def __init__(self, access_token):
auth_params = {}
if access_token:
auth_params = {'Authorization': "Bearer {}".format(access_token)}
auth_params = {'Bearer-Authorization': "Bearer {}".format(access_token)}
super().__init__(auth_params=auth_params)
self._access_token = access_token
91 changes: 54 additions & 37 deletions tests/apimatic_core/request_builder_tests/test_request_builder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from datetime import datetime, date
import pytest
import sys

from apimatic_core.exceptions.auth_validation_exception import AuthValidationException
from apimatic_core_interfaces.types.http_method_enum import HttpMethodEnum
from apimatic_core.authentication.multiple.and_auth_group import And
from apimatic_core.authentication.multiple.or_auth_group import Or
Expand Down Expand Up @@ -589,9 +591,9 @@ def test_multipart_request_with_file_wrapper(self, input_multipart_param_value1,
expected_multipart_param_value1.close()

@pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key, expected_auth_header_value', [
(Single('basic_auth'), 'Authorization', 'Basic {}'.format(
(Single('basic_auth'), 'Basic-Authorization', 'Basic {}'.format(
AuthHelper.get_base64_encoded_value('test_username', 'test_password'))),
(Single('bearer_auth'), 'Authorization', 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42'),
(Single('bearer_auth'), 'Bearer-Authorization', 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42'),
(Single('custom_header_auth'), 'token', 'Qaws2W233WedeRe4T56G6Vref2')
])
def test_header_authentication(self, input_auth_scheme, expected_auth_header_key, expected_auth_header_value):
Expand Down Expand Up @@ -620,44 +622,59 @@ def test_invalid_key_authentication(self, input_invalid_auth_scheme):
.build(self.global_configuration_with_auth)
assert validation_error.value.args[0] == 'Auth key is invalid.'

@pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key_1,'
'expected_auth_header_value_1, expected_auth_header_key_2,'
'expected_auth_header_value_2', [
(Or('basic_auth', 'bearer_auth', 'custom_header_auth'), 'Authorization',
'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', 'token',
'Qaws2W233WedeRe4T56G6Vref2'),
(And('basic_auth', 'bearer_auth', 'custom_header_auth'), 'Authorization',
'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', 'token',
'Qaws2W233WedeRe4T56G6Vref2'),
(Or('basic_auth', And('bearer_auth', 'custom_header_auth')), 'Authorization',
'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', 'token',
'Qaws2W233WedeRe4T56G6Vref2'),
(And('basic_auth', Or('bearer_auth', 'custom_header_auth')), 'Authorization',
'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', 'token',
'Qaws2W233WedeRe4T56G6Vref2'),
(And('basic_auth', And('bearer_auth', 'custom_header_auth')), 'Authorization',
'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', 'token',
'Qaws2W233WedeRe4T56G6Vref2'),
(Or('basic_auth', Or('bearer_auth', 'custom_header_auth')), 'Authorization',
'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', 'token',
'Qaws2W233WedeRe4T56G6Vref2'),
(Or('basic_auth', Or(None, 'custom_header_auth')), 'Authorization',
'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', 'token',
'Qaws2W233WedeRe4T56G6Vref2'),
(Or('basic_auth', And(None, 'custom_header_auth')), 'Authorization',
'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', 'token',
'Qaws2W233WedeRe4T56G6Vref2'),
@pytest.mark.parametrize('input_auth_scheme, expected_request_headers', [
(Or('basic_auth', 'custom_header_auth'),
{
'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk',
'token': None
}),
(And('basic_auth', 'bearer_auth', 'custom_header_auth'),
{
'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk',
'Bearer-Authorization': 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42',
'token': 'Qaws2W233WedeRe4T56G6Vref2'
}),
(Or('basic_auth', And('bearer_auth', 'custom_header_auth')),
{
'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk',
'Bearer-Authorization': None,
'token': None
}),
(And('basic_auth', Or('bearer_auth', 'custom_header_auth')),
{
'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk',
'Bearer-Authorization': 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42',
'token': None
}),
(And('basic_auth', And('bearer_auth', 'custom_header_auth')),
{
'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk',
'Bearer-Authorization': 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42',
'token': 'Qaws2W233WedeRe4T56G6Vref2'
}),
(Or('basic_auth', Or('bearer_auth', 'custom_header_auth')),
{
'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk',
'Bearer-Authorization': None,
'token': None
}),
(Or('basic_auth', Or(None, 'custom_header_auth')),
{
'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk',
'token': None
}),
(Or('basic_auth', And(None, 'custom_header_auth')),
{
'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk',
'token': None
}),
])
def test_success_case_of_multiple_authentications(self, input_auth_scheme, expected_auth_header_key_1,
expected_auth_header_value_1,
expected_auth_header_key_2,
expected_auth_header_value_2):
def test_success_case_of_multiple_authentications(self, input_auth_scheme, expected_request_headers):
http_request = self.new_request_builder \
.auth(input_auth_scheme) \
.build(self.global_configuration_with_auth)

assert http_request.headers[expected_auth_header_key_1] == expected_auth_header_value_1 \
and http_request.headers[expected_auth_header_key_2] == expected_auth_header_value_2
for key in expected_request_headers.keys():
assert http_request.headers.get(key) == expected_request_headers.get(key)

@pytest.mark.parametrize('input_auth_scheme, expected_error_message', [
(Or('basic_auth', 'bearer_auth', 'custom_header_auth'), '[BasicAuth: _basic_auth_user_name or '
Expand Down Expand Up @@ -693,7 +710,7 @@ def test_success_case_of_multiple_authentications(self, input_auth_scheme, expec
(And(None, 'basic_auth'), '[BasicAuth: _basic_auth_user_name or _basic_auth_password is undefined.]')
])
def test_failed_case_of_multiple_authentications(self, input_auth_scheme, expected_error_message):
with pytest.raises(PermissionError) as errors:
with pytest.raises(AuthValidationException) as errors:
self.new_request_builder \
.auth(input_auth_scheme) \
.build(self.global_configuration_with_uninitialized_auth_params)
Expand Down

0 comments on commit 9343356

Please sign in to comment.