Skip to content

Commit

Permalink
Merge pull request #93 from minos-framework/issue-92-autz-rules
Browse files Browse the repository at this point in the history
#92 - Authorization
  • Loading branch information
vladyslav-fenchak authored Feb 16, 2022
2 parents 3276904 + 4e36827 commit 93b3c7e
Show file tree
Hide file tree
Showing 10 changed files with 523 additions and 16 deletions.

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

0 comments on commit 93b3c7e

Please sign in to comment.