Skip to content

Commit 4be8564

Browse files
Merge pull request #94 from minos-framework/0.4.0
v0.4.0
2 parents 0c1f760 + 3caf72e commit 4be8564

File tree

12 files changed

+531
-17
lines changed

12 files changed

+531
-17
lines changed

HISTORY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@
4848
## 0.3.1 (2022-02-04)
4949

5050
* Administration section BugFix getting index file.
51+
52+
## 0.4.0 (2022-02-16)
53+
54+
* Add authorization rules to administration section.
55+
* Authorization rules CRUD.
56+
* Authorization checking on microservice call.

minos/api_gateway/rest/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.3.1"
1+
__version__ = "0.4.0"
22

33
from .config import (
44
ApiGatewayConfig,

minos/api_gateway/rest/backend/admin/main.33051811e0a291cf.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

minos/api_gateway/rest/backend/templates/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
<style>@charset "UTF-8";:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-family-monospace:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}@media print{*,*:before,*:after{text-shadow:none!important;box-shadow:none!important}@page{size:a3}body{min-width:992px!important}}:root{--surface-a:#ffffff;--surface-b:#f8f9fa;--surface-c:#e9ecef;--surface-d:#dee2e6;--surface-e:#ffffff;--surface-f:#ffffff;--text-color:#495057;--text-color-secondary:#6c757d;--primary-color:#2196F3;--primary-color-text:#ffffff;--font-family:-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;--surface-0:#ffffff;--surface-50:#FAFAFA;--surface-100:#F5F5F5;--surface-200:#EEEEEE;--surface-300:#E0E0E0;--surface-400:#BDBDBD;--surface-500:#9E9E9E;--surface-600:#757575;--surface-700:#616161;--surface-800:#424242;--surface-900:#212121;--gray-50:#FAFAFA;--gray-100:#F5F5F5;--gray-200:#EEEEEE;--gray-300:#E0E0E0;--gray-400:#BDBDBD;--gray-500:#9E9E9E;--gray-600:#757575;--gray-700:#616161;--gray-800:#424242;--gray-900:#212121;--content-padding:1rem;--inline-spacing:.5rem;--border-radius:3px;--surface-ground:#f8f9fa;--surface-section:#ffffff;--surface-card:#ffffff;--surface-overlay:#ffffff;--surface-border:#dee2e6;--surface-hover:#e9ecef;--maskbg:rgba(0, 0, 0, .4);--focus-ring:0 0 0 .2rem #a6d5fa}*{box-sizing:border-box}:root{--blue-50:#f4fafe;--blue-100:#cae6fc;--blue-200:#a0d2fa;--blue-300:#75bef8;--blue-400:#4baaf5;--blue-500:#2196f3;--blue-600:#1c80cf;--blue-700:#1769aa;--blue-800:#125386;--blue-900:#0d3c61;--green-50:#f6fbf6;--green-100:#d4ecd5;--green-200:#b2ddb4;--green-300:#90cd93;--green-400:#6ebe71;--green-500:#4caf50;--green-600:#419544;--green-700:#357b38;--green-800:#2a602c;--green-900:#1e4620;--yellow-50:#fffcf5;--yellow-100:#fef0cd;--yellow-200:#fde4a5;--yellow-300:#fdd87d;--yellow-400:#fccc55;--yellow-500:#fbc02d;--yellow-600:#d5a326;--yellow-700:#b08620;--yellow-800:#8a6a19;--yellow-900:#644d12;--cyan-50:#f2fcfd;--cyan-100:#c2eff5;--cyan-200:#91e2ed;--cyan-300:#61d5e4;--cyan-400:#30c9dc;--cyan-500:#00bcd4;--cyan-600:#00a0b4;--cyan-700:#008494;--cyan-800:#006775;--cyan-900:#004b55;--pink-50:#fef4f7;--pink-100:#fac9da;--pink-200:#f69ebc;--pink-300:#f1749e;--pink-400:#ed4981;--pink-500:#e91e63;--pink-600:#c61a54;--pink-700:#a31545;--pink-800:#801136;--pink-900:#5d0c28;--indigo-50:#f5f6fb;--indigo-100:#d1d5ed;--indigo-200:#acb4df;--indigo-300:#8893d1;--indigo-400:#6372c3;--indigo-500:#3f51b5;--indigo-600:#36459a;--indigo-700:#2c397f;--indigo-800:#232d64;--indigo-900:#192048;--teal-50:#f2faf9;--teal-100:#c2e6e2;--teal-200:#91d2cc;--teal-300:#61beb5;--teal-400:#30aa9f;--teal-500:#009688;--teal-600:#008074;--teal-700:#00695f;--teal-800:#00534b;--teal-900:#003c36;--orange-50:#fff8f2;--orange-100:#fde0c2;--orange-200:#fbc791;--orange-300:#f9ae61;--orange-400:#f79530;--orange-500:#f57c00;--orange-600:#d06900;--orange-700:#ac5700;--orange-800:#874400;--orange-900:#623200;--bluegray-50:#f7f9f9;--bluegray-100:#d9e0e3;--bluegray-200:#bbc7cd;--bluegray-300:#9caeb7;--bluegray-400:#7e96a1;--bluegray-500:#607d8b;--bluegray-600:#526a76;--bluegray-700:#435861;--bluegray-800:#35454c;--bluegray-900:#263238;--purple-50:#faf4fb;--purple-100:#e7cbec;--purple-200:#d4a2dd;--purple-300:#c279ce;--purple-400:#af50bf;--purple-500:#9c27b0;--purple-600:#852196;--purple-700:#6d1b7b;--purple-800:#561561;--purple-900:#3e1046}</style><link rel="stylesheet" href="styles.ee7401a7ef5a34ac.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.ee7401a7ef5a34ac.css"></noscript></head>
88
<body>
99
<app-root></app-root>
10-
<script src="runtime.3586ab90ea013581.js" type="module"></script><script src="polyfills.cacc82dae5605706.js" type="module"></script><script src="main.727c29f160d3bc80.js" type="module"></script>
10+
<script src="runtime.3586ab90ea013581.js" type="module"></script><script src="polyfills.cacc82dae5605706.js" type="module"></script><script src="main.33051811e0a291cf.js" type="module"></script>
1111

1212
</body></html>

minos/api_gateway/rest/database/models.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,51 @@ def __init__(self, model: AuthRule):
5656
self.methods = model.methods
5757
self.created_at = str(model.created_at)
5858
self.updated_at = str(model.updated_at)
59+
60+
61+
class AutzRule(Base):
62+
__tablename__ = "autz_rules"
63+
id = Column(Integer, Sequence("item_id_seq"), nullable=False, primary_key=True)
64+
service = Column(String, primary_key=True, nullable=False)
65+
rule = Column(String, primary_key=True, nullable=False)
66+
roles = Column(JSON)
67+
methods = Column(JSON)
68+
created_at = Column(TIMESTAMP)
69+
updated_at = Column(TIMESTAMP)
70+
71+
def __repr__(self):
72+
return (
73+
"<AuthRule(id='{}', service='{}', rule='{}',"
74+
"methods={}, created_at={}, updated_at={})>".format( # pragma: no cover
75+
self.id, self.service, self.roles, self.methods, self.created_at, self.updated_at
76+
)
77+
)
78+
79+
def to_serializable_dict(self):
80+
return {
81+
"id": self.id,
82+
"service": self.service,
83+
"roles": self.roles,
84+
"methods": self.methods,
85+
"created_at": str(self.created_at),
86+
"updated_at": str(self.updated_at),
87+
}
88+
89+
90+
class AutzRuleDTO:
91+
id: int
92+
service: str
93+
rule: str
94+
roles: list
95+
methods: list
96+
created_at: str
97+
updated_at: str
98+
99+
def __init__(self, model: AutzRule):
100+
self.id = model.id
101+
self.rule = model.rule
102+
self.service = model.service
103+
self.roles = model.roles
104+
self.methods = model.methods
105+
self.created_at = str(model.created_at)
106+
self.updated_at = str(model.updated_at)

minos/api_gateway/rest/database/repository.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from .models import (
99
AuthRule,
1010
AuthRuleDTO,
11+
AutzRule,
12+
AutzRuleDTO,
1113
)
1214

1315

@@ -17,31 +19,60 @@ def __init__(self, engine):
1719
self.s = sessionmaker(bind=engine)
1820
self.session = self.s()
1921

20-
def create(self, record: AuthRule):
22+
def create_auth_rule(self, record: AuthRule):
2123
self.session.add(record)
2224
self.session.commit()
2325
return record.to_serializable_dict()
2426

25-
def get_all(self):
27+
def create_autz_rule(self, record: AutzRule):
28+
self.session.add(record)
29+
self.session.commit()
30+
return record.to_serializable_dict()
31+
32+
def get_auth_rules(self):
2633
r = self.session.query(AuthRule).all()
2734

2835
records = list()
2936
for record in r:
3037
records.append(AuthRuleDTO(record).__dict__)
3138
return records
3239

33-
def update(self, id: int, **kwargs):
40+
def get_autz_rules(self):
41+
r = self.session.query(AutzRule).all()
42+
43+
records = list()
44+
for record in r:
45+
records.append(AutzRuleDTO(record).__dict__)
46+
return records
47+
48+
def update_auth_rule(self, id: int, **kwargs):
3449
self.session.query(AuthRule).filter(AuthRule.id == id).update(kwargs)
3550
self.session.commit()
3651

37-
def delete(self, id: int):
52+
def update_autz_rule(self, id: int, **kwargs):
53+
self.session.query(AutzRule).filter(AutzRule.id == id).update(kwargs)
54+
self.session.commit()
55+
56+
def delete_auth_rule(self, id: int):
3857
self.session.query(AuthRule).filter(AuthRule.id == id).delete()
3958
self.session.commit()
4059

41-
def get_by_service(self, service: str):
60+
def delete_autz_rule(self, id: int):
61+
self.session.query(AutzRule).filter(AutzRule.id == id).delete()
62+
self.session.commit()
63+
64+
def get_auth_rule_by_service(self, service: str):
4265
r = self.session.query(AuthRule).filter(or_(AuthRule.service == service, AuthRule.service == "*")).all()
4366

4467
records = list()
4568
for record in r:
4669
records.append(AuthRuleDTO(record))
4770
return records
71+
72+
def get_autz_rule_by_service(self, service: str):
73+
r = self.session.query(AutzRule).filter(or_(AutzRule.service == service, AutzRule.service == "*")).all()
74+
75+
records = list()
76+
for record in r:
77+
records.append(AutzRuleDTO(record))
78+
return records

minos/api_gateway/rest/handler.py

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from minos.api_gateway.rest.database.models import (
2323
AuthRule,
24+
AutzRule,
2425
)
2526
from minos.api_gateway.rest.urlmatch.authmatch import (
2627
AuthMatch,
@@ -29,6 +30,9 @@
2930
from .database.repository import (
3031
Repository,
3132
)
33+
from .urlmatch.autzmatch import (
34+
AutzMatch,
35+
)
3236

3337
logger = logging.getLogger(__name__)
3438

@@ -46,20 +50,44 @@ async def orchestrate(request: web.Request) -> web.Response:
4650
auth = request.app["config"].rest.auth
4751
user = None
4852
if auth is not None and auth.enabled:
49-
if await check_auth(request=request, service=request.url.parts[1], url=str(request.url), method=request.method):
53+
if await check_authentication(
54+
request=request, service=request.url.parts[1], url=str(request.url), method=request.method
55+
):
5056
response = await validate_token(request)
5157
user = json.loads(response)
5258
user = user["uuid"]
5359

60+
if await check_authorization(
61+
request=request, service=request.url.parts[1], url=str(request.url), method=request.method
62+
):
63+
response = await validate_token(request)
64+
data = json.loads(response)
65+
user = data["uuid"]
66+
role = data["role"]
67+
if not await is_authorized_role(
68+
request=request, role=role, service=request.url.parts[1], url=str(request.url), method=request.method
69+
):
70+
return web.HTTPUnauthorized()
71+
5472
microservice_response = await call(**discovery_data, original_req=request, user=user)
5573
return microservice_response
5674

5775

58-
async def check_auth(request: web.Request, service: str, url: str, method: str) -> bool:
59-
records = Repository(request.app["db_engine"]).get_by_service(service)
76+
async def check_authentication(request: web.Request, service: str, url: str, method: str) -> bool:
77+
records = Repository(request.app["db_engine"]).get_auth_rule_by_service(service)
78+
return AuthMatch.match(url=url, method=method, records=records)
79+
80+
81+
async def check_authorization(request: web.Request, service: str, url: str, method: str) -> bool:
82+
records = Repository(request.app["db_engine"]).get_autz_rule_by_service(service)
6083
return AuthMatch.match(url=url, method=method, records=records)
6184

6285

86+
async def is_authorized_role(request: web.Request, role: int, service: str, url: str, method: str) -> bool:
87+
records = Repository(request.app["db_engine"]).get_autz_rule_by_service(service)
88+
return AutzMatch.match(url=url, role=role, method=method, records=records)
89+
90+
6391
async def authentication_default(request: web.Request) -> web.Response:
6492
""" Orchestrate discovery and microservice call """
6593
auth_host = request.app["config"].rest.auth.host
@@ -239,9 +267,26 @@ async def get_endpoints(request: web.Request) -> web.Response:
239267
{"error": "The requested endpoint is not available."}, status=web.HTTPServiceUnavailable.status_code
240268
)
241269

270+
@staticmethod
271+
async def get_roles(request: web.Request) -> web.Response:
272+
auth_host = request.app["config"].rest.auth.host
273+
auth_port = request.app["config"].rest.auth.port
274+
auth_path = request.app["config"].rest.auth.path
275+
276+
url = URL.build(scheme="http", host=auth_host, port=auth_port, path=f"{auth_path}/roles")
277+
278+
try:
279+
async with ClientSession() as session:
280+
async with session.get(url=url) as response:
281+
return await _clone_response(response)
282+
except ClientConnectorError:
283+
return web.json_response(
284+
{"error": "The requested endpoint is not available."}, status=web.HTTPServiceUnavailable.status_code
285+
)
286+
242287
@staticmethod
243288
async def get_rules(request: web.Request) -> web.Response:
244-
records = Repository(request.app["db_engine"]).get_all()
289+
records = Repository(request.app["db_engine"]).get_auth_rules()
245290
return web.json_response(records)
246291

247292
@staticmethod
@@ -265,7 +310,7 @@ async def create_rule(request: web.Request) -> web.Response:
265310
updated_at=now,
266311
)
267312

268-
record = Repository(request.app["db_engine"]).create(rule)
313+
record = Repository(request.app["db_engine"]).create_auth_rule(rule)
269314

270315
return web.json_response(record)
271316
except Exception as e:
@@ -276,7 +321,17 @@ async def update_rule(request: web.Request) -> web.Response:
276321
try:
277322
id = int(request.url.name)
278323
content = await request.json()
279-
Repository(request.app["db_engine"]).update(id=id, **content)
324+
Repository(request.app["db_engine"]).update_auth_rule(id=id, **content)
325+
return web.json_response(status=web.HTTPOk.status_code)
326+
except Exception as e:
327+
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)
328+
329+
@staticmethod
330+
async def update_autz_rule(request: web.Request) -> web.Response:
331+
try:
332+
id = int(request.url.name)
333+
content = await request.json()
334+
Repository(request.app["db_engine"]).update_autz_rule(id=id, **content)
280335
return web.json_response(status=web.HTTPOk.status_code)
281336
except Exception as e:
282337
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)
@@ -285,7 +340,54 @@ async def update_rule(request: web.Request) -> web.Response:
285340
async def delete_rule(request: web.Request) -> web.Response:
286341
try:
287342
id = int(request.url.name)
288-
Repository(request.app["db_engine"]).delete(id)
343+
Repository(request.app["db_engine"]).delete_auth_rule(id)
344+
return web.json_response(status=web.HTTPOk.status_code)
345+
except Exception as e:
346+
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)
347+
348+
@staticmethod
349+
async def delete_autz_rule(request: web.Request) -> web.Response:
350+
try:
351+
id = int(request.url.name)
352+
Repository(request.app["db_engine"]).delete_autz_rule(id)
289353
return web.json_response(status=web.HTTPOk.status_code)
290354
except Exception as e:
291355
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)
356+
357+
@staticmethod
358+
async def create_autz_rule(request: web.Request) -> web.Response:
359+
try:
360+
content = await request.json()
361+
362+
if (
363+
"service" not in content
364+
and "rule" not in content
365+
and "roles" not in content
366+
and "methods" not in content
367+
):
368+
return web.json_response(
369+
{"error": "Wrong data. Provide 'service', 'rule', 'roles' and 'methods' parameters."},
370+
status=web.HTTPBadRequest.status_code,
371+
)
372+
373+
now = datetime.now()
374+
375+
rule = AutzRule(
376+
service=content["service"],
377+
rule=content["rule"],
378+
roles=content["roles"],
379+
methods=content["methods"],
380+
created_at=now,
381+
updated_at=now,
382+
)
383+
384+
record = Repository(request.app["db_engine"]).create_autz_rule(rule)
385+
386+
return web.json_response(record)
387+
except Exception as e:
388+
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)
389+
390+
@staticmethod
391+
async def get_autz_rules(request: web.Request) -> web.Response:
392+
records = Repository(request.app["db_engine"]).get_autz_rules()
393+
return web.json_response(records)

minos/api_gateway/rest/service.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ async def create_application(self) -> web.Application:
7070
app.router.add_route("PATCH", "/admin/rules/{id}", AdminHandler.update_rule)
7171
app.router.add_route("DELETE", "/admin/rules/{id}", AdminHandler.delete_rule)
7272

73+
app.router.add_route("GET", "/admin/roles", AdminHandler.get_roles)
74+
app.router.add_route("POST", "/admin/autz-rules", AdminHandler.create_autz_rule)
75+
app.router.add_route("GET", "/admin/autz-rules", AdminHandler.get_autz_rules)
76+
app.router.add_route("PATCH", "/admin/autz-rules/{id}", AdminHandler.update_autz_rule)
77+
app.router.add_route("DELETE", "/admin/autz-rules/{id}", AdminHandler.delete_autz_rule)
78+
7379
# Administration routes
7480
path = Path(__file__).parent
7581
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(f"{path}/backend/templates"))
@@ -92,7 +98,7 @@ async def create_database(self):
9298
Base.metadata.create_all(self.engine)
9399

94100
@aiohttp_jinja2.template("tmpl.jinja2")
95-
async def handler(self, request):
101+
async def handler(self, request): # pragma: no cover
96102
try:
97103
path = Path(__file__).parent
98104
filename = Path(request.match_info["path"].replace("/", "", 1))
@@ -107,7 +113,7 @@ async def handler(self, request):
107113
return response
108114

109115
@staticmethod
110-
async def _get_file(file_path) -> web.FileResponse:
116+
async def _get_file(file_path) -> web.FileResponse: # pragma: no cover
111117
try:
112118
return web.FileResponse(path=file_path, status=200)
113119
except (ValueError, FileNotFoundError) as error:
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from ..database.models import (
2+
AutzRuleDTO,
3+
)
4+
from .urlmatch import (
5+
UrlMatch,
6+
)
7+
8+
9+
class AutzMatch(UrlMatch):
10+
@staticmethod
11+
def match(url: str, role: int, method: str, records: list[AutzRuleDTO]) -> bool:
12+
for record in records:
13+
if AutzMatch.urlmatch(record.rule, url):
14+
if record.roles is None: # pragma: no cover
15+
return True
16+
else:
17+
if role in record.roles or "*" in record.roles:
18+
return True
19+
20+
return False

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "minos_apigateway"
3-
version = "0.3.1"
3+
version = "0.4.0"
44
description = "Python Package containing the main API Gateway implementation used in Minos Microservices."
55
readme = "README.md"
66
repository = "https://github.com/clariteia/api_gateway"

0 commit comments

Comments
 (0)