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

#92 - Authorization #93

Merged
merged 10 commits into from
Feb 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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

This file was deleted.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion minos/api_gateway/rest/backend/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
<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>
<body>
<app-root></app-root>
<script src="runtime.3586ab90ea013581.js" type="module"></script><script src="polyfills.cacc82dae5605706.js" type="module"></script><script src="main.0d2304017c562996.js" type="module"></script>
<script src="runtime.3586ab90ea013581.js" type="module"></script><script src="polyfills.cacc82dae5605706.js" type="module"></script><script src="main.33051811e0a291cf.js" type="module"></script>

</body></html>
48 changes: 48 additions & 0 deletions minos/api_gateway/rest/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,51 @@ def __init__(self, model: AuthRule):
self.methods = model.methods
self.created_at = str(model.created_at)
self.updated_at = str(model.updated_at)


class AutzRule(Base):
__tablename__ = "autz_rules"
id = Column(Integer, Sequence("item_id_seq"), nullable=False, primary_key=True)
service = Column(String, primary_key=True, nullable=False)
rule = Column(String, primary_key=True, nullable=False)
roles = Column(JSON)
methods = Column(JSON)
created_at = Column(TIMESTAMP)
updated_at = Column(TIMESTAMP)

def __repr__(self):
return (
"<AuthRule(id='{}', service='{}', rule='{}',"
"methods={}, created_at={}, updated_at={})>".format( # pragma: no cover
self.id, self.service, self.roles, self.methods, self.created_at, self.updated_at
)
)

def to_serializable_dict(self):
return {
"id": self.id,
"service": self.service,
"roles": self.roles,
"methods": self.methods,
"created_at": str(self.created_at),
"updated_at": str(self.updated_at),
}


class AutzRuleDTO:
id: int
service: str
rule: str
roles: list
methods: list
created_at: str
updated_at: str

def __init__(self, model: AutzRule):
self.id = model.id
self.rule = model.rule
self.service = model.service
self.roles = model.roles
self.methods = model.methods
self.created_at = str(model.created_at)
self.updated_at = str(model.updated_at)
41 changes: 36 additions & 5 deletions minos/api_gateway/rest/database/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from .models import (
AuthRule,
AuthRuleDTO,
AutzRule,
AutzRuleDTO,
)


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

def create(self, record: AuthRule):
def create_auth_rule(self, record: AuthRule):
self.session.add(record)
self.session.commit()
return record.to_serializable_dict()

def get_all(self):
def create_autz_rule(self, record: AutzRule):
self.session.add(record)
self.session.commit()
return record.to_serializable_dict()

def get_auth_rules(self):
r = self.session.query(AuthRule).all()

records = list()
for record in r:
records.append(AuthRuleDTO(record).__dict__)
return records

def update(self, id: int, **kwargs):
def get_autz_rules(self):
r = self.session.query(AutzRule).all()

records = list()
for record in r:
records.append(AutzRuleDTO(record).__dict__)
return records

def update_auth_rule(self, id: int, **kwargs):
self.session.query(AuthRule).filter(AuthRule.id == id).update(kwargs)
self.session.commit()

def delete(self, id: int):
def update_autz_rule(self, id: int, **kwargs):
self.session.query(AutzRule).filter(AutzRule.id == id).update(kwargs)
self.session.commit()

def delete_auth_rule(self, id: int):
self.session.query(AuthRule).filter(AuthRule.id == id).delete()
self.session.commit()

def get_by_service(self, service: str):
def delete_autz_rule(self, id: int):
self.session.query(AutzRule).filter(AutzRule.id == id).delete()
self.session.commit()

def get_auth_rule_by_service(self, service: str):
r = self.session.query(AuthRule).filter(or_(AuthRule.service == service, AuthRule.service == "*")).all()

records = list()
for record in r:
records.append(AuthRuleDTO(record))
return records

def get_autz_rule_by_service(self, service: str):
r = self.session.query(AutzRule).filter(or_(AutzRule.service == service, AutzRule.service == "*")).all()

records = list()
for record in r:
records.append(AutzRuleDTO(record))
return records
116 changes: 109 additions & 7 deletions minos/api_gateway/rest/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from minos.api_gateway.rest.database.models import (
AuthRule,
AutzRule,
)
from minos.api_gateway.rest.urlmatch.authmatch import (
AuthMatch,
Expand All @@ -29,6 +30,9 @@
from .database.repository import (
Repository,
)
from .urlmatch.autzmatch import (
AutzMatch,
)

logger = logging.getLogger(__name__)

Expand All @@ -46,20 +50,44 @@ async def orchestrate(request: web.Request) -> web.Response:
auth = request.app["config"].rest.auth
user = None
if auth is not None and auth.enabled:
if await check_auth(request=request, service=request.url.parts[1], url=str(request.url), method=request.method):
if await check_authentication(
request=request, service=request.url.parts[1], url=str(request.url), method=request.method
):
response = await validate_token(request)
user = json.loads(response)
user = user["uuid"]

if await check_authorization(
request=request, service=request.url.parts[1], url=str(request.url), method=request.method
):
response = await validate_token(request)
data = json.loads(response)
user = data["uuid"]
role = data["role"]
if not await is_authorized_role(
request=request, role=role, service=request.url.parts[1], url=str(request.url), method=request.method
):
return web.HTTPUnauthorized()

microservice_response = await call(**discovery_data, original_req=request, user=user)
return microservice_response


async def check_auth(request: web.Request, service: str, url: str, method: str) -> bool:
records = Repository(request.app["db_engine"]).get_by_service(service)
async def check_authentication(request: web.Request, service: str, url: str, method: str) -> bool:
records = Repository(request.app["db_engine"]).get_auth_rule_by_service(service)
return AuthMatch.match(url=url, method=method, records=records)


async def check_authorization(request: web.Request, service: str, url: str, method: str) -> bool:
records = Repository(request.app["db_engine"]).get_autz_rule_by_service(service)
return AuthMatch.match(url=url, method=method, records=records)


async def is_authorized_role(request: web.Request, role: int, service: str, url: str, method: str) -> bool:
records = Repository(request.app["db_engine"]).get_autz_rule_by_service(service)
return AutzMatch.match(url=url, role=role, method=method, records=records)


async def authentication_default(request: web.Request) -> web.Response:
""" Orchestrate discovery and microservice call """
auth_host = request.app["config"].rest.auth.host
Expand Down Expand Up @@ -239,9 +267,26 @@ async def get_endpoints(request: web.Request) -> web.Response:
{"error": "The requested endpoint is not available."}, status=web.HTTPServiceUnavailable.status_code
)

@staticmethod
async def get_roles(request: web.Request) -> web.Response:
auth_host = request.app["config"].rest.auth.host
auth_port = request.app["config"].rest.auth.port
auth_path = request.app["config"].rest.auth.path

url = URL.build(scheme="http", host=auth_host, port=auth_port, path=f"{auth_path}/roles")

try:
async with ClientSession() as session:
async with session.get(url=url) as response:
return await _clone_response(response)
except ClientConnectorError:
return web.json_response(
{"error": "The requested endpoint is not available."}, status=web.HTTPServiceUnavailable.status_code
)

@staticmethod
async def get_rules(request: web.Request) -> web.Response:
records = Repository(request.app["db_engine"]).get_all()
records = Repository(request.app["db_engine"]).get_auth_rules()
return web.json_response(records)

@staticmethod
Expand All @@ -265,7 +310,7 @@ async def create_rule(request: web.Request) -> web.Response:
updated_at=now,
)

record = Repository(request.app["db_engine"]).create(rule)
record = Repository(request.app["db_engine"]).create_auth_rule(rule)

return web.json_response(record)
except Exception as e:
Expand All @@ -276,7 +321,17 @@ async def update_rule(request: web.Request) -> web.Response:
try:
id = int(request.url.name)
content = await request.json()
Repository(request.app["db_engine"]).update(id=id, **content)
Repository(request.app["db_engine"]).update_auth_rule(id=id, **content)
return web.json_response(status=web.HTTPOk.status_code)
except Exception as e:
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)

@staticmethod
async def update_autz_rule(request: web.Request) -> web.Response:
try:
id = int(request.url.name)
content = await request.json()
Repository(request.app["db_engine"]).update_autz_rule(id=id, **content)
return web.json_response(status=web.HTTPOk.status_code)
except Exception as e:
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)
Expand All @@ -285,7 +340,54 @@ async def update_rule(request: web.Request) -> web.Response:
async def delete_rule(request: web.Request) -> web.Response:
try:
id = int(request.url.name)
Repository(request.app["db_engine"]).delete(id)
Repository(request.app["db_engine"]).delete_auth_rule(id)
return web.json_response(status=web.HTTPOk.status_code)
except Exception as e:
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)

@staticmethod
async def delete_autz_rule(request: web.Request) -> web.Response:
try:
id = int(request.url.name)
Repository(request.app["db_engine"]).delete_autz_rule(id)
return web.json_response(status=web.HTTPOk.status_code)
except Exception as e:
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)

@staticmethod
async def create_autz_rule(request: web.Request) -> web.Response:
try:
content = await request.json()

if (
"service" not in content
and "rule" not in content
and "roles" not in content
and "methods" not in content
):
return web.json_response(
{"error": "Wrong data. Provide 'service', 'rule', 'roles' and 'methods' parameters."},
status=web.HTTPBadRequest.status_code,
)

now = datetime.now()

rule = AutzRule(
service=content["service"],
rule=content["rule"],
roles=content["roles"],
methods=content["methods"],
created_at=now,
updated_at=now,
)

record = Repository(request.app["db_engine"]).create_autz_rule(rule)

return web.json_response(record)
except Exception as e:
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)

@staticmethod
async def get_autz_rules(request: web.Request) -> web.Response:
records = Repository(request.app["db_engine"]).get_autz_rules()
return web.json_response(records)
10 changes: 8 additions & 2 deletions minos/api_gateway/rest/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ async def create_application(self) -> web.Application:
app.router.add_route("PATCH", "/admin/rules/{id}", AdminHandler.update_rule)
app.router.add_route("DELETE", "/admin/rules/{id}", AdminHandler.delete_rule)

app.router.add_route("GET", "/admin/roles", AdminHandler.get_roles)
app.router.add_route("POST", "/admin/autz-rules", AdminHandler.create_autz_rule)
app.router.add_route("GET", "/admin/autz-rules", AdminHandler.get_autz_rules)
app.router.add_route("PATCH", "/admin/autz-rules/{id}", AdminHandler.update_autz_rule)
app.router.add_route("DELETE", "/admin/autz-rules/{id}", AdminHandler.delete_autz_rule)

# Administration routes
path = Path(Path.cwd())
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(f"{path}/minos/api_gateway/rest/backend/templates"))
Expand All @@ -92,7 +98,7 @@ async def create_database(self):
Base.metadata.create_all(self.engine)

@aiohttp_jinja2.template("tmpl.jinja2")
async def handler(self, request):
async def handler(self, request): # pragma: no cover
try:
path = Path(Path.cwd())
self._directory = path.resolve()
Expand All @@ -108,7 +114,7 @@ async def handler(self, request):
return response

@staticmethod
async def _get_file(file_path) -> web.FileResponse:
async def _get_file(file_path) -> web.FileResponse: # pragma: no cover
try:
return web.FileResponse(path=file_path, status=200)
except (ValueError, FileNotFoundError) as error:
Expand Down
20 changes: 20 additions & 0 deletions minos/api_gateway/rest/urlmatch/autzmatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from ..database.models import (
AutzRuleDTO,
)
from .urlmatch import (
UrlMatch,
)


class AutzMatch(UrlMatch):
@staticmethod
def match(url: str, role: int, method: str, records: list[AutzRuleDTO]) -> bool:
for record in records:
if AutzMatch.urlmatch(record.rule, url):
if record.roles is None: # pragma: no cover
return True
else:
if role in record.roles or "*" in record.roles:
return True

return False
Loading