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.
- mrok/__init__.py +6 -0
- mrok/agent/__init__.py +0 -0
- mrok/agent/sidecar/__init__.py +3 -0
- mrok/agent/sidecar/app.py +30 -0
- mrok/agent/sidecar/main.py +27 -0
- mrok/agent/ziticorn.py +29 -0
- mrok/cli/__init__.py +3 -0
- mrok/cli/commands/__init__.py +7 -0
- mrok/cli/commands/admin/__init__.py +12 -0
- mrok/cli/commands/admin/bootstrap.py +58 -0
- mrok/cli/commands/admin/list/__init__.py +8 -0
- mrok/cli/commands/admin/list/extensions.py +144 -0
- mrok/cli/commands/admin/list/instances.py +167 -0
- mrok/cli/commands/admin/register/__init__.py +8 -0
- mrok/cli/commands/admin/register/extensions.py +46 -0
- mrok/cli/commands/admin/register/instances.py +60 -0
- mrok/cli/commands/admin/unregister/__init__.py +8 -0
- mrok/cli/commands/admin/unregister/extensions.py +33 -0
- mrok/cli/commands/admin/unregister/instances.py +34 -0
- mrok/cli/commands/admin/utils.py +49 -0
- mrok/cli/commands/agent/__init__.py +6 -0
- mrok/cli/commands/agent/run/__init__.py +7 -0
- mrok/cli/commands/agent/run/asgi.py +49 -0
- mrok/cli/commands/agent/run/sidecar.py +54 -0
- mrok/cli/commands/controller/__init__.py +7 -0
- mrok/cli/commands/controller/openapi.py +47 -0
- mrok/cli/commands/controller/run.py +87 -0
- mrok/cli/main.py +97 -0
- mrok/cli/rich.py +18 -0
- mrok/conf.py +32 -0
- mrok/controller/__init__.py +0 -0
- mrok/controller/app.py +62 -0
- mrok/controller/auth.py +87 -0
- mrok/controller/dependencies/__init__.py +4 -0
- mrok/controller/dependencies/conf.py +7 -0
- mrok/controller/dependencies/ziti.py +27 -0
- mrok/controller/openapi/__init__.py +3 -0
- mrok/controller/openapi/examples.py +44 -0
- mrok/controller/openapi/utils.py +35 -0
- mrok/controller/pagination.py +79 -0
- mrok/controller/routes.py +294 -0
- mrok/controller/schemas.py +67 -0
- mrok/errors.py +2 -0
- mrok/http/__init__.py +0 -0
- mrok/http/config.py +65 -0
- mrok/http/forwarder.py +299 -0
- mrok/http/lifespan.py +10 -0
- mrok/http/master.py +90 -0
- mrok/http/protocol.py +11 -0
- mrok/http/server.py +14 -0
- mrok/logging.py +76 -0
- mrok/ziti/__init__.py +15 -0
- mrok/ziti/api.py +481 -0
- mrok/ziti/bootstrap.py +71 -0
- mrok/ziti/constants.py +9 -0
- mrok/ziti/errors.py +25 -0
- mrok/ziti/identities.py +169 -0
- mrok/ziti/pki.py +52 -0
- mrok/ziti/services.py +87 -0
- {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/METADATA +7 -9
- mrok-0.1.8.dist-info/RECORD +64 -0
- {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/WHEEL +1 -2
- mrok-0.1.6.dist-info/RECORD +0 -6
- mrok-0.1.6.dist-info/top_level.txt +0 -1
- {mrok-0.1.6.dist-info → mrok-0.1.8.dist-info}/entry_points.txt +0 -0
- {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,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,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()
|
mrok/controller/auth.py
ADDED
|
@@ -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,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
|
+
]
|