From d450e93f198e3c8435eef8eabfd6f944e126035a Mon Sep 17 00:00:00 2001 From: Pim Witlox Date: Tue, 10 Dec 2024 10:41:26 +0100 Subject: [PATCH] rewire api calls --- horao/__init__.py | 6 +++- horao/api/__init__.py | 2 +- horao/api/authenticate.py | 2 ++ horao/api/synchronization.py | 18 +----------- horao/persistance/store.py | 54 +++++++++++++++++++++++++++++++++++- pyproject.toml | 2 +- tests/basic_auth.py | 16 +++++++++-- tests/test_api.py | 4 +-- 8 files changed, 78 insertions(+), 26 deletions(-) diff --git a/horao/__init__.py b/horao/__init__.py index 8556dd6..0b8d116 100644 --- a/horao/__init__.py +++ b/horao/__init__.py @@ -138,7 +138,6 @@ def init(authorization: Optional[AuthenticationBackend] = None) -> Starlette: if cors == "*": logger.warning("CORS is set to *") routes = [ - Route("/ping", endpoint=horao.api.alive_controller.is_alive, methods=["GET"]), Route("/login", endpoint=horao.api.authenticate.login, methods=["POST"]), Route("/logout", endpoint=horao.api.authenticate.logout, methods=["POST"]), Route( @@ -146,6 +145,11 @@ def init(authorization: Optional[AuthenticationBackend] = None) -> Starlette: endpoint=horao.api.synchronization.synchronize, methods=["POST"], ), + Route( + "/reservations", + endpoint=horao.api.user_actions.get_reservations, + methods=["GET"], + ), Route("/openapi.json", endpoint=openapi_schema, include_in_schema=False), ] if os.getenv("UI", "False") == "True": diff --git a/horao/api/__init__.py b/horao/api/__init__.py index 5f3b9ce..23a6f7e 100644 --- a/horao/api/__init__.py +++ b/horao/api/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*-# """API definitions.""" -from .alive_controller import is_alive from .authenticate import login, logout from .synchronization import synchronize +from .user_actions import get_reservations diff --git a/horao/api/authenticate.py b/horao/api/authenticate.py index 90e62e6..4dfee2a 100644 --- a/horao/api/authenticate.py +++ b/horao/api/authenticate.py @@ -2,6 +2,7 @@ import os from authlib.integrations.starlette_client import OAuth # type: ignore +from starlette.authentication import requires from starlette.requests import Request from starlette.responses import RedirectResponse @@ -38,6 +39,7 @@ async def login(request: Request): return await client.authorize_redirect(request, redirect_uri) +@requires("authenticated") async def logout(request: Request): """ responses: diff --git a/horao/api/synchronization.py b/horao/api/synchronization.py index 09f6d76..5b03821 100644 --- a/horao/api/synchronization.py +++ b/horao/api/synchronization.py @@ -47,23 +47,7 @@ async def synchronize(request: Request) -> JSONResponse: return JSONResponse(status_code=400, content={"error": "Error parsing request"}) try: session = init_session() - for k, v in logical_infrastructure.infrastructure.items(): - local_dc = await session.async_load(k.name) - if not local_dc: - await session.async_save(k.name, k) - else: - local_dc.merge(k) - local_dc_content = await session.async_load(f"{k.name}.content") - if not local_dc_content: - await session.async_save(f"{k.name}.content", v) - else: - local_dc_content.merge(v) - if logical_infrastructure.claims: - for k, v in logical_infrastructure.claims.items(): - await session.async_save(k.name, k) - if logical_infrastructure.constraints: - for k, v in logical_infrastructure.contraints.items(): - await session.async_save(k.name, k) + session.save_logical_infrastructure(logical_infrastructure) except Exception as e: logging.error(f"Error synchronizing: {e}") if os.getenv("DEBUG", "False") == "True": diff --git a/horao/persistance/store.py b/horao/persistance/store.py index 1fca482..f5e269c 100644 --- a/horao/persistance/store.py +++ b/horao/persistance/store.py @@ -4,10 +4,11 @@ import logging from typing import Any, Dict, Optional -from redis.asyncio import Redis as RedisAIO from redis import Redis as Redis +from redis.asyncio import Redis as RedisAIO from horao.conceptual.decorators import instrument_class_function +from horao.logical.infrastructure import LogicalInfrastructure from horao.persistance.serialize import HoraoDecoder, HoraoEncoder @@ -52,6 +53,57 @@ async def items(self) -> Dict[str, Any] | Any: return await self.redis_aio.items() return self.memory.items() + async def load_logical_infrastructure(self) -> LogicalInfrastructure: + """ + Load the logical infrastructure from the store + :return: LogicalInfrastructure + """ + infrastructure = {} + claims = {} + constraints = {} + for key in await self.keys(): + if key.startswith("datacenter-"): + dc = await self.async_load(key) + content = await self.async_load(f"datacenter-{key}.content") + infrastructure[dc] = content + elif key.startswith("claim-"): + claim = await self.async_load(key) + content = await self.async_load(f"claim-{key}.content") + claims[claim] = content + elif key.startswith("constraint-"): + constraint = await self.async_load(key) + content = await self.async_load(f"constraint-{key}.content") + constraints[constraint] = content + return LogicalInfrastructure(infrastructure, constraints, claims) + + async def save_logical_infrastructure( + self, logical_infrastructure: LogicalInfrastructure + ) -> None: + """ + Save the logical infrastructure to the store + :param logical_infrastructure: infrastructure to save + :return: None + """ + for k, v in logical_infrastructure.infrastructure.items(): + local_dc = await self.async_load(k.name) + if not local_dc: + await self.async_save(f"datacenter-{k.name}", k) + else: + local_dc.merge(k) + local_dc_content = await self.async_load(f"datacenter-{k.name}.content") + if not local_dc_content: + await self.async_save(f"datacenter-{k.name}.content", v) + else: + local_dc_content.merge(v) + if logical_infrastructure.claims: + for k, v in logical_infrastructure.claims.items(): # type: ignore + await self.async_save(f"claim-{k.name}", k) + await self.async_save(f"claim-{k.name}.content", v) + if logical_infrastructure.constraints: + for k, v in logical_infrastructure.constraints.items(): # type: ignore + await self.async_save(f"constraint-{k.name}", k) + await self.async_save(f"constraint-{k.name}.content", v) + @instrument_class_function(name="async_load", level=logging.DEBUG) async def async_load(self, key: str) -> Any | None: """ diff --git a/pyproject.toml b/pyproject.toml index 8594112..72dfb1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ redis = "^5.1.1" authlib = "^1.3.2" [tool.poetry.scripts] -horao = 'horao.__main__:main' +horao = 'horao.main:main' [tool.poetry.group.dev.dependencies] black = "^24.10.0" diff --git a/tests/basic_auth.py b/tests/basic_auth.py index c719b31..ea96f2b 100644 --- a/tests/basic_auth.py +++ b/tests/basic_auth.py @@ -18,9 +18,16 @@ from starlette.requests import HTTPException, Request # type: ignore from starlette.responses import JSONResponse # type: ignore +from horao.auth.roles import Administrator, TenantController, User +from horao.conceptual.tenant import Tenant + basic_auth_structure = { - "netadm": {"password": "secret", "role": "network.admin"}, - "sysadm": {"password": "secret", "role": "system.admin"}, + "read_usr": {"password": "secret1", "role": User("read_usr")}, + "tenant": { + "password": "secret2", + "role": TenantController("tenant", [Tenant("test", "owner")]), + }, + "admin": {"password": "secret3", "role": Administrator("admin")}, } @@ -45,7 +52,10 @@ async def authenticate(self, conn): or basic_auth_structure[username]["password"] != password ): raise AuthenticationError(f"access not allowed for {username}") - return AuthCredentials(["authenticated"]), SimpleUser(username) + return ( + AuthCredentials(["authenticated"]), + basic_auth_structure[username]["role"], + ) def basic_auth(username, password) -> str: diff --git a/tests/test_api.py b/tests/test_api.py index c53a74c..cacb342 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -16,7 +16,7 @@ def test_ping_service_unauthorized(): os.environ["TELEMETRY"] = "OFF" ia = init(BasicAuthBackend()) with TestClient(ia) as client: - lg = client.get("/ping") + lg = client.get("/reservations") assert 403 == lg.status_code @@ -25,7 +25,7 @@ def test_ping_service_authorized(): ia = init(BasicAuthBackend()) with TestClient(ia) as client: lg = client.get( - "/ping", headers={"Authorization": basic_auth("netadm", "secret")} + "/reservations", headers={"Authorization": basic_auth("tenant", "secret2")} ) assert 200 == lg.status_code