Skip to content

Commit 0a08448

Browse files
author
Sergio García Prado
authored
Merge pull request #57 from Clariteia/issue-56-parameterize-auth
#56 - Parameterize `auth` configuration
2 parents 948d9d5 + 0859659 commit 0a08448

File tree

9 files changed

+241
-67
lines changed

9 files changed

+241
-67
lines changed

minos/api_gateway/rest/config.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,20 @@
1919
ApiGatewayConfigException,
2020
)
2121

22-
REST = collections.namedtuple("Rest", "host port cors")
22+
REST = collections.namedtuple("Rest", "host port cors auth")
2323
DISCOVERY = collections.namedtuple("Discovery", "host port")
2424
CORS = collections.namedtuple("Cors", "enabled")
25+
AUTH = collections.namedtuple("Auth", "enabled host port method path")
2526

2627
_ENVIRONMENT_MAPPER = {
2728
"rest.host": "API_GATEWAY_REST_HOST",
2829
"rest.port": "API_GATEWAY_REST_PORT",
2930
"rest.cors.enabled": "API_GATEWAY_REST_CORS_ENABLED",
31+
"rest.auth.enabled": "API_GATEWAY_REST_AUTH_ENABLED",
32+
"rest.auth.host": "API_GATEWAY_REST_AUTH_HOST",
33+
"rest.auth.port": "API_GATEWAY_REST_AUTH_PORT",
34+
"rest.auth.method": "API_GATEWAY_REST_AUTH_METHOD",
35+
"rest.auth.path": "API_GATEWAY_REST_AUTH_PATH",
3036
"discovery.host": "API_GATEWAY_DISCOVERY_HOST",
3137
"discovery.port": "API_GATEWAY_DISCOVERY_PORT",
3238
}
@@ -35,6 +41,11 @@
3541
"rest.host": "api_gateway_rest_host",
3642
"rest.port": "api_gateway_rest_port",
3743
"rest.cors.enabled": "api_gateway_rest_cors_enabled",
44+
"rest.auth.enabled": "api_gateway_rest_auth_enabled",
45+
"rest.auth.host": "api_gateway_rest_auth_host",
46+
"rest.auth.port": "api_gateway_rest_auth_port",
47+
"rest.auth.method": "api_gateway_rest_auth_method",
48+
"rest.auth.path": "api_gateway_rest_auth_path",
3849
"discovery.host": "api_gateway_discovery_host",
3950
"discovery.port": "api_gateway_discovery_port",
4051
}
@@ -93,7 +104,7 @@ def rest(self) -> REST:
93104
94105
:return: A ``REST`` NamedTuple instance.
95106
"""
96-
return REST(host=self._get("rest.host"), port=int(self._get("rest.port")), cors=self._cors)
107+
return REST(host=self._get("rest.host"), port=int(self._get("rest.port")), cors=self._cors, auth=self._auth)
97108

98109
@property
99110
def _cors(self) -> CORS:
@@ -103,6 +114,19 @@ def _cors(self) -> CORS:
103114
"""
104115
return CORS(enabled=self._get("rest.cors.enabled"))
105116

117+
@property
118+
def _auth(self) -> t.Optional[AUTH]:
119+
try:
120+
return AUTH(
121+
enabled=self._get("rest.auth.enabled"),
122+
host=self._get("rest.auth.host"),
123+
port=int(self._get("rest.auth.port")),
124+
method=self._get("rest.auth.method"),
125+
path=self._get("rest.auth.path"),
126+
)
127+
except KeyError:
128+
return None
129+
106130
@property
107131
def discovery(self) -> DISCOVERY:
108132
"""Get the rest config.

minos/api_gateway/rest/exceptions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
class ApiGatewayException(Exception):
2-
"""TODO"""
2+
"""Base Api Gateway Exception."""
33

44

55
class InvalidAuthenticationException(ApiGatewayException):
6-
"""TODO"""
6+
"""Exception to be raised when authentication is not valid."""
77

88

99
class NoTokenException(ApiGatewayException):
10-
"""TODO"""
10+
"""Exception to be raised when token is not available."""
1111

1212

1313
class ApiGatewayConfigException(ApiGatewayException):

minos/api_gateway/rest/handler.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
from typing import (
33
Any,
4+
Optional,
45
)
56

67
from aiohttp import (
@@ -18,8 +19,6 @@
1819
NoTokenException,
1920
)
2021

21-
ANONYMOUS = "Anonymous"
22-
2322
logger = logging.getLogger(__name__)
2423

2524

@@ -39,19 +38,26 @@ async def orchestrate(request: web.Request) -> web.Response:
3938
return microservice_response
4039

4140

42-
async def get_user(request) -> str:
41+
async def get_user(request: web.Request) -> Optional[str]:
42+
"""Get The user identifier if it is available.
43+
44+
:param request: The external request.
45+
:return: An string value containing the user identifier or ``None`` if no user information is available.
46+
"""
47+
auth = request.app["config"].rest.auth
48+
if auth is None or not auth.enabled:
49+
return None
50+
4351
try:
4452
await get_token(request)
4553
except NoTokenException:
46-
return ANONYMOUS
47-
else:
48-
try:
49-
original_headers = dict(request.headers.copy())
50-
user_uuid = await authenticate("localhost", "8082", "POST", "token", original_headers)
51-
except InvalidAuthenticationException:
52-
return ANONYMOUS
53-
else:
54-
return user_uuid
54+
return None
55+
56+
try:
57+
original_headers = dict(request.headers.copy())
58+
return await authenticate(auth.host, auth.port, auth.method, auth.path, original_headers)
59+
except InvalidAuthenticationException:
60+
return None
5561

5662

5763
async def discover(host: str, port: int, path: str, verb: str, endpoint: str) -> dict[str, Any]:
@@ -81,7 +87,7 @@ async def discover(host: str, port: int, path: str, verb: str, endpoint: str) ->
8187

8288

8389
# noinspection PyUnusedLocal
84-
async def call(address: str, port: int, original_req: web.Request, user: str, **kwargs) -> web.Response:
90+
async def call(address: str, port: int, original_req: web.Request, user: Optional[str], **kwargs) -> web.Response:
8591
"""Call microservice (redirect the original call)
8692
8793
:param address: The ip of the microservices.
@@ -93,7 +99,11 @@ async def call(address: str, port: int, original_req: web.Request, user: str, **
9399
"""
94100

95101
headers = original_req.headers.copy()
96-
headers["User"] = user
102+
if user is not None:
103+
headers["User"] = user
104+
else: # Enforce that the 'User' entry is only generated by the auth system.
105+
# noinspection PyTypeChecker
106+
headers.pop("User", None)
97107

98108
url = original_req.url.with_scheme("http").with_host(address).with_port(port)
99109
method = original_req.method
@@ -116,8 +126,8 @@ async def _clone_response(response: ClientResponse) -> web.Response:
116126
)
117127

118128

119-
async def authenticate(address: str, port: str, method: str, path: str, authorization_headers: dict[str, str]) -> str:
120-
authentication_url = URL(f"http://{address}:{port}/{path}")
129+
async def authenticate(host: str, port: str, method: str, path: str, authorization_headers: dict[str, str]) -> str:
130+
authentication_url = URL(f"http://{host}:{port}{path}")
121131
authentication_method = method
122132
logger.info("Authenticating request...")
123133

poetry.lock

Lines changed: 37 additions & 33 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ typer = "^0.3.2"
3535
cached-property = "^1.5.2"
3636
aiohttp-middlewares = "^1.1.0"
3737
aiomisc = "^15.2.16"
38+
PyYAML = "^6.0"
3839

3940
[tool.poetry.dev-dependencies]
4041
black = "^19.10b"

tests/config.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ rest:
22
host: localhost
33
port: 55909
44
cors:
5-
enabled: false
5+
enabled: true
6+
auth:
7+
enabled: true
8+
host: localhost
9+
port: 8082
10+
method: POST
11+
path: /token
612
discovery:
713
host: localhost
814
port: 5567

tests/config_without_auth.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
rest:
2+
host: localhost
3+
port: 55909
4+
cors:
5+
enabled: false
6+
discovery:
7+
host: localhost
8+
port: 5567
9+

tests/test_api_gateway/test_rest/test_authentication.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,52 @@ async def test_request_has_token(self):
9494
self.assertIn("Microservice call correct!!!", await response.text())
9595

9696

97+
class TestAuthDisabled(AioHTTPTestCase):
98+
CONFIG_FILE_PATH = BASE_PATH / "config_without_auth.yml"
99+
100+
def setUp(self) -> None:
101+
self.config = ApiGatewayConfig(self.CONFIG_FILE_PATH)
102+
103+
self.discovery = MockServer(host=self.config.discovery.host, port=self.config.discovery.port,)
104+
self.discovery.add_json_response(
105+
"/microservices", {"address": "localhost", "port": "5568", "status": True},
106+
)
107+
108+
self.microservice = MockServer(host="localhost", port=5568)
109+
self.microservice.add_json_response(
110+
"/order/5", "Microservice call correct!!!", methods=("GET", "PUT", "PATCH", "DELETE",)
111+
)
112+
self.microservice.add_json_response("/order", "Microservice call correct!!!", methods=("POST",))
113+
114+
self.discovery.start()
115+
self.microservice.start()
116+
super().setUp()
117+
118+
def tearDown(self) -> None:
119+
self.discovery.shutdown_server()
120+
self.microservice.shutdown_server()
121+
super().tearDown()
122+
123+
async def get_application(self):
124+
"""
125+
Override the get_app method to return your application.
126+
"""
127+
rest_service = ApiGatewayRestService(
128+
address=self.config.rest.host, port=self.config.rest.port, config=self.config
129+
)
130+
131+
return await rest_service.create_application()
132+
133+
@unittest_run_loop
134+
async def test_auth_unreachable(self):
135+
url = "/order"
136+
headers = {"Authorization": "Bearer test_token"}
137+
response = await self.client.request("POST", url, headers=headers)
138+
139+
self.assertEqual(200, response.status)
140+
self.assertIn("Microservice call correct!!!", await response.text())
141+
142+
97143
class TestAuthUnreachable(AioHTTPTestCase):
98144
CONFIG_FILE_PATH = BASE_PATH / "config.yml"
99145

0 commit comments

Comments
 (0)