mrok 0.7.0__py3-none-any.whl → 0.8.0__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/agent/devtools/inspector/__main__.py +3 -1
- mrok/cli/commands/admin/bootstrap.py +2 -2
- mrok/cli/commands/admin/register/extensions.py +7 -9
- mrok/cli/commands/admin/register/instances.py +13 -16
- mrok/cli/commands/admin/unregister/extensions.py +7 -11
- mrok/cli/commands/admin/unregister/instances.py +12 -12
- mrok/cli/commands/agent/run/asgi.py +1 -1
- mrok/cli/commands/frontend/run.py +1 -1
- mrok/cli/utils.py +26 -0
- mrok/conf.py +15 -7
- mrok/controller/app.py +12 -10
- mrok/controller/auth/__init__.py +11 -0
- mrok/controller/auth/backends.py +60 -0
- mrok/controller/auth/base.py +38 -0
- mrok/controller/auth/manager.py +31 -0
- mrok/controller/auth/registry.py +17 -0
- mrok/frontend/app.py +94 -26
- mrok/frontend/main.py +8 -5
- mrok/frontend/middleware.py +35 -0
- mrok/frontend/utils.py +83 -0
- mrok/logging.py +24 -0
- mrok/proxy/app.py +13 -5
- mrok/ziti/api.py +3 -3
- mrok/ziti/identities.py +1 -1
- mrok/ziti/services.py +6 -6
- {mrok-0.7.0.dist-info → mrok-0.8.0.dist-info}/METADATA +2 -2
- {mrok-0.7.0.dist-info → mrok-0.8.0.dist-info}/RECORD +30 -24
- mrok/controller/auth.py +0 -87
- {mrok-0.7.0.dist-info → mrok-0.8.0.dist-info}/WHEEL +0 -0
- {mrok-0.7.0.dist-info → mrok-0.8.0.dist-info}/entry_points.txt +0 -0
- {mrok-0.7.0.dist-info → mrok-0.8.0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -5,8 +5,8 @@ import typer
|
|
|
5
5
|
from rich import print
|
|
6
6
|
|
|
7
7
|
from mrok.cli.commands.admin.utils import parse_tags
|
|
8
|
-
from mrok.
|
|
9
|
-
from mrok.
|
|
8
|
+
from mrok.cli.utils import validate_extension_id
|
|
9
|
+
from mrok.conf import Settings, get_settings
|
|
10
10
|
from mrok.ziti.api import ZitiManagementAPI
|
|
11
11
|
from mrok.ziti.services import register_service
|
|
12
12
|
|
|
@@ -16,18 +16,16 @@ async def do_register(settings: Settings, extension_id: str, tags: list[str] | N
|
|
|
16
16
|
await register_service(settings, api, extension_id, tags=parse_tags(tags))
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def validate_extension_id(extension_id: str) -> str:
|
|
20
|
-
if not RE_EXTENSION_ID.fullmatch(extension_id):
|
|
21
|
-
raise typer.BadParameter("it must match EXT-xxxx-yyyy (case-insensitive)")
|
|
22
|
-
return extension_id
|
|
23
|
-
|
|
24
|
-
|
|
25
19
|
def register(app: typer.Typer) -> None:
|
|
20
|
+
settings = get_settings()
|
|
21
|
+
|
|
26
22
|
@app.command("extension")
|
|
27
23
|
def register_extension(
|
|
28
24
|
ctx: typer.Context,
|
|
29
25
|
extension_id: str = typer.Argument(
|
|
30
|
-
...,
|
|
26
|
+
...,
|
|
27
|
+
callback=validate_extension_id,
|
|
28
|
+
help=f"Extension ID in the format {settings.identifiers.extension.format}",
|
|
31
29
|
),
|
|
32
30
|
tags: Annotated[
|
|
33
31
|
list[str] | None,
|
|
@@ -6,8 +6,11 @@ from typing import Annotated
|
|
|
6
6
|
import typer
|
|
7
7
|
|
|
8
8
|
from mrok.cli.commands.admin.utils import parse_tags
|
|
9
|
-
from mrok.
|
|
10
|
-
|
|
9
|
+
from mrok.cli.utils import (
|
|
10
|
+
validate_extension_id,
|
|
11
|
+
validate_instance_id,
|
|
12
|
+
)
|
|
13
|
+
from mrok.conf import Settings, get_settings
|
|
11
14
|
from mrok.ziti.api import ZitiClientAPI, ZitiManagementAPI
|
|
12
15
|
from mrok.ziti.identities import register_identity
|
|
13
16
|
|
|
@@ -21,27 +24,21 @@ async def do_register(
|
|
|
21
24
|
)
|
|
22
25
|
|
|
23
26
|
|
|
24
|
-
def validate_extension_id(extension_id: str):
|
|
25
|
-
if not RE_EXTENSION_ID.fullmatch(extension_id):
|
|
26
|
-
raise typer.BadParameter("it must match EXT-xxxx-yyyy (case-insensitive)")
|
|
27
|
-
return extension_id
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def validate_instance_id(instance_id: str):
|
|
31
|
-
if not RE_INSTANCE_ID.fullmatch(instance_id):
|
|
32
|
-
raise typer.BadParameter("it must match INS-xxxx-yyyy-zzzz (case-insensitive)")
|
|
33
|
-
return instance_id
|
|
34
|
-
|
|
35
|
-
|
|
36
27
|
def register(app: typer.Typer) -> None:
|
|
28
|
+
settings = get_settings()
|
|
29
|
+
|
|
37
30
|
@app.command("instance")
|
|
38
31
|
def register_instance(
|
|
39
32
|
ctx: typer.Context,
|
|
40
33
|
extension_id: str = typer.Argument(
|
|
41
|
-
...,
|
|
34
|
+
...,
|
|
35
|
+
callback=validate_extension_id,
|
|
36
|
+
help=f"Extension ID in the format {settings.identifiers.extension.format}",
|
|
42
37
|
),
|
|
43
38
|
instance_id: str = typer.Argument(
|
|
44
|
-
...,
|
|
39
|
+
...,
|
|
40
|
+
callback=validate_instance_id,
|
|
41
|
+
help=f"Instance ID in the format {settings.identifiers.instance.format}",
|
|
45
42
|
),
|
|
46
43
|
output: Path = typer.Argument(
|
|
47
44
|
...,
|
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import re
|
|
3
2
|
|
|
4
3
|
import typer
|
|
5
4
|
|
|
6
|
-
from mrok.
|
|
5
|
+
from mrok.cli.utils import validate_extension_id
|
|
6
|
+
from mrok.conf import Settings, get_settings
|
|
7
7
|
from mrok.ziti.api import ZitiManagementAPI
|
|
8
8
|
from mrok.ziti.services import unregister_service
|
|
9
9
|
|
|
10
|
-
RE_EXTENSION_ID = re.compile(r"(?i)EXT-\d{4}-\d{4}")
|
|
11
|
-
|
|
12
10
|
|
|
13
11
|
async def do_unregister(settings: Settings, extension_id: str):
|
|
14
12
|
async with ZitiManagementAPI(settings) as api:
|
|
15
13
|
await unregister_service(settings, api, extension_id)
|
|
16
14
|
|
|
17
15
|
|
|
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
16
|
def register(app: typer.Typer) -> None:
|
|
17
|
+
settings = get_settings()
|
|
18
|
+
|
|
25
19
|
@app.command("extension")
|
|
26
20
|
def unregister_extension(
|
|
27
21
|
ctx: typer.Context,
|
|
28
22
|
extension_id: str = typer.Argument(
|
|
29
|
-
...,
|
|
23
|
+
...,
|
|
24
|
+
callback=validate_extension_id,
|
|
25
|
+
help=f"Extension ID in the format {settings.identifiers.extension.format}",
|
|
30
26
|
),
|
|
31
27
|
):
|
|
32
28
|
"""Unregister a new Extension in OpenZiti (service)."""
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import re
|
|
3
2
|
|
|
4
3
|
import typer
|
|
5
4
|
|
|
6
|
-
from mrok.
|
|
5
|
+
from mrok.cli.utils import validate_extension_id, validate_instance_id
|
|
6
|
+
from mrok.conf import Settings, get_settings
|
|
7
7
|
from mrok.ziti.api import ZitiManagementAPI
|
|
8
8
|
from mrok.ziti.identities import unregister_identity
|
|
9
9
|
|
|
10
|
-
RE_EXTENSION_ID = re.compile(r"(?i)EXT-\d{4}-\d{4}")
|
|
11
|
-
|
|
12
10
|
|
|
13
11
|
async def do_unregister(settings: Settings, extension_id: str, instance_id: str):
|
|
14
12
|
async with ZitiManagementAPI(settings) as api:
|
|
15
13
|
await unregister_identity(settings, api, extension_id, instance_id)
|
|
16
14
|
|
|
17
15
|
|
|
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
16
|
def register(app: typer.Typer) -> None:
|
|
17
|
+
settings = get_settings()
|
|
18
|
+
|
|
25
19
|
@app.command("instance")
|
|
26
20
|
def unregister_instance(
|
|
27
21
|
ctx: typer.Context,
|
|
28
22
|
extension_id: str = typer.Argument(
|
|
29
|
-
...,
|
|
23
|
+
...,
|
|
24
|
+
callback=validate_extension_id,
|
|
25
|
+
help=f"Extension ID in the format {settings.identifiers.extension.format}",
|
|
26
|
+
),
|
|
27
|
+
instance_id: str = typer.Argument(
|
|
28
|
+
...,
|
|
29
|
+
callback=validate_instance_id,
|
|
30
|
+
help=f"Instance ID in the format {settings.identifiers.instance.format}",
|
|
30
31
|
),
|
|
31
|
-
instance_id: str = typer.Argument(..., help="Instance ID"),
|
|
32
32
|
):
|
|
33
33
|
"""Register a new Extension Instance in OpenZiti (identity)."""
|
|
34
34
|
asyncio.run(do_unregister(ctx.obj, extension_id, instance_id))
|
|
@@ -12,8 +12,8 @@ default_workers = number_of_workers()
|
|
|
12
12
|
def register(app: typer.Typer) -> None:
|
|
13
13
|
@app.command("asgi")
|
|
14
14
|
def run_asgi(
|
|
15
|
-
app: Annotated[str, typer.Argument(..., help="ASGI application")],
|
|
16
15
|
identity_file: Annotated[Path, typer.Argument(..., help="Identity json file")],
|
|
16
|
+
app: Annotated[str, typer.Argument(..., help="ASGI application")],
|
|
17
17
|
workers: Annotated[
|
|
18
18
|
int,
|
|
19
19
|
typer.Option(
|
mrok/cli/utils.py
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import multiprocessing
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from mrok.conf import get_settings
|
|
2
7
|
|
|
3
8
|
|
|
4
9
|
def number_of_workers() -> int:
|
|
5
10
|
return (multiprocessing.cpu_count() * 2) + 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_identifier(regex_exp: str, format: str, identifier: str) -> str:
|
|
14
|
+
match = re.fullmatch(regex_exp, identifier)
|
|
15
|
+
if not match:
|
|
16
|
+
raise typer.BadParameter(f"it must match {format}")
|
|
17
|
+
return identifier
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_extension_id(extension_id: str) -> str:
|
|
21
|
+
settings = get_settings()
|
|
22
|
+
return validate_identifier(
|
|
23
|
+
settings.identifiers.extension.regex, settings.identifiers.extension.format, extension_id
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_instance_id(instance_id: str) -> str:
|
|
28
|
+
settings = get_settings()
|
|
29
|
+
return validate_identifier(
|
|
30
|
+
settings.identifiers.instance.regex, settings.identifiers.instance.format, instance_id
|
|
31
|
+
)
|
mrok/conf.py
CHANGED
|
@@ -7,19 +7,27 @@ DEFAULT_SETTINGS = {
|
|
|
7
7
|
"debug": False,
|
|
8
8
|
"rich": False,
|
|
9
9
|
},
|
|
10
|
-
"
|
|
10
|
+
"FRONTEND": {
|
|
11
11
|
"identity": "public",
|
|
12
12
|
"mode": "zrok",
|
|
13
13
|
},
|
|
14
14
|
"ZITI": {
|
|
15
15
|
"ssl_verify": False,
|
|
16
16
|
},
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
17
|
+
"CONTROLLER": {
|
|
18
|
+
"pagination": {"limit": 50},
|
|
19
|
+
},
|
|
20
|
+
"IDENTIFIERS": {
|
|
21
|
+
"extension": {
|
|
22
|
+
"regex": "(?i)EXT-\\d{4}-\\d{4}",
|
|
23
|
+
"format": "EXT-xxxx-yyyy",
|
|
24
|
+
"example": "EXT-2000-1000",
|
|
25
|
+
},
|
|
26
|
+
"instance": {
|
|
27
|
+
"regex": "(?i)INS-\\d{4}-\\d{4}-\\d{4}",
|
|
28
|
+
"format": "INS-xxxx-yyyy-zzzz",
|
|
29
|
+
"example": "INS-2004-2000-3000",
|
|
30
|
+
},
|
|
23
31
|
},
|
|
24
32
|
}
|
|
25
33
|
|
mrok/controller/app.py
CHANGED
|
@@ -5,8 +5,8 @@ import fastapi_pagination
|
|
|
5
5
|
from fastapi import Depends, FastAPI
|
|
6
6
|
from fastapi.routing import APIRoute, APIRouter
|
|
7
7
|
|
|
8
|
-
from mrok.conf import get_settings
|
|
9
|
-
from mrok.controller.auth import
|
|
8
|
+
from mrok.conf import Settings, get_settings
|
|
9
|
+
from mrok.controller.auth import HTTPAuthManager
|
|
10
10
|
from mrok.controller.openapi import generate_openapi_spec
|
|
11
11
|
from mrok.controller.routes.extensions import router as extensions_router
|
|
12
12
|
from mrok.controller.routes.instances import router as instances_router
|
|
@@ -36,7 +36,8 @@ def setup_custom_serialization(router: APIRouter):
|
|
|
36
36
|
api_route.response_model_exclude_none = True
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def setup_app():
|
|
39
|
+
def setup_app(settings: Settings):
|
|
40
|
+
auth_manager = HTTPAuthManager(settings.controller.auth)
|
|
40
41
|
app = FastAPI(
|
|
41
42
|
title="mrok Controller API",
|
|
42
43
|
description="API to orchestrate OpenZiti for Extensions.",
|
|
@@ -49,22 +50,23 @@ def setup_app():
|
|
|
49
50
|
|
|
50
51
|
setup_custom_serialization(extensions_router)
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
@app.get("/healthcheck")
|
|
54
|
+
async def healthcheck():
|
|
55
|
+
return {"status": "healthy"}
|
|
56
|
+
|
|
53
57
|
app.include_router(
|
|
54
58
|
extensions_router,
|
|
55
59
|
prefix="/extensions",
|
|
56
|
-
dependencies=[Depends(
|
|
60
|
+
dependencies=[Depends(auth_manager)],
|
|
57
61
|
)
|
|
58
62
|
app.include_router(
|
|
59
63
|
instances_router,
|
|
60
64
|
prefix="/instances",
|
|
61
|
-
dependencies=[Depends(
|
|
65
|
+
dependencies=[Depends(auth_manager)],
|
|
62
66
|
)
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
app.openapi = partial(generate_openapi_spec, app, settings)
|
|
68
|
+
app.openapi = partial(generate_openapi_spec, app, settings) # type: ignore[method-assign]
|
|
67
69
|
return app
|
|
68
70
|
|
|
69
71
|
|
|
70
|
-
app = setup_app()
|
|
72
|
+
app = setup_app(get_settings())
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from mrok.controller.auth.backends import OIDCJWTAuthenticationBackend # noqa: F401
|
|
2
|
+
from mrok.controller.auth.base import AuthIdentity, BaseHTTPAuthBackend
|
|
3
|
+
from mrok.controller.auth.manager import HTTPAuthManager
|
|
4
|
+
from mrok.controller.auth.registry import register_authentication_backend
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AuthIdentity",
|
|
8
|
+
"BaseHTTPAuthBackend",
|
|
9
|
+
"HTTPAuthManager",
|
|
10
|
+
"register_authentication_backend",
|
|
11
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import jwt
|
|
5
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
6
|
+
from fastapi.security.http import HTTPBase
|
|
7
|
+
|
|
8
|
+
from mrok.controller.auth.base import UNAUTHORIZED_EXCEPTION, AuthIdentity, BaseHTTPAuthBackend
|
|
9
|
+
from mrok.controller.auth.registry import register_authentication_backend
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("mrok.controller")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@register_authentication_backend("oidc")
|
|
15
|
+
class OIDCJWTAuthenticationBackend(BaseHTTPAuthBackend):
|
|
16
|
+
def init_scheme(self) -> HTTPBase:
|
|
17
|
+
return HTTPBearer(auto_error=False)
|
|
18
|
+
|
|
19
|
+
async def authenticate(self, credentials: HTTPAuthorizationCredentials) -> AuthIdentity | None:
|
|
20
|
+
async with httpx.AsyncClient() as client:
|
|
21
|
+
try:
|
|
22
|
+
config_resp = await client.get(self.config.config_url)
|
|
23
|
+
config_resp.raise_for_status()
|
|
24
|
+
config = config_resp.json()
|
|
25
|
+
issuer = config["issuer"]
|
|
26
|
+
jwks_uri = config["jwks_uri"]
|
|
27
|
+
|
|
28
|
+
jwks_resp = await client.get(jwks_uri)
|
|
29
|
+
jwks_resp.raise_for_status()
|
|
30
|
+
jwks = jwks_resp.json()
|
|
31
|
+
|
|
32
|
+
header = jwt.get_unverified_header(credentials.credentials)
|
|
33
|
+
kid = header["kid"]
|
|
34
|
+
|
|
35
|
+
key_data = next((k for k in jwks["keys"] if k["kid"] == kid), None)
|
|
36
|
+
except Exception:
|
|
37
|
+
logger.exception("Error fetching openid-config/jwks")
|
|
38
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
39
|
+
if key_data is None:
|
|
40
|
+
logger.error("Key ID not found in JWKS")
|
|
41
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
payload = jwt.decode(
|
|
45
|
+
credentials.credentials,
|
|
46
|
+
jwt.PyJWK(key_data),
|
|
47
|
+
algorithms=[header["alg"]],
|
|
48
|
+
issuer=issuer,
|
|
49
|
+
audience=self.config.audience,
|
|
50
|
+
)
|
|
51
|
+
return AuthIdentity(
|
|
52
|
+
subject=payload["sub"],
|
|
53
|
+
metadata=payload,
|
|
54
|
+
)
|
|
55
|
+
except jwt.InvalidKeyError as e:
|
|
56
|
+
logger.error(f"Invalid jwt token: {e} ({credentials.credentials})")
|
|
57
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
58
|
+
except jwt.InvalidTokenError as e:
|
|
59
|
+
logger.error(f"Invalid jwt token: {e} ({credentials.credentials})")
|
|
60
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from dynaconf.utils.boxing import DynaBox
|
|
5
|
+
from fastapi import HTTPException, Request, status
|
|
6
|
+
from fastapi.security import HTTPAuthorizationCredentials
|
|
7
|
+
from fastapi.security.http import HTTPBase
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
UNAUTHORIZED_EXCEPTION = HTTPException(
|
|
11
|
+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized."
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthIdentity(BaseModel):
|
|
16
|
+
subject: str
|
|
17
|
+
scopes: list[str] = []
|
|
18
|
+
metadata: dict[str, Any] = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseHTTPAuthBackend(ABC):
|
|
22
|
+
def __init__(self, config: DynaBox):
|
|
23
|
+
self.config = config
|
|
24
|
+
self.scheme = self.init_scheme()
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def init_scheme(self) -> HTTPBase:
|
|
28
|
+
raise NotImplementedError()
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def authenticate(self, credentials: HTTPAuthorizationCredentials) -> AuthIdentity | None:
|
|
32
|
+
raise NotImplementedError()
|
|
33
|
+
|
|
34
|
+
async def __call__(self, request: Request) -> AuthIdentity | None:
|
|
35
|
+
credentials = await self.scheme(request)
|
|
36
|
+
if not credentials:
|
|
37
|
+
return None
|
|
38
|
+
return await self.authenticate(credentials)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from dynaconf.utils.boxing import DynaBox
|
|
2
|
+
from fastapi import Request
|
|
3
|
+
|
|
4
|
+
from mrok.controller.auth.base import UNAUTHORIZED_EXCEPTION, AuthIdentity, BaseHTTPAuthBackend
|
|
5
|
+
from mrok.controller.auth.registry import get_authentication_backend
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HTTPAuthManager:
|
|
9
|
+
def __init__(self, auth_settings: DynaBox):
|
|
10
|
+
self.auth_settings = auth_settings
|
|
11
|
+
self.active_backends: list[BaseHTTPAuthBackend] = []
|
|
12
|
+
self._setup_backends()
|
|
13
|
+
|
|
14
|
+
def _setup_backends(self):
|
|
15
|
+
enabled_keys = self.auth_settings.get("backends", [])
|
|
16
|
+
|
|
17
|
+
for key in enabled_keys:
|
|
18
|
+
backend_cls = get_authentication_backend(key)
|
|
19
|
+
if not backend_cls:
|
|
20
|
+
raise ValueError(f"Backend '{key}' is not registered.")
|
|
21
|
+
|
|
22
|
+
specific_config = self.auth_settings.get(key, {})
|
|
23
|
+
self.active_backends.append(backend_cls(specific_config))
|
|
24
|
+
|
|
25
|
+
async def __call__(self, request: Request) -> AuthIdentity:
|
|
26
|
+
for backend in self.active_backends:
|
|
27
|
+
identity = await backend(request)
|
|
28
|
+
if identity:
|
|
29
|
+
return identity
|
|
30
|
+
|
|
31
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from mrok.controller.auth.base import BaseHTTPAuthBackend
|
|
2
|
+
|
|
3
|
+
BACKEND_REGISTRY: dict[str, type[BaseHTTPAuthBackend]] = {}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_authentication_backend(name: str):
|
|
7
|
+
"""Decorator to register a backend class with a unique key."""
|
|
8
|
+
|
|
9
|
+
def decorator(cls: type[BaseHTTPAuthBackend]):
|
|
10
|
+
BACKEND_REGISTRY[name] = cls
|
|
11
|
+
return cls
|
|
12
|
+
|
|
13
|
+
return decorator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_authentication_backend(name: str) -> type[BaseHTTPAuthBackend] | None:
|
|
17
|
+
return BACKEND_REGISTRY.get(name)
|
mrok/frontend/app.py
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
2
4
|
|
|
3
5
|
from httpcore import AsyncConnectionPool
|
|
6
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
4
7
|
|
|
5
8
|
from mrok.conf import get_settings
|
|
9
|
+
from mrok.frontend.utils import get_target_name, parse_accept_header
|
|
6
10
|
from mrok.proxy.app import ProxyAppBase
|
|
7
11
|
from mrok.proxy.backend import AIOZitiNetworkBackend
|
|
8
12
|
from mrok.proxy.exceptions import InvalidTargetError
|
|
9
|
-
from mrok.types.proxy import Scope
|
|
13
|
+
from mrok.types.proxy import ASGISend, Scope
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
ERROR_TEMPLATE_FORMATS = {
|
|
16
|
+
"application/json": "json",
|
|
17
|
+
"text/html": "html",
|
|
18
|
+
}
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
class FrontendProxyApp(ProxyAppBase):
|
|
@@ -19,10 +26,11 @@ class FrontendProxyApp(ProxyAppBase):
|
|
|
19
26
|
max_connections: int | None = 10,
|
|
20
27
|
max_keepalive_connections: int | None = None,
|
|
21
28
|
keepalive_expiry: float | None = None,
|
|
22
|
-
retries=0,
|
|
29
|
+
retries: int = 0,
|
|
23
30
|
):
|
|
24
31
|
self._identity_file = identity_file
|
|
25
|
-
self.
|
|
32
|
+
self._jinja_env_cache: dict[Path, Environment] = {}
|
|
33
|
+
self._templates_by_error = get_settings().frontend.get("errors", {})
|
|
26
34
|
super().__init__(
|
|
27
35
|
max_connections=max_connections,
|
|
28
36
|
max_keepalive_connections=max_keepalive_connections,
|
|
@@ -46,30 +54,90 @@ class FrontendProxyApp(ProxyAppBase):
|
|
|
46
54
|
)
|
|
47
55
|
|
|
48
56
|
def get_upstream_base_url(self, scope: Scope) -> str:
|
|
49
|
-
target =
|
|
57
|
+
target = get_target_name(
|
|
50
58
|
{k.decode("latin1"): v.decode("latin1") for k, v in scope.get("headers", {})}
|
|
51
59
|
)
|
|
60
|
+
if not target:
|
|
61
|
+
raise InvalidTargetError()
|
|
62
|
+
|
|
52
63
|
return f"http://{target.lower()}"
|
|
53
64
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
async def send_error_response(
|
|
66
|
+
self,
|
|
67
|
+
scope: Scope,
|
|
68
|
+
send: ASGISend,
|
|
69
|
+
http_status: int,
|
|
70
|
+
body: str,
|
|
71
|
+
headers: list[tuple[bytes, bytes]] | None = None,
|
|
72
|
+
):
|
|
73
|
+
request_headers = {
|
|
74
|
+
k.decode("latin1"): v.decode("latin1") for k, v in scope.get("headers", {})
|
|
75
|
+
}
|
|
76
|
+
accept_header = request_headers.get("accept")
|
|
77
|
+
if not (accept_header and str(http_status) in self._templates_by_error):
|
|
78
|
+
return await super().send_error_response(scope, send, http_status, body)
|
|
61
79
|
|
|
62
|
-
|
|
63
|
-
header_value = headers.get(name, "")
|
|
64
|
-
if self._proxy_domain in header_value:
|
|
65
|
-
if ":" in header_value:
|
|
66
|
-
header_value, _ = header_value.split(":", 1)
|
|
67
|
-
return header_value[: -len(self._proxy_domain)]
|
|
80
|
+
available_templates = self._templates_by_error[str(http_status)]
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
media_types = parse_accept_header(accept_header)
|
|
83
|
+
for media_type in media_types:
|
|
84
|
+
template_format = ERROR_TEMPLATE_FORMATS.get(media_type)
|
|
85
|
+
if template_format and template_format in available_templates:
|
|
86
|
+
template_path = available_templates[template_format]
|
|
87
|
+
rendered = await self._render_error_template(
|
|
88
|
+
Path(template_path), scope, http_status, body
|
|
89
|
+
)
|
|
90
|
+
return await super().send_error_response(
|
|
91
|
+
scope,
|
|
92
|
+
send,
|
|
93
|
+
http_status,
|
|
94
|
+
rendered,
|
|
95
|
+
headers=[(b"content-type", media_type.encode("latin-1"))],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return await super().send_error_response(scope, send, http_status, body)
|
|
99
|
+
|
|
100
|
+
async def _render_error_template(
|
|
101
|
+
self, template_path: Path, scope: Scope, http_status: int, body: str
|
|
102
|
+
) -> str:
|
|
103
|
+
env = self._get_jinja_env(template_path)
|
|
104
|
+
template = env.get_template(template_path.name)
|
|
105
|
+
status_title = HTTPStatus(http_status).name.replace("_", " ").title()
|
|
106
|
+
context = {
|
|
107
|
+
"status": http_status,
|
|
108
|
+
"status_title": status_title,
|
|
109
|
+
"body": body,
|
|
110
|
+
"request": self._extract_request_context(scope),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return await template.render_async(context)
|
|
114
|
+
|
|
115
|
+
def _extract_request_context(self, scope: Scope) -> dict[str, Any]:
|
|
116
|
+
headers = {k.decode("latin-1"): v.decode("latin-1") for k, v in scope.get("headers", [])}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"method": scope.get("method"),
|
|
120
|
+
"path": scope.get("path"),
|
|
121
|
+
"raw_path": scope.get("raw_path", b"").decode("latin-1"),
|
|
122
|
+
"query_string": scope.get("query_string", b"").decode("latin-1"),
|
|
123
|
+
"scheme": scope.get("scheme"),
|
|
124
|
+
"headers": headers,
|
|
125
|
+
"client": scope.get("client"),
|
|
126
|
+
"server": scope.get("server"),
|
|
127
|
+
"http_version": scope.get("http_version"),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def _get_jinja_env(self, template_path: Path) -> Environment:
|
|
131
|
+
template_dir = template_path.parent
|
|
132
|
+
|
|
133
|
+
if template_dir not in self._jinja_env_cache:
|
|
134
|
+
self._jinja_env_cache[template_dir] = Environment(
|
|
135
|
+
loader=FileSystemLoader(str(template_dir)),
|
|
136
|
+
autoescape=select_autoescape(
|
|
137
|
+
enabled_extensions=("html", "xml"),
|
|
138
|
+
default_for_string=False,
|
|
139
|
+
),
|
|
140
|
+
enable_async=True,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return self._jinja_env_cache[template_dir]
|
mrok/frontend/main.py
CHANGED
|
@@ -7,6 +7,7 @@ from uvicorn_worker import UvicornWorker
|
|
|
7
7
|
|
|
8
8
|
from mrok.conf import get_settings
|
|
9
9
|
from mrok.frontend.app import FrontendProxyApp
|
|
10
|
+
from mrok.frontend.middleware import HealthCheckMiddleware
|
|
10
11
|
from mrok.logging import get_logging_config
|
|
11
12
|
|
|
12
13
|
|
|
@@ -42,11 +43,13 @@ def run(
|
|
|
42
43
|
max_keepalive_connections: int | None,
|
|
43
44
|
keepalive_expiry: float | None,
|
|
44
45
|
):
|
|
45
|
-
app =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
app = HealthCheckMiddleware(
|
|
47
|
+
FrontendProxyApp(
|
|
48
|
+
str(identity_file),
|
|
49
|
+
max_connections=max_connections,
|
|
50
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
51
|
+
keepalive_expiry=keepalive_expiry,
|
|
52
|
+
)
|
|
50
53
|
)
|
|
51
54
|
|
|
52
55
|
options = {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from mrok.frontend.utils import get_target_name
|
|
4
|
+
from mrok.types.proxy import ASGIApp, ASGIReceive, ASGISend, Scope
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HealthCheckMiddleware:
|
|
8
|
+
def __init__(self, app: ASGIApp):
|
|
9
|
+
self.app = app
|
|
10
|
+
|
|
11
|
+
async def __call__(self, scope: Scope, receive: ASGIReceive, send: ASGISend):
|
|
12
|
+
if scope["type"] == "http" and scope["path"] == "/healthcheck":
|
|
13
|
+
target = get_target_name(
|
|
14
|
+
{k.decode("latin1"): v.decode("latin1") for k, v in scope.get("headers", {})}
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if not target:
|
|
18
|
+
await send(
|
|
19
|
+
{
|
|
20
|
+
"type": "http.response.start",
|
|
21
|
+
"status": 200,
|
|
22
|
+
"headers": [
|
|
23
|
+
[b"content-type", b"application/json"],
|
|
24
|
+
],
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
await send(
|
|
28
|
+
{
|
|
29
|
+
"type": "http.response.body",
|
|
30
|
+
"body": json.dumps({"status": "healthy"}).encode("utf-8"),
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
await self.app(scope, receive, send)
|
mrok/frontend/utils.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from mrok.conf import get_settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_accept_header(accept: str | None) -> list[str]:
|
|
7
|
+
if not accept:
|
|
8
|
+
return ["*/*"]
|
|
9
|
+
|
|
10
|
+
result: list[tuple[str, float, int]] = []
|
|
11
|
+
|
|
12
|
+
for index, item in enumerate(accept.split(",")):
|
|
13
|
+
item = item.strip()
|
|
14
|
+
if not item:
|
|
15
|
+
continue
|
|
16
|
+
|
|
17
|
+
parts = [p.strip() for p in item.split(";")]
|
|
18
|
+
media_type = parts[0].lower()
|
|
19
|
+
|
|
20
|
+
q = 1.0
|
|
21
|
+
for param in parts[1:]:
|
|
22
|
+
if param.startswith("q="):
|
|
23
|
+
try:
|
|
24
|
+
q = float(param[2:])
|
|
25
|
+
except ValueError:
|
|
26
|
+
q = 0.0
|
|
27
|
+
|
|
28
|
+
result.append((media_type, q, index))
|
|
29
|
+
|
|
30
|
+
# Sort by:
|
|
31
|
+
# 1) q value (desc)
|
|
32
|
+
# 2) specificity (more specific first)
|
|
33
|
+
# 3) original order (stable)
|
|
34
|
+
result.sort(
|
|
35
|
+
key=lambda x: (
|
|
36
|
+
-x[1],
|
|
37
|
+
-_media_type_specificity(x[0]),
|
|
38
|
+
x[2],
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return [media_type for media_type, _, _ in result]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _media_type_specificity(media_type: str) -> int:
|
|
46
|
+
if media_type == "*/*":
|
|
47
|
+
return 0
|
|
48
|
+
if media_type.endswith("/*"):
|
|
49
|
+
return 1
|
|
50
|
+
return 2
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_frontend_domain():
|
|
54
|
+
settings = get_settings()
|
|
55
|
+
return (
|
|
56
|
+
settings.frontend.domain
|
|
57
|
+
if settings.frontend.domain[0] == "."
|
|
58
|
+
else f".{settings.frontend.domain}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get_target_from_header(headers: dict[str, str], name: str) -> str | None:
|
|
63
|
+
domain_name = get_frontend_domain()
|
|
64
|
+
header_value = headers.get(name, "")
|
|
65
|
+
if domain_name in header_value:
|
|
66
|
+
if ":" in header_value:
|
|
67
|
+
header_value, _ = header_value.split(":", 1)
|
|
68
|
+
return header_value[: -len(domain_name)]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_target_name(headers: dict[str, str]) -> str | None:
|
|
72
|
+
settings = get_settings()
|
|
73
|
+
|
|
74
|
+
target = _get_target_from_header(headers, "x-forwarded-host")
|
|
75
|
+
if not target:
|
|
76
|
+
target = _get_target_from_header(headers, "host")
|
|
77
|
+
|
|
78
|
+
if target and (
|
|
79
|
+
re.fullmatch(settings.identifiers.extension.regex, target)
|
|
80
|
+
or re.fullmatch(settings.identifiers.instance.regex, target)
|
|
81
|
+
):
|
|
82
|
+
return target
|
|
83
|
+
return None
|
mrok/logging.py
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import logging.config
|
|
2
3
|
|
|
3
4
|
from mrok.conf import Settings
|
|
4
5
|
|
|
5
6
|
|
|
7
|
+
class HealthCheckFilter(logging.Filter):
|
|
8
|
+
def filter(self, record):
|
|
9
|
+
return "/healthcheck" not in record.getMessage()
|
|
10
|
+
|
|
11
|
+
|
|
6
12
|
def get_logging_config(settings: Settings, cli_mode: bool = False) -> dict:
|
|
7
13
|
log_level = "DEBUG" if settings.logging.debug else "INFO"
|
|
8
14
|
handler = "rich" if settings.logging.rich else "console"
|
|
@@ -26,6 +32,11 @@ def get_logging_config(settings: Settings, cli_mode: bool = False) -> dict:
|
|
|
26
32
|
},
|
|
27
33
|
"plain": {"format": "%(message)s"},
|
|
28
34
|
},
|
|
35
|
+
"filters": {
|
|
36
|
+
"healthcheck_filter": {
|
|
37
|
+
"()": HealthCheckFilter,
|
|
38
|
+
}
|
|
39
|
+
},
|
|
29
40
|
"handlers": {
|
|
30
41
|
"console": {
|
|
31
42
|
"class": "logging.StreamHandler",
|
|
@@ -54,17 +65,30 @@ def get_logging_config(settings: Settings, cli_mode: bool = False) -> dict:
|
|
|
54
65
|
"handlers": [handler],
|
|
55
66
|
"level": log_level,
|
|
56
67
|
"propagate": False,
|
|
68
|
+
"filters": ["healthcheck_filter"],
|
|
57
69
|
},
|
|
58
70
|
"gunicorn.error": {
|
|
59
71
|
"handlers": [handler],
|
|
60
72
|
"level": log_level,
|
|
61
73
|
"propagate": False,
|
|
62
74
|
},
|
|
75
|
+
"uvicorn.access": {
|
|
76
|
+
"handlers": [handler],
|
|
77
|
+
"level": log_level,
|
|
78
|
+
"propagate": False,
|
|
79
|
+
"filters": ["healthcheck_filter"],
|
|
80
|
+
},
|
|
63
81
|
"mrok": {
|
|
64
82
|
"handlers": [mrok_handler],
|
|
65
83
|
"level": log_level,
|
|
66
84
|
"propagate": False,
|
|
67
85
|
},
|
|
86
|
+
"mrok.access": {
|
|
87
|
+
"handlers": [mrok_handler],
|
|
88
|
+
"level": log_level,
|
|
89
|
+
"propagate": False,
|
|
90
|
+
"filters": ["healthcheck_filter"],
|
|
91
|
+
},
|
|
68
92
|
},
|
|
69
93
|
}
|
|
70
94
|
|
mrok/proxy/app.py
CHANGED
|
@@ -57,7 +57,7 @@ class ProxyAppBase(abc.ABC):
|
|
|
57
57
|
return
|
|
58
58
|
|
|
59
59
|
if scope.get("type") != "http":
|
|
60
|
-
await self.
|
|
60
|
+
await self.send_error_response(scope, send, 500, "Unsupported")
|
|
61
61
|
return
|
|
62
62
|
|
|
63
63
|
try:
|
|
@@ -105,15 +105,23 @@ class ProxyAppBase(abc.ABC):
|
|
|
105
105
|
await response.aclose()
|
|
106
106
|
|
|
107
107
|
except ProxyError as pe:
|
|
108
|
-
await self.
|
|
108
|
+
await self.send_error_response(scope, send, pe.http_status, pe.message)
|
|
109
109
|
|
|
110
110
|
except Exception:
|
|
111
111
|
logger.exception("Unexpected error in forwarder")
|
|
112
|
-
await self.
|
|
112
|
+
await self.send_error_response(scope, send, 502, "Bad Gateway")
|
|
113
113
|
|
|
114
|
-
async def
|
|
114
|
+
async def send_error_response(
|
|
115
|
+
self,
|
|
116
|
+
scope: Scope,
|
|
117
|
+
send: ASGISend,
|
|
118
|
+
http_status: int,
|
|
119
|
+
body: str,
|
|
120
|
+
headers: list[tuple[bytes, bytes]] | None = None,
|
|
121
|
+
):
|
|
122
|
+
headers = headers or [(b"content-type", b"text/plain")]
|
|
115
123
|
try:
|
|
116
|
-
await send({"type": "http.response.start", "status": http_status, "headers":
|
|
124
|
+
await send({"type": "http.response.start", "status": http_status, "headers": headers})
|
|
117
125
|
await send({"type": "http.response.body", "body": body.encode()})
|
|
118
126
|
except Exception as e: # pragma: no cover
|
|
119
127
|
logger.error(f"Cannot send error response: {e}")
|
mrok/ziti/api.py
CHANGED
|
@@ -38,7 +38,7 @@ class ZitiBadRequestError(ZitiAPIError):
|
|
|
38
38
|
class BaseZitiAPI(ABC):
|
|
39
39
|
def __init__(self, settings: Settings):
|
|
40
40
|
self.settings = settings
|
|
41
|
-
self.limit = self.settings.pagination.limit
|
|
41
|
+
self.limit = self.settings.controller.pagination.limit
|
|
42
42
|
self.token = None
|
|
43
43
|
|
|
44
44
|
@property
|
|
@@ -263,7 +263,7 @@ class ZitiIdentityAuth(BaseZitiAuth):
|
|
|
263
263
|
class ZitiManagementAPI(BaseZitiAPI):
|
|
264
264
|
@property
|
|
265
265
|
def base_url(self):
|
|
266
|
-
return f"{self.settings.ziti.
|
|
266
|
+
return f"{self.settings.ziti.base_urls.management}/edge/management/v1"
|
|
267
267
|
|
|
268
268
|
def services(
|
|
269
269
|
self,
|
|
@@ -465,7 +465,7 @@ class ZitiManagementAPI(BaseZitiAPI):
|
|
|
465
465
|
class ZitiClientAPI(BaseZitiAPI):
|
|
466
466
|
@property
|
|
467
467
|
def base_url(self):
|
|
468
|
-
return f"{self.settings.ziti.
|
|
468
|
+
return f"{self.settings.ziti.base_urls.client}/edge/client/v1"
|
|
469
469
|
|
|
470
470
|
async def enroll_identity(self, jti: str, csr_pem: str) -> dict[str, Any]:
|
|
471
471
|
response = await self.httpx_client.post(
|
mrok/ziti/identities.py
CHANGED
mrok/ziti/services.py
CHANGED
|
@@ -19,15 +19,15 @@ async def register_service(
|
|
|
19
19
|
) -> dict[str, Any]:
|
|
20
20
|
service_name = external_id.lower()
|
|
21
21
|
registered = False
|
|
22
|
-
proxy_identity = await mgmt_api.search_identity(settings.
|
|
22
|
+
proxy_identity = await mgmt_api.search_identity(settings.frontend.identity)
|
|
23
23
|
if not proxy_identity:
|
|
24
24
|
raise ProxyIdentityNotFoundError(
|
|
25
|
-
f"Identity for proxy `{settings.
|
|
25
|
+
f"Identity for proxy `{settings.frontend.identity}` not found.",
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
config_type = await mgmt_api.search_config_type(f"{settings.
|
|
28
|
+
config_type = await mgmt_api.search_config_type(f"{settings.frontend.mode}.proxy.v1")
|
|
29
29
|
if not config_type:
|
|
30
|
-
raise ConfigTypeNotFoundError(f"Config type `{settings.
|
|
30
|
+
raise ConfigTypeNotFoundError(f"Config type `{settings.frontend.mode}.proxy.v1` not found.")
|
|
31
31
|
|
|
32
32
|
config = await mgmt_api.search_config(service_name)
|
|
33
33
|
if not config:
|
|
@@ -43,7 +43,7 @@ async def register_service(
|
|
|
43
43
|
else:
|
|
44
44
|
service_id = service["id"]
|
|
45
45
|
proxy_identity_id = proxy_identity["id"]
|
|
46
|
-
service_policy_name = f"{service_name}:{settings.
|
|
46
|
+
service_policy_name = f"{service_name}:{settings.frontend.identity}:dial"
|
|
47
47
|
dial_service_policy = await mgmt_api.search_service_policy(service_policy_name)
|
|
48
48
|
if not dial_service_policy:
|
|
49
49
|
await mgmt_api.create_dial_service_policy(
|
|
@@ -75,7 +75,7 @@ async def unregister_service(
|
|
|
75
75
|
if router_policy:
|
|
76
76
|
await mgmt_api.delete_service_router_policy(router_policy["id"])
|
|
77
77
|
|
|
78
|
-
service_policy_name = f"{service_name}:{settings.
|
|
78
|
+
service_policy_name = f"{service_name}:{settings.frontend.identity}:dial"
|
|
79
79
|
|
|
80
80
|
dial_service_policy = await mgmt_api.search_service_policy(service_policy_name)
|
|
81
81
|
if dial_service_policy:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mrok
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: MPT Extensions OpenZiti Orchestrator
|
|
5
5
|
Author: SoftwareOne AG
|
|
6
6
|
License: Apache License
|
|
@@ -225,7 +225,7 @@ Requires-Dist: pyzmq<28.0.0,>=27.1.0
|
|
|
225
225
|
Requires-Dist: rich<15.0.0,>=14.1.0
|
|
226
226
|
Requires-Dist: textual-serve<2.0.0,>=1.1.3
|
|
227
227
|
Requires-Dist: textual[syntax]<8.0.0,>=7.2.0
|
|
228
|
-
Requires-Dist: typer<0.
|
|
228
|
+
Requires-Dist: typer<1.0.0,>=0.21.1
|
|
229
229
|
Requires-Dist: uvicorn-worker<0.5.0,>=0.4.0
|
|
230
230
|
Description-Content-Type: text/markdown
|
|
231
231
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
mrok/__init__.py,sha256=D1PUs3KtMCqG4bFLceVNG62L3RN53NS95uSCNXpgvzs,181
|
|
2
|
-
mrok/conf.py,sha256=
|
|
2
|
+
mrok/conf.py,sha256=5AgRgwE_Yq0Dv7xDv0SWakhPSr3nnUNjIIgnn0Zgf9c,1057
|
|
3
3
|
mrok/constants.py,sha256=QXaMw4LuHijj_TUTCsM5uUjpgT04HBvd0wRbjvn1z9A,449
|
|
4
4
|
mrok/errors.py,sha256=ruNMDFr2_0ezCGXuCG1OswCEv-bHOIzMMd02J_0ABcs,37
|
|
5
|
-
mrok/logging.py,sha256=
|
|
5
|
+
mrok/logging.py,sha256=PS_x0uAQDsIPF_tbF11fIyijwLS03DQwJzzUHMdVdnE,3000
|
|
6
6
|
mrok/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
mrok/agent/ziticorn.py,sha256=eHUYs9QaSp35rBzYHRV-SrYxF5ySyECaQg7U-XbdINE,1025
|
|
8
8
|
mrok/agent/devtools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
mrok/agent/devtools/inspector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
mrok/agent/devtools/inspector/__main__.py,sha256=
|
|
10
|
+
mrok/agent/devtools/inspector/__main__.py,sha256=vof04S9fwiU8lGjt7YiM6O9YOXEW_lT1AxGoXyF4bg8,97
|
|
11
11
|
mrok/agent/devtools/inspector/app.py,sha256=TxBBIy8lXHj6h8KmQHCeLiIM2QqMzMsCpSWFjH8cGu4,25636
|
|
12
12
|
mrok/agent/devtools/inspector/server.py,sha256=C4uD6_1psSHMjJLUDCMPGvKdQYKaEwYTw27NAbwuuA0,636
|
|
13
13
|
mrok/agent/devtools/inspector/utils.py,sha256=K-_4rTyB54Y0faEIGgutMEHGyP1W7eOhbcuUKxNnsYA,4261
|
|
@@ -17,37 +17,41 @@ mrok/agent/sidecar/main.py,sha256=jeJzrCbltfXOYsKSCjcw8h5lxh4_bGT87kCC5dV4kYU,21
|
|
|
17
17
|
mrok/cli/__init__.py,sha256=mtFEa8IeS1x6Gm4dUYoSnAxyEzNqbUVSmWxtuZUMR84,61
|
|
18
18
|
mrok/cli/main.py,sha256=T029FYxK_jDrwiA14oX-Onoqp_X14XHRAA_4bbaOgV8,3123
|
|
19
19
|
mrok/cli/rich.py,sha256=P3Dyu8EArUR9_0j7DPK7LRx85TWdYdZ1SaJzD_S1ZCE,511
|
|
20
|
-
mrok/cli/utils.py,sha256=
|
|
20
|
+
mrok/cli/utils.py,sha256=FXqyNef0cRFy_d-63ZY-kT3uqmDOK3USSNgBSZyKXIE,831
|
|
21
21
|
mrok/cli/commands/__init__.py,sha256=-UOGzh38oWX7fPeI2nc5I9z8LylRdQAt868q4G6rNGk,140
|
|
22
22
|
mrok/cli/commands/admin/__init__.py,sha256=WU49jpMF9p18UONjYywWEFzjF57zLpLKJ0qAZvrzcR4,414
|
|
23
|
-
mrok/cli/commands/admin/bootstrap.py,sha256=
|
|
23
|
+
mrok/cli/commands/admin/bootstrap.py,sha256=McIGngjpRQSsI2CqW_LBTF1lBeGXvTbbt31jYS35xIY,1705
|
|
24
24
|
mrok/cli/commands/admin/utils.py,sha256=Z7YTAFZKOi6nkw2oX4rJoGoUD41RYL3AOqEDhlV3jR0,1357
|
|
25
25
|
mrok/cli/commands/admin/list/__init__.py,sha256=kjCMcpn1gopcrQaaHxfFh8Kyngldepnle8R2br5dJ_0,195
|
|
26
26
|
mrok/cli/commands/admin/list/extensions.py,sha256=16fhDB5ucL8su2WQnSaQ1E6MhgC4vkP9-nuHAcPpzyE,4405
|
|
27
27
|
mrok/cli/commands/admin/list/instances.py,sha256=kaqeyidwUxgYqfaHXqp2m76rm5h2ErBsYyZcNeaBRwY,5912
|
|
28
28
|
mrok/cli/commands/admin/register/__init__.py,sha256=5Jb_bc2L47MEpQIrOcquzduTFWQ01Jd1U1MpqaR-Ekw,209
|
|
29
|
-
mrok/cli/commands/admin/register/extensions.py,sha256=
|
|
30
|
-
mrok/cli/commands/admin/register/instances.py,sha256=
|
|
29
|
+
mrok/cli/commands/admin/register/extensions.py,sha256=3ooHR-zfFImtqAZ-06kS0555v9gQLQ1G5-ARe_mJ9e4,1353
|
|
30
|
+
mrok/cli/commands/admin/register/instances.py,sha256=KjyLJX1mSXK-6ZmkW9I4PJFYNfgsOyAmJWh92XxSkYg,1982
|
|
31
31
|
mrok/cli/commands/admin/unregister/__init__.py,sha256=-GjjCPX1pISbWmJK6GpKO3ijGsDQb21URjU1hNu99O4,215
|
|
32
|
-
mrok/cli/commands/admin/unregister/extensions.py,sha256=
|
|
33
|
-
mrok/cli/commands/admin/unregister/instances.py,sha256
|
|
32
|
+
mrok/cli/commands/admin/unregister/extensions.py,sha256=xL2yX0kn8dhitQL7NcLTn83bbPZfgPJNzHjZAdiP8yM,891
|
|
33
|
+
mrok/cli/commands/admin/unregister/instances.py,sha256=sLBfBhHDgR7Qw5Zc-EVOSuQUgfLMgh7cFnOP-73iM70,1167
|
|
34
34
|
mrok/cli/commands/agent/__init__.py,sha256=ZAi7eTkKQtfwwV1c1mv3uvEEsyMMrhCQ_-id_0wksAQ,218
|
|
35
35
|
mrok/cli/commands/agent/dev/__init__.py,sha256=ZfreyRuaLqO0AwPS8Ll1DIpsKacsu7_dTmbxV5QecOM,172
|
|
36
36
|
mrok/cli/commands/agent/dev/console.py,sha256=rrKAGoKXVQQBOC75H0JSuX1sYyvc2QSrV-dfMPK49p4,673
|
|
37
37
|
mrok/cli/commands/agent/dev/web.py,sha256=O9dYk-o1FV2E_sKLOezdEmLsnexwbJNDdsYL5pATZRQ,1028
|
|
38
38
|
mrok/cli/commands/agent/run/__init__.py,sha256=E_IJCl3BfMffqFASe8gzJwhhQgt5bQfjhuyekVwdEBA,164
|
|
39
|
-
mrok/cli/commands/agent/run/asgi.py,sha256=
|
|
39
|
+
mrok/cli/commands/agent/run/asgi.py,sha256=FzM3suWJPRqQ08SoDrF9mLfjiBJ6huSbfP3wkTbh3Uo,2054
|
|
40
40
|
mrok/cli/commands/agent/run/sidecar.py,sha256=UOewegTLFwAZ70VFJb6_9kV0LmsvnXuq-yqgrMlTeZo,4182
|
|
41
41
|
mrok/cli/commands/controller/__init__.py,sha256=2xw-YVN0akiLiuGUU3XbYyZZ0ugOjQ6XhtTkzEKSmMA,161
|
|
42
42
|
mrok/cli/commands/controller/openapi.py,sha256=QLjVao9UkB2vBaGkFi_q_jrlg4Np4ldMRwDIJsrJ7A8,1175
|
|
43
43
|
mrok/cli/commands/controller/run.py,sha256=yl1p7oRHhQINWWjUKlRHtMIWUCV0KsxYdyVyazhX834,2406
|
|
44
44
|
mrok/cli/commands/frontend/__init__.py,sha256=0kK37yG6qs7yAa8TYlKZUA-nHrWsO4y5CjbVkXafnuk,123
|
|
45
|
-
mrok/cli/commands/frontend/run.py,sha256=
|
|
45
|
+
mrok/cli/commands/frontend/run.py,sha256=E6vJC9LprGyPetGLfyfJm8GDemEIRVnqetao4V3W9Kk,2796
|
|
46
46
|
mrok/controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
|
-
mrok/controller/app.py,sha256=
|
|
48
|
-
mrok/controller/auth.py,sha256=hYa0OPJ5X0beGxRP6qbxwJOVXj5TmzHjmam2OjTBKn4,2704
|
|
47
|
+
mrok/controller/app.py,sha256=JgyfEFbQeGLpHCrjFKQjdP8TRfV4YVec7Re8i0P4FE8,2040
|
|
49
48
|
mrok/controller/pagination.py,sha256=raYpYa34q8Ckl4BXBOEdpWlKkFj6z7e6QLWr2HT7dzI,2187
|
|
50
49
|
mrok/controller/schemas.py,sha256=PZPEsSJNrGSuplfjCPF_E-VJ721AzgR1Jj8P-Shw1cg,1699
|
|
50
|
+
mrok/controller/auth/__init__.py,sha256=st0q-NHQEQwYlvLEnQdonMVsDmczeL5cS4hkVK7NT6s,412
|
|
51
|
+
mrok/controller/auth/backends.py,sha256=xwiF7qFHh5okhqbTld4P2jSnFkSPZlGR4gfQu598Xrg,2288
|
|
52
|
+
mrok/controller/auth/base.py,sha256=NWEVtc9Y8I56NnyYrBiNzxHZxFSzVEbmkMY2u6RCANs,1131
|
|
53
|
+
mrok/controller/auth/manager.py,sha256=VQwov4UiAOXHBl_a9oPG90_QKRMNz2tWONF3JFR5RmM,1118
|
|
54
|
+
mrok/controller/auth/registry.py,sha256=VmwPPI6E2-oyB2MZDkK_G39EaAeU3Uq-b14GGKz1E-A,485
|
|
51
55
|
mrok/controller/dependencies/__init__.py,sha256=voewk6gjkA0OarL6HFmfT_RLqBns0Fpl-VIqK5xVAEI,202
|
|
52
56
|
mrok/controller/dependencies/conf.py,sha256=2Pa8fxJHkZ29q6UL-w6hUP_wr7WnNELfw5LlzWg1Tec,162
|
|
53
57
|
mrok/controller/dependencies/ziti.py,sha256=fYoxeJb4s6p2_3gxbExbFSRabjpvp_gZMBb3ocXZV3Y,702
|
|
@@ -58,10 +62,12 @@ mrok/controller/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
58
62
|
mrok/controller/routes/extensions.py,sha256=zoY4sNz_BIZcbly6WXM7Rbpn2jmB89njS_0xdJkoKfs,9192
|
|
59
63
|
mrok/controller/routes/instances.py,sha256=v-fn_F6JHbDZ4YUNCIZzClgHp6aC1Eu5HB7k7qBG5pk,2202
|
|
60
64
|
mrok/frontend/__init__.py,sha256=SN3LoFwAye18lfJ8OKNNS-7kLc2A9OxPGIEIEYYtAOA,54
|
|
61
|
-
mrok/frontend/app.py,sha256=
|
|
62
|
-
mrok/frontend/main.py,sha256=
|
|
65
|
+
mrok/frontend/app.py,sha256=_FLz5fqZdlFc1tdBBMwhmvjGa0UnNeaNj6EnYEnuPAQ,5280
|
|
66
|
+
mrok/frontend/main.py,sha256=zvfCh7NDx-mkpN-ppM2AqWmgKn1cBrNOCky8UgjLou4,1833
|
|
67
|
+
mrok/frontend/middleware.py,sha256=xTXt5gYikr9RCXaI4uVKgxFhlH49RNY3MD3eZPcX9cc,1161
|
|
68
|
+
mrok/frontend/utils.py,sha256=Oh987pCpg7ZIIIpRWcpX7Nrh8FGbJ4cH4ciiG1xRoQI,2120
|
|
63
69
|
mrok/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
|
-
mrok/proxy/app.py,sha256=
|
|
70
|
+
mrok/proxy/app.py,sha256=HU-YRKA3ijY2AW9v-NwfUnbSgdI-XqsnkQ90TocZ1Ts,6257
|
|
65
71
|
mrok/proxy/asgi.py,sha256=2uw5bLquyUsiYlNwq8RhJd8OqVvSJDvYjzOVGLLB3Cs,3528
|
|
66
72
|
mrok/proxy/backend.py,sha256=dRmIUJin2DM3PUxrVX0j4t1oB6DOX7N9JV2lIcopE38,1649
|
|
67
73
|
mrok/proxy/event_publisher.py,sha256=TAuwEqIhRYxgazJFgC3DekwUAXlJ2UFjbdx_A9vwA1g,2511
|
|
@@ -77,15 +83,15 @@ mrok/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
77
83
|
mrok/types/proxy.py,sha256=40Yds4tUykMpzsoQbMtHG85r8xtm5Q3fQZ17bp7cDiM,818
|
|
78
84
|
mrok/types/ziti.py,sha256=EeQnTbDEJ-Y-KMS6zu1Xjxb58Up2VxUwqzUwy3H28JY,36
|
|
79
85
|
mrok/ziti/__init__.py,sha256=20OWMiexRhOovZOX19zlX87-V78QyWnEnSZfyAftUdE,263
|
|
80
|
-
mrok/ziti/api.py,sha256=
|
|
86
|
+
mrok/ziti/api.py,sha256=Z6Hs17-UaKtcRc6uMowhvQW5fbyW6wFr-7lAvI3aZm0,16125
|
|
81
87
|
mrok/ziti/bootstrap.py,sha256=RSL8nZfI-MZ_z6h0F-rNevQoE6g9oGKLr6HlZF696_c,2499
|
|
82
88
|
mrok/ziti/constants.py,sha256=Urq1X3bCBQZfw8NbnEa1pqmY4oq1wmzkwPfzam3kbTw,339
|
|
83
89
|
mrok/ziti/errors.py,sha256=yYCbVDwktnR0AYduqtynIjo73K3HOhIrwA_vQimvEd4,368
|
|
84
|
-
mrok/ziti/identities.py,sha256=
|
|
90
|
+
mrok/ziti/identities.py,sha256=cOMv-Jv8MEjtzyjRcWOF8Ziz4HJY2Z-uHXWpZRqgPxs,6883
|
|
85
91
|
mrok/ziti/pki.py,sha256=o2tySqHC8-7bvFuI2Tqxg9vX6H6ZSxWxfP_9x29e19M,1954
|
|
86
|
-
mrok/ziti/services.py,sha256=
|
|
87
|
-
mrok-0.
|
|
88
|
-
mrok-0.
|
|
89
|
-
mrok-0.
|
|
90
|
-
mrok-0.
|
|
91
|
-
mrok-0.
|
|
92
|
+
mrok/ziti/services.py,sha256=P2c9qRUyUFu1pSKPdT8L6s3yTKYVpTabRiHPWqbBIiU,3231
|
|
93
|
+
mrok-0.8.0.dist-info/METADATA,sha256=SmmFB0ZpkWORFIvUKENkpOszVnFjgqVZS6p7rbPyIwc,15978
|
|
94
|
+
mrok-0.8.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
95
|
+
mrok-0.8.0.dist-info/entry_points.txt,sha256=tloXwvU1uJicBJR2h-8HoVclPgwJWDwuREMHN8Zq-nU,38
|
|
96
|
+
mrok-0.8.0.dist-info/licenses/LICENSE.txt,sha256=6PaICaoA3yNsZKLv5G6OKqSfLSoX7MakYqTDgJoTCBs,11346
|
|
97
|
+
mrok-0.8.0.dist-info/RECORD,,
|
mrok/controller/auth.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
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} ({credentials.credentials})")
|
|
84
|
-
raise UNAUTHORIZED_EXCEPTION
|
|
85
|
-
except jwt.InvalidTokenError as e:
|
|
86
|
-
logger.error(f"Invalid jwt token: {e} ({credentials.credentials})")
|
|
87
|
-
raise UNAUTHORIZED_EXCEPTION
|
|
File without changes
|
|
File without changes
|
|
File without changes
|