Skip to content

Commit 0882411

Browse files
author
Sergio García Prado
authored
Merge pull request #60 from Clariteia/issue-59-improve-validation-errors
#59 - Raise an exception when token validations fails
2 parents 952222d + d6f8838 commit 0882411

File tree

5 files changed

+128
-25
lines changed

5 files changed

+128
-25
lines changed

minos/api_gateway/rest/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from .exceptions import (
77
ApiGatewayConfigException,
88
ApiGatewayException,
9-
InvalidAuthenticationException,
109
NoTokenException,
1110
)
1211
from .launchers import (

minos/api_gateway/rest/exceptions.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ class ApiGatewayException(Exception):
22
"""Base Api Gateway Exception."""
33

44

5-
class InvalidAuthenticationException(ApiGatewayException):
6-
"""Exception to be raised when authentication is not valid."""
7-
8-
95
class NoTokenException(ApiGatewayException):
106
"""Exception to be raised when token is not available."""
117

minos/api_gateway/rest/handler.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
)
1616

1717
from .exceptions import (
18-
InvalidAuthenticationException,
1918
NoTokenException,
2019
)
2120

@@ -53,11 +52,8 @@ async def get_user(request: web.Request) -> Optional[str]:
5352
except NoTokenException:
5453
return None
5554

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
55+
original_headers = dict(request.headers.copy())
56+
return await authenticate(auth.host, auth.port, auth.method, auth.path, original_headers)
6157

6258

6359
async def discover(host: str, port: int, path: str, verb: str, endpoint: str) -> dict[str, Any]:
@@ -76,10 +72,13 @@ async def discover(host: str, port: int, path: str, verb: str, endpoint: str) ->
7672
async with ClientSession() as session:
7773
async with session.get(url=url) as response:
7874
if not response.ok:
79-
raise web.HTTPBadGateway(text="The discovery response is not okay.")
75+
if response.status == 404:
76+
raise web.HTTPNotFound(text=f"The {endpoint!r} path is not available for {verb!r} method.")
77+
raise web.HTTPBadGateway(text="The Discovery Service response is wrong.")
78+
8079
data = await response.json()
8180
except ClientConnectorError:
82-
raise web.HTTPGatewayTimeout(text="The discovery is not available.")
81+
raise web.HTTPGatewayTimeout(text="The Discovery Service is not available.")
8382

8483
data["port"] = int(data["port"])
8584

@@ -127,20 +126,30 @@ async def _clone_response(response: ClientResponse) -> web.Response:
127126

128127

129128
async def authenticate(host: str, port: str, method: str, path: str, authorization_headers: dict[str, str]) -> str:
129+
"""Authenticate a request based on its headers.
130+
131+
:param host: The authentication service host.
132+
:param port: The authentication Service port.
133+
:param method: The Authentication Service method.
134+
:param path: The Authentication Service path.
135+
:param authorization_headers: The headers that contain the authentication metadata.
136+
:return: The authenticated user identifier.
137+
"""
130138
authentication_url = URL(f"http://{host}:{port}{path}")
131139
authentication_method = method
132140
logger.info("Authenticating request...")
133141

134142
try:
135143
async with ClientSession(headers=authorization_headers) as session:
136144
async with session.request(method=authentication_method, url=authentication_url) as response:
137-
if response.ok:
138-
jwt_payload = await response.json()
139-
return jwt_payload["sub"]
140-
else:
141-
raise InvalidAuthenticationException
142-
except (ClientConnectorError, web.HTTPError):
143-
raise InvalidAuthenticationException
145+
if not response.ok:
146+
raise web.HTTPUnauthorized(text="The given request does not have authorization to be forwarded.")
147+
148+
payload = await response.json()
149+
return payload["sub"]
150+
151+
except ClientConnectorError:
152+
raise web.HTTPGatewayTimeout(text="The Authentication Service is not available.")
144153

145154

146155
async def get_token(request: web.Request) -> str:

tests/test_api_gateway/test_rest/test_authentication.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import unittest
23
from unittest import (
34
mock,
45
)
@@ -10,6 +11,9 @@
1011
AioHTTPTestCase,
1112
unittest_run_loop,
1213
)
14+
from werkzeug.exceptions import (
15+
abort,
16+
)
1317

1418
from minos.api_gateway.rest import (
1519
ApiGatewayConfig,
@@ -131,7 +135,7 @@ async def get_application(self):
131135
return await rest_service.create_application()
132136

133137
@unittest_run_loop
134-
async def test_auth_unreachable(self):
138+
async def test_auth_disabled(self):
135139
url = "/order"
136140
headers = {"Authorization": "Bearer test_token"}
137141
response = await self.client.request("POST", url, headers=headers)
@@ -140,6 +144,59 @@ async def test_auth_unreachable(self):
140144
self.assertIn("Microservice call correct!!!", await response.text())
141145

142146

147+
class TestAuthFailed(AioHTTPTestCase):
148+
CONFIG_FILE_PATH = BASE_PATH / "config.yml"
149+
150+
@mock.patch.dict(os.environ, {"API_GATEWAY_REST_CORS_ENABLED": "true"})
151+
def setUp(self) -> None:
152+
self.config = ApiGatewayConfig(self.CONFIG_FILE_PATH)
153+
154+
self.discovery = MockServer(host=self.config.discovery.host, port=self.config.discovery.port,)
155+
self.discovery.add_json_response(
156+
"/microservices", {"address": "localhost", "port": "5568", "status": True},
157+
)
158+
159+
self.microservice = MockServer(host="localhost", port=5568)
160+
self.microservice.add_json_response(
161+
"/order/5", "Microservice call correct!!!", methods=("GET", "PUT", "PATCH", "DELETE",)
162+
)
163+
self.microservice.add_json_response("/order", "Microservice call correct!!!", methods=("POST",))
164+
165+
self.authentication_service = MockServer(host="localhost", port=8082)
166+
167+
self.authentication_service.add_callback_response("/token", lambda: abort(400), methods=("POST",))
168+
169+
self.discovery.start()
170+
self.microservice.start()
171+
self.authentication_service.start()
172+
super().setUp()
173+
174+
def tearDown(self) -> None:
175+
self.discovery.shutdown_server()
176+
self.microservice.shutdown_server()
177+
self.authentication_service.shutdown_server()
178+
super().tearDown()
179+
180+
async def get_application(self):
181+
"""
182+
Override the get_app method to return your application.
183+
"""
184+
rest_service = ApiGatewayRestService(
185+
address=self.config.rest.host, port=self.config.rest.port, config=self.config
186+
)
187+
188+
return await rest_service.create_application()
189+
190+
@unittest_run_loop
191+
async def test_auth_unauthorized(self):
192+
url = "/order"
193+
headers = {"Authorization": "Bearer test_token"}
194+
response = await self.client.request("POST", url, headers=headers)
195+
196+
self.assertEqual(401, response.status)
197+
self.assertIn("The given request does not have authorization to be forwarded", await response.text())
198+
199+
143200
class TestAuthUnreachable(AioHTTPTestCase):
144201
CONFIG_FILE_PATH = BASE_PATH / "config.yml"
145202

@@ -182,5 +239,9 @@ async def test_auth_unreachable(self):
182239
headers = {"Authorization": "Bearer test_token"}
183240
response = await self.client.request("POST", url, headers=headers)
184241

185-
self.assertEqual(200, response.status)
186-
self.assertIn("Microservice call correct!!!", await response.text())
242+
self.assertEqual(504, response.status)
243+
self.assertIn("The Authentication Service is not available", await response.text())
244+
245+
246+
if __name__ == "__main__":
247+
unittest.main()

tests/test_api_gateway/test_rest/test_service.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
AioHTTPTestCase,
77
unittest_run_loop,
88
)
9+
from werkzeug.exceptions import (
10+
abort,
11+
)
912

1013
from minos.api_gateway.rest import (
1114
ApiGatewayConfig,
@@ -95,13 +98,48 @@ async def test_delete(self):
9598
self.assertIn("Microservice call correct!!!", await response.text())
9699

97100

101+
class TestApiGatewayRestServiceNotFoundDiscovery(AioHTTPTestCase):
102+
CONFIG_FILE_PATH = BASE_PATH / "config.yml"
103+
104+
def setUp(self) -> None:
105+
self.config = ApiGatewayConfig(self.CONFIG_FILE_PATH)
106+
107+
self.discovery = MockServer(host=self.config.discovery.host, port=self.config.discovery.port,)
108+
109+
self.discovery.start()
110+
super().setUp()
111+
112+
def tearDown(self) -> None:
113+
self.discovery.shutdown_server()
114+
super().tearDown()
115+
116+
async def get_application(self):
117+
"""
118+
Override the get_app method to return your application.
119+
"""
120+
rest_service = ApiGatewayRestService(
121+
address=self.config.rest.host, port=self.config.rest.port, config=self.config
122+
)
123+
124+
return await rest_service.create_application()
125+
126+
@unittest_run_loop
127+
async def test_get(self):
128+
url = "/order/5?verb=GET&path=12324"
129+
response = await self.client.request("GET", url)
130+
131+
self.assertEqual(404, response.status)
132+
self.assertIn("The '/order/5' path is not available for 'GET' method.", await response.text())
133+
134+
98135
class TestApiGatewayRestServiceFailedDiscovery(AioHTTPTestCase):
99136
CONFIG_FILE_PATH = BASE_PATH / "config.yml"
100137

101138
def setUp(self) -> None:
102139
self.config = ApiGatewayConfig(self.CONFIG_FILE_PATH)
103140

104141
self.discovery = MockServer(host=self.config.discovery.host, port=self.config.discovery.port,)
142+
self.discovery.add_callback_response("/microservices", lambda: abort(400), methods=("GET",))
105143

106144
self.discovery.start()
107145
super().setUp()
@@ -126,7 +164,7 @@ async def test_get(self):
126164
response = await self.client.request("GET", url)
127165

128166
self.assertEqual(502, response.status)
129-
self.assertIn("The discovery response is not okay.", await response.text())
167+
self.assertIn("The Discovery Service response is wrong.", await response.text())
130168

131169

132170
class TestApiGatewayRestServiceUnreachableDiscovery(AioHTTPTestCase):
@@ -152,7 +190,7 @@ async def test_get(self):
152190
response = await self.client.request("GET", url)
153191

154192
self.assertEqual(504, response.status)
155-
self.assertIn("The discovery is not available.", await response.text())
193+
self.assertIn("The Discovery Service is not available.", await response.text())
156194

157195

158196
class TestApiGatewayRestServiceUnreachableMicroservice(AioHTTPTestCase):

0 commit comments

Comments
 (0)