mrok 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. mrok/__init__.py +6 -0
  2. mrok/agent/__init__.py +0 -0
  3. mrok/agent/sidecar/__init__.py +3 -0
  4. mrok/agent/sidecar/app.py +30 -0
  5. mrok/agent/sidecar/main.py +27 -0
  6. mrok/agent/ziticorn.py +29 -0
  7. mrok/cli/__init__.py +3 -0
  8. mrok/cli/commands/__init__.py +7 -0
  9. mrok/cli/commands/admin/__init__.py +12 -0
  10. mrok/cli/commands/admin/bootstrap.py +58 -0
  11. mrok/cli/commands/admin/list/__init__.py +8 -0
  12. mrok/cli/commands/admin/list/extensions.py +144 -0
  13. mrok/cli/commands/admin/list/instances.py +167 -0
  14. mrok/cli/commands/admin/register/__init__.py +8 -0
  15. mrok/cli/commands/admin/register/extensions.py +46 -0
  16. mrok/cli/commands/admin/register/instances.py +60 -0
  17. mrok/cli/commands/admin/unregister/__init__.py +8 -0
  18. mrok/cli/commands/admin/unregister/extensions.py +33 -0
  19. mrok/cli/commands/admin/unregister/instances.py +34 -0
  20. mrok/cli/commands/admin/utils.py +49 -0
  21. mrok/cli/commands/agent/__init__.py +6 -0
  22. mrok/cli/commands/agent/run/__init__.py +7 -0
  23. mrok/cli/commands/agent/run/asgi.py +49 -0
  24. mrok/cli/commands/agent/run/sidecar.py +54 -0
  25. mrok/cli/commands/controller/__init__.py +7 -0
  26. mrok/cli/commands/controller/openapi.py +47 -0
  27. mrok/cli/commands/controller/run.py +87 -0
  28. mrok/cli/main.py +97 -0
  29. mrok/cli/rich.py +18 -0
  30. mrok/conf.py +32 -0
  31. mrok/controller/__init__.py +0 -0
  32. mrok/controller/app.py +62 -0
  33. mrok/controller/auth.py +87 -0
  34. mrok/controller/dependencies/__init__.py +4 -0
  35. mrok/controller/dependencies/conf.py +7 -0
  36. mrok/controller/dependencies/ziti.py +27 -0
  37. mrok/controller/openapi/__init__.py +3 -0
  38. mrok/controller/openapi/examples.py +44 -0
  39. mrok/controller/openapi/utils.py +35 -0
  40. mrok/controller/pagination.py +79 -0
  41. mrok/controller/routes.py +294 -0
  42. mrok/controller/schemas.py +67 -0
  43. mrok/errors.py +2 -0
  44. mrok/http/__init__.py +0 -0
  45. mrok/http/config.py +65 -0
  46. mrok/http/forwarder.py +299 -0
  47. mrok/http/lifespan.py +10 -0
  48. mrok/http/master.py +90 -0
  49. mrok/http/protocol.py +11 -0
  50. mrok/http/server.py +14 -0
  51. mrok/logging.py +76 -0
  52. mrok/ziti/__init__.py +15 -0
  53. mrok/ziti/api.py +481 -0
  54. mrok/ziti/bootstrap.py +71 -0
  55. mrok/ziti/constants.py +9 -0
  56. mrok/ziti/errors.py +25 -0
  57. mrok/ziti/identities.py +169 -0
  58. mrok/ziti/pki.py +52 -0
  59. mrok/ziti/services.py +87 -0
  60. {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/METADATA +7 -9
  61. mrok-0.1.8.dist-info/RECORD +64 -0
  62. {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/WHEEL +1 -2
  63. mrok-0.1.6.dist-info/RECORD +0 -6
  64. mrok-0.1.6.dist-info/top_level.txt +0 -1
  65. {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/entry_points.txt +0 -0
  66. {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,34 @@
1
+ import asyncio
2
+ import re
3
+
4
+ import typer
5
+
6
+ from mrok.conf import Settings
7
+ from mrok.ziti.api import ZitiManagementAPI
8
+ from mrok.ziti.identities import unregister_instance
9
+
10
+ RE_EXTENSION_ID = re.compile(r"(?i)EXT-\d{4}-\d{4}")
11
+
12
+
13
+ async def do_unregister(settings: Settings, extension_id: str, instance_id: str):
14
+ async with ZitiManagementAPI(settings) as api:
15
+ await unregister_instance(api, extension_id, instance_id)
16
+
17
+
18
+ def validate_extension_id(extension_id: str):
19
+ if not RE_EXTENSION_ID.fullmatch(extension_id):
20
+ raise typer.BadParameter("ext_id must match EXT-xxxx-yyyy (case-insensitive)")
21
+ return extension_id
22
+
23
+
24
+ def register(app: typer.Typer) -> None:
25
+ @app.command("instance")
26
+ def unregister_instance(
27
+ ctx: typer.Context,
28
+ extension_id: str = typer.Argument(
29
+ ..., callback=validate_extension_id, help="Extension ID in format EXT-xxxx-yyyy"
30
+ ),
31
+ instance_id: str = typer.Argument(..., help="Instance ID"),
32
+ ):
33
+ """Register a new Extension Instance in OpenZiti (identity)."""
34
+ asyncio.run(do_unregister(ctx.obj, extension_id, instance_id))
@@ -0,0 +1,49 @@
1
+ from datetime import datetime
2
+
3
+ import typer
4
+
5
+ from mrok.ziti.api import TagsType
6
+
7
+
8
+ def parse_tags(pairs: list[str] | None) -> TagsType | None:
9
+ if not pairs:
10
+ return None
11
+
12
+ result: dict[str, str | bool | None] = {}
13
+ for item in pairs:
14
+ if "=" not in item:
15
+ raise typer.BadParameter(f"Invalid format {item!r}, expected key=value")
16
+ key, raw = item.split("=", 1)
17
+ raw_lower = raw.strip().lower()
18
+ if raw_lower in ("true", "false"):
19
+ val: str | bool | None = raw_lower == "true"
20
+ elif raw == "":
21
+ val = None
22
+ else:
23
+ val = raw
24
+ result[key.strip()] = val
25
+ return result
26
+
27
+
28
+ def tags_to_filter(tags: list[str]) -> str:
29
+ parsed_tags = parse_tags(tags)
30
+ return " and ".join([f'tags.{key}="{value}"' for key, value in parsed_tags.items()])
31
+
32
+
33
+ def format_timestamp(iso_timestamp: str) -> str:
34
+ dt = datetime.strptime(iso_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
35
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
36
+
37
+
38
+ def format_tags(tags: dict, delimiter: str = "\n") -> str:
39
+ if not tags:
40
+ return "-"
41
+
42
+ return f"{delimiter}".join(f"{k}: {v}" for k, v in tags.items())
43
+
44
+
45
+ def extract_names(data: list[dict], delimiter: str = "\n") -> str:
46
+ if not data:
47
+ return "-"
48
+
49
+ return f"{delimiter}".join(item["name"] for item in data if item.get("name"))
@@ -0,0 +1,6 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.agent.run import app as run_app
4
+
5
+ app = typer.Typer(help="mrok agent commands.")
6
+ app.add_typer(run_app)
@@ -0,0 +1,7 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.agent.run import asgi, sidecar
4
+
5
+ app = typer.Typer(name="run", help="Run mrok agent.")
6
+ asgi.register(app)
7
+ sidecar.register(app)
@@ -0,0 +1,49 @@
1
+ import multiprocessing
2
+ from pathlib import Path
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ # from app.logging import get_logging_config
8
+ from mrok.agent import ziticorn
9
+
10
+
11
+ def number_of_workers():
12
+ return (multiprocessing.cpu_count() * 2) + 1
13
+
14
+
15
+ default_workers = number_of_workers()
16
+
17
+
18
+ def register(app: typer.Typer) -> None:
19
+ @app.command("asgi")
20
+ def run_asgi(
21
+ app: str = typer.Argument(
22
+ ...,
23
+ help="ASGI application",
24
+ ),
25
+ identity_file: Path = typer.Argument(
26
+ ...,
27
+ help="Identity json file",
28
+ ),
29
+ workers: Annotated[
30
+ int,
31
+ typer.Option(
32
+ "--workers",
33
+ "-w",
34
+ help=f"Number of workers. Default: {default_workers}",
35
+ show_default=True,
36
+ ),
37
+ ] = default_workers,
38
+ reload: Annotated[
39
+ bool,
40
+ typer.Option(
41
+ "--reload",
42
+ "-r",
43
+ help="Enable auto-reload. Default: False",
44
+ show_default=True,
45
+ ),
46
+ ] = False,
47
+ ):
48
+ """Run an ASGI application exposing it through OpenZiti network."""
49
+ ziticorn.run(app, str(identity_file), workers=workers, reload=reload)
@@ -0,0 +1,54 @@
1
+ import multiprocessing
2
+ from pathlib import Path
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from mrok.agent import sidecar
8
+
9
+
10
+ def number_of_workers():
11
+ return (multiprocessing.cpu_count() * 2) + 1
12
+
13
+
14
+ default_workers = number_of_workers()
15
+
16
+
17
+ def register(app: typer.Typer) -> None:
18
+ @app.command("sidecar")
19
+ def run_sidecar(
20
+ identity_file: Path = typer.Argument(
21
+ ...,
22
+ help="Identity json file",
23
+ ),
24
+ target: Path = typer.Argument(
25
+ ...,
26
+ help="Target service (host:port or path to unix domain socket)",
27
+ ),
28
+ workers: Annotated[
29
+ int,
30
+ typer.Option(
31
+ "--workers",
32
+ "-w",
33
+ help=f"Number of workers. Default: {default_workers}",
34
+ show_default=True,
35
+ ),
36
+ ] = default_workers,
37
+ reload: Annotated[
38
+ bool,
39
+ typer.Option(
40
+ "--reload",
41
+ "-r",
42
+ help="Enable auto-reload. Default: False",
43
+ show_default=True,
44
+ ),
45
+ ] = False,
46
+ ):
47
+ """Run a Sidecar Proxy to expose a web application through OpenZiti."""
48
+ if ":" in str(target):
49
+ host, port = str(target).split(":", 1)
50
+ target_addr = (host or "127.0.0.1", int(port))
51
+ else:
52
+ target_addr = str(target) # type: ignore
53
+
54
+ sidecar.run(str(identity_file), target_addr, workers=workers, reload=reload)
@@ -0,0 +1,7 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.controller import openapi, run
4
+
5
+ app = typer.Typer(help="mrok controller commands.")
6
+ run.register(app)
7
+ openapi.register(app)
@@ -0,0 +1,47 @@
1
+ import json
2
+ from enum import Enum
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ import yaml
8
+
9
+ from mrok.controller.openapi import generate_openapi_spec
10
+
11
+
12
+ class OutputFormat(str, Enum):
13
+ json = "json"
14
+ yaml = "yaml"
15
+
16
+
17
+ def register(app: typer.Typer) -> None:
18
+ @app.command("openapi")
19
+ def generate_spec(
20
+ ctx: typer.Context,
21
+ output: Annotated[
22
+ Path | None,
23
+ typer.Option(
24
+ "--output",
25
+ "-o",
26
+ help="Output file",
27
+ ),
28
+ ] = Path("mrok_openapi_spec.yml"),
29
+ output_format: Annotated[
30
+ OutputFormat,
31
+ typer.Option(
32
+ "--output-format",
33
+ "-f",
34
+ help="Output file format",
35
+ ),
36
+ ] = OutputFormat.yaml,
37
+ ):
38
+ """
39
+ Generates the mrok controller OpenAPI spec file.
40
+ """
41
+ from mrok.controller.app import app
42
+
43
+ dump_fn = json.dump if output_format == OutputFormat.json else yaml.dump
44
+ spec = generate_openapi_spec(app, ctx.obj)
45
+
46
+ with open(output, "w") as f: # type: ignore
47
+ dump_fn(spec, f, indent=2)
@@ -0,0 +1,87 @@
1
+ import multiprocessing
2
+ from collections.abc import Callable
3
+ from typing import Annotated, Any
4
+
5
+ import typer
6
+ from gunicorn.app.base import BaseApplication
7
+
8
+ from mrok.controller.app import app as asgi_app
9
+ from mrok.logging import get_logging_config
10
+
11
+
12
+ def number_of_workers():
13
+ return (multiprocessing.cpu_count() * 2) + 1
14
+
15
+
16
+ class StandaloneApplication(BaseApplication): # pragma: no cover
17
+ def __init__(self, application: Callable, options: dict[str, Any] | None = None):
18
+ self.options = options or {}
19
+ self.application = application
20
+ super().__init__()
21
+
22
+ def load_config(self):
23
+ config = {
24
+ key: value
25
+ for key, value in self.options.items()
26
+ if key in self.cfg.settings and value is not None
27
+ }
28
+ for key, value in config.items():
29
+ self.cfg.set(key.lower(), value)
30
+
31
+ def load(self):
32
+ return self.application
33
+
34
+
35
+ default_workers = number_of_workers()
36
+
37
+
38
+ def register(app: typer.Typer) -> None:
39
+ @app.command("run")
40
+ def run_controller(
41
+ ctx: typer.Context,
42
+ host: Annotated[
43
+ str,
44
+ typer.Option(
45
+ "--host",
46
+ "-h",
47
+ help="Host to bind to. Default: 127.0.0.1",
48
+ show_default=True,
49
+ ),
50
+ ] = "127.0.0.1",
51
+ port: Annotated[
52
+ int,
53
+ typer.Option(
54
+ "--port",
55
+ "-p",
56
+ help="Port to bind to. Default: 8000",
57
+ show_default=True,
58
+ ),
59
+ ] = 8000,
60
+ workers: Annotated[
61
+ int,
62
+ typer.Option(
63
+ "--workers",
64
+ "-w",
65
+ help=f"Number of workers. Default: {default_workers}",
66
+ show_default=True,
67
+ ),
68
+ ] = default_workers,
69
+ dev: Annotated[
70
+ bool,
71
+ typer.Option(
72
+ "--reload",
73
+ "-r",
74
+ help="Enable auto-reload. Default: False",
75
+ show_default=True,
76
+ ),
77
+ ] = False,
78
+ ):
79
+ """Run the mrok controller with Gunicorn and Uvicorn workers."""
80
+ options = {
81
+ "bind": f"{host}:{port}",
82
+ "workers": workers,
83
+ "worker_class": "uvicorn_worker.UvicornWorker",
84
+ "logconfig_dict": get_logging_config(ctx.obj),
85
+ "reload": dev,
86
+ }
87
+ StandaloneApplication(asgi_app, options).run()
mrok/cli/main.py ADDED
@@ -0,0 +1,97 @@
1
+ import inspect
2
+ import sys
3
+
4
+ import typer
5
+ from pyfiglet import Figlet
6
+ from rich.text import Text
7
+ from typer.core import TyperGroup
8
+
9
+ from mrok.cli import commands
10
+ from mrok.cli.rich import get_console
11
+ from mrok.conf import get_settings
12
+ from mrok.logging import setup_logging
13
+
14
+
15
+ def gradient(start_hex, end_hex, num_samples=10):
16
+ start_rgb = tuple(int(start_hex[i : i + 2], 16) for i in range(1, 6, 2))
17
+ end_rgb = tuple(int(end_hex[i : i + 2], 16) for i in range(1, 6, 2))
18
+ gradient_colors = [start_hex]
19
+ for sample in range(1, num_samples):
20
+ red = int(start_rgb[0] + (float(sample) / (num_samples - 1)) * (end_rgb[0] - start_rgb[0]))
21
+ green = int(
22
+ start_rgb[1] + (float(sample) / (num_samples - 1)) * (end_rgb[1] - start_rgb[1])
23
+ )
24
+ blue = int(start_rgb[2] + (float(sample) / (num_samples - 1)) * (end_rgb[2] - start_rgb[2]))
25
+ gradient_colors.append(f"#{red:02X}{green:02X}{blue:02X}")
26
+
27
+ return gradient_colors
28
+
29
+
30
+ def show_banner():
31
+ program_name = "mrok"
32
+ figlet = Figlet("georgia11")
33
+
34
+ banner_text = figlet.renderText(program_name)
35
+
36
+ banner_lines = [Text(line) for line in banner_text.splitlines()]
37
+ max_line_length = max(len(line) for line in banner_lines)
38
+ half_length = max_line_length // 2
39
+
40
+ colors = gradient("#00C9CD", "#472AFF", half_length) + gradient(
41
+ "#472AFF", "#392D9C", half_length + 1
42
+ )
43
+ console = get_console()
44
+
45
+ for line in banner_lines:
46
+ colored_line = Text()
47
+ for i in range(len(line)):
48
+ char = line[i : i + 1]
49
+ char.stylize(colors[i])
50
+ colored_line = Text.assemble(colored_line, char)
51
+ console.print(colored_line)
52
+
53
+
54
+ class MrokGroup(TyperGroup):
55
+ def get_help(self, ctx):
56
+ show_banner()
57
+ return super().get_help(ctx)
58
+
59
+ def invoke(self, ctx):
60
+ show_banner()
61
+ return super().invoke(ctx)
62
+
63
+
64
+ app = typer.Typer(
65
+ help="SoftwareOne Marketplace Extension Channel CLI",
66
+ add_completion=True,
67
+ rich_markup_mode="rich",
68
+ cls=MrokGroup,
69
+ )
70
+
71
+ err_console = get_console(stderr=True)
72
+
73
+ for name, module in inspect.getmembers(commands):
74
+ if not inspect.ismodule(module):
75
+ continue
76
+
77
+ if hasattr(module, "command"): # pragma: no branch
78
+ app.command(name=name.replace("_", "-"))(module.command)
79
+ elif hasattr(module, "app"): # pragma: no branch
80
+ app.add_typer(module.app, name=name.replace("_", "-"))
81
+
82
+
83
+ @app.callback()
84
+ def main(
85
+ ctx: typer.Context,
86
+ ):
87
+ settings = get_settings()
88
+ setup_logging(settings, cli_mode=True)
89
+ ctx.obj = settings
90
+
91
+
92
+ def run():
93
+ try:
94
+ app()
95
+ except Exception as e:
96
+ err_console.print(f"[bold red]Error:[/bold red] {e}")
97
+ sys.exit(-1)
mrok/cli/rich.py ADDED
@@ -0,0 +1,18 @@
1
+ from rich.console import Console
2
+ from rich.highlighter import ReprHighlighter
3
+ from rich.theme import Theme
4
+
5
+
6
+ class MrokHighlighter(ReprHighlighter):
7
+ prefixes = ("EXT", "ext", "INS", "ins")
8
+ highlights = ReprHighlighter.highlights + [
9
+ rf"(?P<mrok_id>(?:{'|'.join(prefixes)})(?:-\d{{4}})*)"
10
+ ]
11
+
12
+
13
+ def get_console(stderr: bool = False) -> Console:
14
+ return Console(
15
+ stderr=stderr,
16
+ highlighter=MrokHighlighter(),
17
+ theme=Theme({"repr.mrok_id": "bold light_salmon3"}),
18
+ )
mrok/conf.py ADDED
@@ -0,0 +1,32 @@
1
+ from dynaconf import Dynaconf, LazySettings
2
+
3
+ type Settings = LazySettings
4
+
5
+ DEFAULT_SETTINGS = {
6
+ "LOGGING": {
7
+ "debug": False,
8
+ "rich": False,
9
+ },
10
+ "PROXY": {
11
+ "identity": "public",
12
+ "mode": "zrok",
13
+ },
14
+ "ZITI": {
15
+ "ssl_verify": False,
16
+ },
17
+ "PAGINATION": {"limit": 50},
18
+ }
19
+
20
+ _settings = None
21
+
22
+
23
+ def get_settings() -> Settings:
24
+ global _settings
25
+ if not _settings:
26
+ _settings = Dynaconf(
27
+ envvar_prefix="MROK",
28
+ settings_files=["settings.yaml", ".secrets.yaml"],
29
+ merge_enabled=True,
30
+ )
31
+ _settings.configure(**DEFAULT_SETTINGS)
32
+ return _settings
File without changes
mrok/controller/app.py ADDED
@@ -0,0 +1,62 @@
1
+ import logging
2
+ from functools import partial
3
+
4
+ import fastapi_pagination
5
+ from fastapi import Depends, FastAPI
6
+ from fastapi.routing import APIRoute, APIRouter
7
+
8
+ from mrok.conf import get_settings
9
+ from mrok.controller.auth import authenticate
10
+ from mrok.controller.openapi import generate_openapi_spec
11
+ from mrok.controller.routes import router as extensions_router
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ tags_metadata = [
17
+ {
18
+ "name": "Extensions",
19
+ "description": "Manage Extensions (services).",
20
+ },
21
+ {
22
+ "name": "Instances",
23
+ "description": "Manage Extension Instances (identities).",
24
+ },
25
+ ]
26
+
27
+
28
+ def setup_custom_serialization(router: APIRouter):
29
+ for api_route in router.routes:
30
+ if (
31
+ isinstance(api_route, APIRoute)
32
+ and hasattr(api_route, "response_model")
33
+ and api_route.response_model
34
+ ):
35
+ api_route.response_model_exclude_none = True
36
+
37
+
38
+ def setup_app():
39
+ app = FastAPI(
40
+ title="mrok Controller API",
41
+ description="API to orchestrate OpenZiti for Extensions.",
42
+ swagger_ui_parameters={"showExtensions": False, "showCommonExtensions": False},
43
+ openapi_tags=tags_metadata,
44
+ version="5.0.0",
45
+ root_path="/public/v1",
46
+ )
47
+ fastapi_pagination.add_pagination(app)
48
+
49
+ setup_custom_serialization(extensions_router)
50
+
51
+ # TODO: Add healthcheck
52
+ app.include_router(
53
+ extensions_router, prefix="/extensions", dependencies=[Depends(authenticate)]
54
+ )
55
+
56
+ settings = get_settings()
57
+
58
+ app.openapi = partial(generate_openapi_spec, app, settings)
59
+ return app
60
+
61
+
62
+ app = setup_app()
@@ -0,0 +1,87 @@
1
+ import logging
2
+ from typing import Annotated
3
+
4
+ import httpx
5
+ import jwt
6
+ from fastapi import Depends, HTTPException, Request, status
7
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
8
+
9
+ from mrok.controller.dependencies.conf import AppSettings
10
+
11
+ logger = logging.getLogger("mrok.controller")
12
+
13
+ UNAUTHORIZED_EXCEPTION = HTTPException(
14
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized."
15
+ )
16
+
17
+
18
+ class JWTCredentials(HTTPAuthorizationCredentials):
19
+ pass
20
+
21
+
22
+ class JWTBearer(HTTPBearer):
23
+ def __init__(self):
24
+ super().__init__(auto_error=False)
25
+
26
+ async def __call__(self, request: Request) -> JWTCredentials:
27
+ credentials = await super().__call__(request)
28
+ if not credentials:
29
+ raise UNAUTHORIZED_EXCEPTION
30
+ try:
31
+ return JWTCredentials(
32
+ scheme=credentials.scheme,
33
+ credentials=credentials.credentials,
34
+ )
35
+ except jwt.InvalidTokenError:
36
+ raise UNAUTHORIZED_EXCEPTION
37
+
38
+
39
+ async def authenticate(
40
+ settings: AppSettings,
41
+ credentials: Annotated[JWTCredentials, Depends(JWTBearer())],
42
+ ):
43
+ async with httpx.AsyncClient(
44
+ timeout=httpx.Timeout(
45
+ connect=0.25,
46
+ read=settings.auth.read_timeout,
47
+ write=2.0,
48
+ pool=5.0,
49
+ ),
50
+ ) as client:
51
+ try:
52
+ config_resp = await client.get(settings.auth.openid_config_url)
53
+ config_resp.raise_for_status()
54
+ config = config_resp.json()
55
+ issuer = config["issuer"]
56
+ jwks_uri = config["jwks_uri"]
57
+
58
+ jwks_resp = await client.get(jwks_uri)
59
+ jwks_resp.raise_for_status()
60
+ jwks = jwks_resp.json()
61
+
62
+ header = jwt.get_unverified_header(credentials.credentials)
63
+ kid = header["kid"]
64
+
65
+ key_data = next((k for k in jwks["keys"] if k["kid"] == kid), None)
66
+ except Exception:
67
+ logger.exception("Error fetching openid-config/jwks")
68
+ raise UNAUTHORIZED_EXCEPTION
69
+ if key_data is None:
70
+ logger.error("Key ID not found in JWKS")
71
+ raise UNAUTHORIZED_EXCEPTION
72
+
73
+ try:
74
+ payload = jwt.decode(
75
+ credentials.credentials,
76
+ jwt.PyJWK(key_data),
77
+ algorithms=[header["alg"]],
78
+ issuer=issuer,
79
+ audience=settings.auth.audience,
80
+ )
81
+ return payload
82
+ except jwt.InvalidKeyError as e:
83
+ logger.error(f"Invalid jwt token: {e}")
84
+ raise UNAUTHORIZED_EXCEPTION
85
+ except jwt.InvalidTokenError as e:
86
+ logger.error(f"Invalid jwt token: {e}")
87
+ raise UNAUTHORIZED_EXCEPTION
@@ -0,0 +1,4 @@
1
+ from mrok.controller.dependencies.conf import AppSettings
2
+ from mrok.controller.dependencies.ziti import ZitiClientAPI, ZitiManagementAPI
3
+
4
+ __all__ = ["AppSettings", "ZitiClientAPI", "ZitiManagementAPI"]
@@ -0,0 +1,7 @@
1
+ from typing import Annotated
2
+
3
+ from fastapi import Depends
4
+
5
+ from mrok.conf import Settings, get_settings
6
+
7
+ AppSettings = Annotated[Settings, Depends(get_settings)]
@@ -0,0 +1,27 @@
1
+ from collections.abc import AsyncGenerator
2
+ from typing import Annotated
3
+
4
+ from fastapi import Depends
5
+
6
+ from mrok.controller.dependencies.conf import AppSettings
7
+ from mrok.ziti import api
8
+
9
+
10
+ class APIClientFactory[T: api.BaseZitiAPI]:
11
+ def __init__(self, client_cls: type[T]):
12
+ self.client_cls = client_cls
13
+
14
+ async def __call__(self, settings: AppSettings) -> AsyncGenerator[T]:
15
+ client = self.client_cls(settings)
16
+ async with client:
17
+ yield client
18
+
19
+
20
+ ZitiManagementAPI = Annotated[
21
+ api.ZitiManagementAPI,
22
+ Depends(APIClientFactory(api.ZitiManagementAPI)),
23
+ ]
24
+ ZitiClientAPI = Annotated[
25
+ api.ZitiClientAPI,
26
+ Depends(APIClientFactory(api.ZitiClientAPI)),
27
+ ]