Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv
5 changes: 3 additions & 2 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ services:
mrok-controller:
build:
context: .
dockerfile: prod.Dockerfile
dockerfile: dev.Dockerfile
depends_on:
ziti-controller:
condition: service_healthy
Expand All @@ -204,13 +204,14 @@ services:
- MROK_LOGGING__DEBUG=false
networks:
mrok:
ipv4_address: ${MROK_DEVCONTAINER_IP:-172.99.0.15}
dns:
- ${MROK_DNS_IP:-172.99.0.2}

mrok-frontend:
build:
context: .
dockerfile: prod.Dockerfile
dockerfile: dev.Dockerfile
depends_on:
ziti-controller:
condition: service_healthy
Expand Down
2 changes: 1 addition & 1 deletion dev.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM python:3.12
# The uv installer requires curl (and certificates) to download the release archive
RUN apt-get clean -y; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates curl vim postgresql-client netcat-openbsd libprotobuf-c1 dnsutils; \
apt-get install -y --no-install-recommends ca-certificates curl vim postgresql-client netcat-openbsd libprotobuf-c1 dnsutils jq; \
apt-get autoremove --purge -y; \
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
Expand Down
6 changes: 0 additions & 6 deletions mrok/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@
"ssl_verify": False,
},
"PAGINATION": {"limit": 50},
"SIDECAR": {
"textual_port": 4040,
"store_port": 5051,
"store_size": 1000,
"textual_command": "python mrok/agent/sidecar/inspector.py",
},
}

_settings = None
Expand Down
17 changes: 8 additions & 9 deletions mrok/controller/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from fastapi import Depends, FastAPI
from fastapi.routing import APIRoute, APIRouter

from mrok.conf import get_settings
from mrok.controller.auth import authenticate
from mrok.conf import Settings, get_settings
from mrok.controller.auth import HTTPAuthManager
from mrok.controller.openapi import generate_openapi_spec
from mrok.controller.routes.extensions import router as extensions_router
from mrok.controller.routes.instances import router as instances_router
Expand Down Expand Up @@ -36,7 +36,8 @@ def setup_custom_serialization(router: APIRouter):
api_route.response_model_exclude_none = True


def setup_app():
def setup_app(settings: Settings):
auth_manager = HTTPAuthManager(settings.auth)
app = FastAPI(
title="mrok Controller API",
description="API to orchestrate OpenZiti for Extensions.",
Expand All @@ -53,18 +54,16 @@ def setup_app():
app.include_router(
extensions_router,
prefix="/extensions",
dependencies=[Depends(authenticate)],
dependencies=[Depends(auth_manager)],
)
app.include_router(
instances_router,
prefix="/instances",
dependencies=[Depends(authenticate)],
dependencies=[Depends(auth_manager)],
)

settings = get_settings()

app.openapi = partial(generate_openapi_spec, app, settings)
app.openapi = partial(generate_openapi_spec, app, settings) # type: ignore[method-assign]
return app


app = setup_app()
app = setup_app(get_settings())
87 changes: 0 additions & 87 deletions mrok/controller/auth.py

This file was deleted.

11 changes: 11 additions & 0 deletions mrok/controller/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from mrok.controller.auth.backends import OIDCJWTAuthenticationBackend # noqa: F401
from mrok.controller.auth.base import AuthIdentity, BaseHTTPAuthBackend
from mrok.controller.auth.manager import HTTPAuthManager
from mrok.controller.auth.registry import register_authentication_backend

__all__ = [
"AuthIdentity",
"BaseHTTPAuthBackend",
"HTTPAuthManager",
"register_authentication_backend",
]
60 changes: 60 additions & 0 deletions mrok/controller/auth/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import logging

import httpx
import jwt
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.security.http import HTTPBase

from mrok.controller.auth.base import UNAUTHORIZED_EXCEPTION, AuthIdentity, BaseHTTPAuthBackend
from mrok.controller.auth.registry import register_authentication_backend

logger = logging.getLogger("mrok.controller")


@register_authentication_backend("oidc")
class OIDCJWTAuthenticationBackend(BaseHTTPAuthBackend):
def init_scheme(self) -> HTTPBase:
return HTTPBearer(auto_error=False)

async def authenticate(self, credentials: HTTPAuthorizationCredentials) -> AuthIdentity | None:
async with httpx.AsyncClient() as client:
try:
config_resp = await client.get(self.config.openid_config_url)
config_resp.raise_for_status()
config = config_resp.json()
issuer = config["issuer"]
jwks_uri = config["jwks_uri"]

jwks_resp = await client.get(jwks_uri)
jwks_resp.raise_for_status()
jwks = jwks_resp.json()

header = jwt.get_unverified_header(credentials.credentials)
kid = header["kid"]

key_data = next((k for k in jwks["keys"] if k["kid"] == kid), None)
except Exception:
logger.exception("Error fetching openid-config/jwks")
raise UNAUTHORIZED_EXCEPTION
if key_data is None:
logger.error("Key ID not found in JWKS")
raise UNAUTHORIZED_EXCEPTION

try:
payload = jwt.decode(
credentials.credentials,
jwt.PyJWK(key_data),
algorithms=[header["alg"]],
issuer=issuer,
audience=self.config.audience,
)
return AuthIdentity(
subject=payload["sub"],
metadata=payload,
)
except jwt.InvalidKeyError as e:
logger.error(f"Invalid jwt token: {e} ({credentials.credentials})")
raise UNAUTHORIZED_EXCEPTION
except jwt.InvalidTokenError as e:
logger.error(f"Invalid jwt token: {e} ({credentials.credentials})")
raise UNAUTHORIZED_EXCEPTION
38 changes: 38 additions & 0 deletions mrok/controller/auth/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from abc import ABC, abstractmethod
from typing import Any

from dynaconf.utils.boxing import DynaBox
from fastapi import HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials
from fastapi.security.http import HTTPBase
from pydantic import BaseModel

UNAUTHORIZED_EXCEPTION = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized."
)


class AuthIdentity(BaseModel):
subject: str
scopes: list[str] = []
metadata: dict[str, Any] = {}


class BaseHTTPAuthBackend(ABC):
def __init__(self, config: DynaBox):
self.config = config
self.scheme = self.init_scheme()

@abstractmethod
def init_scheme(self) -> HTTPBase:
raise NotImplementedError()

@abstractmethod
async def authenticate(self, credentials: HTTPAuthorizationCredentials) -> AuthIdentity | None:
raise NotImplementedError()

async def __call__(self, request: Request) -> AuthIdentity | None:
credentials = await self.scheme(request)
if not credentials:
return None
return await self.authenticate(credentials)
31 changes: 31 additions & 0 deletions mrok/controller/auth/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from dynaconf.utils.boxing import DynaBox
from fastapi import Request

from mrok.controller.auth.base import UNAUTHORIZED_EXCEPTION, AuthIdentity, BaseHTTPAuthBackend
from mrok.controller.auth.registry import get_authentication_backend


class HTTPAuthManager:
def __init__(self, auth_settings: DynaBox):
self.auth_settings = auth_settings
self.active_backends: list[BaseHTTPAuthBackend] = []
self._setup_backends()

def _setup_backends(self):
enabled_keys = self.auth_settings.get("backends", [])

for key in enabled_keys:
backend_cls = get_authentication_backend(key)
if not backend_cls:
raise ValueError(f"Backend '{key}' is not registered.")

specific_config = self.auth_settings.get(key, {})
self.active_backends.append(backend_cls(specific_config))

async def __call__(self, request: Request) -> AuthIdentity:
for backend in self.active_backends:
identity = await backend(request)
if identity:
return identity

raise UNAUTHORIZED_EXCEPTION
17 changes: 17 additions & 0 deletions mrok/controller/auth/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from mrok.controller.auth.base import BaseHTTPAuthBackend

BACKEND_REGISTRY: dict[str, type[BaseHTTPAuthBackend]] = {}


def register_authentication_backend(name: str):
"""Decorator to register a backend class with a unique key."""

def decorator(cls: type[BaseHTTPAuthBackend]):
BACKEND_REGISTRY[name] = cls
return cls

return decorator


def get_authentication_backend(name: str) -> type[BaseHTTPAuthBackend] | None:
return BACKEND_REGISTRY.get(name)
9 changes: 6 additions & 3 deletions settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ logging:
rich: True

auth:
openid_config_url: https://example.com/openid-configuration
audience: my-audience
read_timeout: 10
backends:
- oidc
oidc:
openid_config_url: https://example.com/openid-configuration
audience: my-audience


pagination:
limit: 50
Loading
Loading