bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
209 lines
6.9 KiB
Python
209 lines
6.9 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from enum import StrEnum
|
|
from pathlib import Path
|
|
from typing import Mapping
|
|
|
|
from mirage.server.auth import storage as _storage
|
|
from mirage.server.daemon_config import read_daemon_table
|
|
from mirage.server.paths import mirage_home
|
|
|
|
ENV_AUTH_MODE = "MIRAGE_AUTH_MODE"
|
|
ENV_AUTH_TOKEN = "MIRAGE_AUTH_TOKEN"
|
|
ENV_JWT_PUBKEY = "MIRAGE_JWT_PUBKEY"
|
|
ENV_JWT_PUBKEY_FILE = "MIRAGE_JWT_PUBKEY_FILE"
|
|
ENV_JWT_ALG = "MIRAGE_JWT_ALG"
|
|
ENV_JWT_ISSUER = "MIRAGE_JWT_ISSUER"
|
|
ENV_JWT_AUDIENCE = "MIRAGE_JWT_AUDIENCE"
|
|
ENV_JWT_AUTHORIZED_PARTIES = "MIRAGE_JWT_AUTHORIZED_PARTIES"
|
|
ENV_JWT_CLOCK_SKEW = "MIRAGE_JWT_CLOCK_SKEW_SECONDS"
|
|
|
|
DEFAULT_CLOCK_SKEW_SECONDS = 5
|
|
|
|
|
|
class AuthMode(StrEnum):
|
|
LOCAL = "local"
|
|
TOKEN = "token"
|
|
JWT = "jwt"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class JWTConfig:
|
|
key: str
|
|
algorithm: str
|
|
issuer: str | None = None
|
|
audience: str | None = None
|
|
authorized_parties: tuple[str, ...] = field(default_factory=tuple)
|
|
clock_skew_seconds: int = DEFAULT_CLOCK_SKEW_SECONDS
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AuthConfig:
|
|
mode: AuthMode
|
|
local_token: str | None = None
|
|
bearer_token: str | None = None
|
|
jwt: JWTConfig | None = None
|
|
|
|
|
|
def resolve_local_token(
|
|
env: Mapping[str, str] | None = None,
|
|
token_file: Path | None = None,
|
|
) -> str | None:
|
|
"""Resolve the local-mode bearer token via env > file > None.
|
|
|
|
Args:
|
|
env (Mapping[str, str] | None): environment to read
|
|
``MIRAGE_AUTH_TOKEN`` from. Defaults to ``os.environ``.
|
|
token_file (Path | None): location of the token file.
|
|
Defaults to ``default_token_file()``.
|
|
|
|
Returns:
|
|
str | None: resolved token, or ``None`` if no source provides one.
|
|
"""
|
|
e = env if env is not None else os.environ
|
|
val = e.get(ENV_AUTH_TOKEN, "").strip()
|
|
if val:
|
|
return val
|
|
path = (token_file
|
|
if token_file is not None else _storage.default_token_file())
|
|
return _storage.read_token_file(path)
|
|
|
|
|
|
def _read_jwt_key(env: Mapping[str, str]) -> str:
|
|
inline = env.get(ENV_JWT_PUBKEY, "").strip()
|
|
if inline:
|
|
return inline
|
|
path = env.get(ENV_JWT_PUBKEY_FILE, "").strip()
|
|
if path:
|
|
return Path(path).read_text()
|
|
raise RuntimeError(
|
|
f"mode=jwt requires {ENV_JWT_PUBKEY} or {ENV_JWT_PUBKEY_FILE}")
|
|
|
|
|
|
def _parse_csv(value: str) -> tuple[str, ...]:
|
|
return tuple(p.strip() for p in value.split(",") if p.strip())
|
|
|
|
|
|
_CONFIG_ENV_KEYS = {
|
|
"auth_mode": ENV_AUTH_MODE,
|
|
"jwt_alg": ENV_JWT_ALG,
|
|
"jwt_issuer": ENV_JWT_ISSUER,
|
|
"jwt_audience": ENV_JWT_AUDIENCE,
|
|
"jwt_pubkey_file": ENV_JWT_PUBKEY_FILE,
|
|
"jwt_clock_skew": ENV_JWT_CLOCK_SKEW,
|
|
"jwt_authorized_parties": ENV_JWT_AUTHORIZED_PARTIES,
|
|
}
|
|
|
|
|
|
def _merge_config_table(env: Mapping[str, str],
|
|
table: Mapping[str, object]) -> dict[str, str]:
|
|
"""Fold config.toml auth keys under their env names, env winning.
|
|
|
|
Only non-secret keys have config counterparts: the raw
|
|
``MIRAGE_AUTH_TOKEN`` and inline ``MIRAGE_JWT_PUBKEY`` stay
|
|
env-only (use the token file / ``jwt_pubkey_file`` instead).
|
|
|
|
Args:
|
|
env (Mapping[str, str]): the process environment view.
|
|
table (Mapping[str, object]): the ``[daemon]`` config table.
|
|
|
|
Returns:
|
|
dict[str, str]: env copy with config fallbacks applied.
|
|
"""
|
|
merged = dict(env)
|
|
for cfg_key, env_name in _CONFIG_ENV_KEYS.items():
|
|
if merged.get(env_name, "").strip():
|
|
continue
|
|
value = table.get(cfg_key)
|
|
if value is not None and str(value).strip():
|
|
merged[env_name] = str(value)
|
|
return merged
|
|
|
|
|
|
def resolve_auth_config(
|
|
env: Mapping[str, str] | None = None,
|
|
token_file: Path | None = None,
|
|
table: Mapping[str, object] | None = None,
|
|
) -> AuthConfig:
|
|
"""Resolve daemon auth configuration from environment and config.
|
|
|
|
Per key the environment variable wins over the ``[daemon]`` table
|
|
in ``config.toml``, which wins over the default.
|
|
|
|
Args:
|
|
env (Mapping[str, str] | None): environment to read from.
|
|
Defaults to ``os.environ``.
|
|
token_file (Path | None): override the local-mode token file
|
|
location. Defaults to ``default_token_file()``.
|
|
table (Mapping[str, object] | None): the ``[daemon]`` config
|
|
table. Defaults to reading ``$MIRAGE_HOME/config.toml``
|
|
when ``env`` is also defaulted; an explicit ``env`` with no
|
|
``table`` stays hermetic and reads no file.
|
|
|
|
Returns:
|
|
AuthConfig: resolved configuration.
|
|
|
|
Raises:
|
|
RuntimeError: if required settings are missing for the chosen mode.
|
|
"""
|
|
if table is None:
|
|
table = read_daemon_table(mirage_home()) if env is None else {}
|
|
e = _merge_config_table(env if env is not None else os.environ, table)
|
|
raw_mode = (e.get(ENV_AUTH_MODE, "")
|
|
or AuthMode.LOCAL.value).strip().lower()
|
|
try:
|
|
mode = AuthMode(raw_mode)
|
|
except ValueError as exc:
|
|
valid = ", ".join(m.value for m in AuthMode)
|
|
raise RuntimeError(
|
|
f"{ENV_AUTH_MODE} must be one of ({valid}), got {raw_mode!r}"
|
|
) from exc
|
|
|
|
if mode == AuthMode.LOCAL:
|
|
return AuthConfig(
|
|
mode=mode,
|
|
local_token=resolve_local_token(env=e, token_file=token_file),
|
|
)
|
|
|
|
if mode == AuthMode.TOKEN:
|
|
token = e.get(ENV_AUTH_TOKEN, "").strip()
|
|
if not token:
|
|
raise RuntimeError(
|
|
f"mode=token requires {ENV_AUTH_TOKEN} to be set")
|
|
return AuthConfig(mode=mode, bearer_token=token)
|
|
|
|
key = _read_jwt_key(e)
|
|
alg = e.get(ENV_JWT_ALG, "").strip()
|
|
if not alg:
|
|
raise RuntimeError(f"mode=jwt requires {ENV_JWT_ALG} (e.g. RS256)")
|
|
issuer = (e.get(ENV_JWT_ISSUER) or "").strip() or None
|
|
audience = (e.get(ENV_JWT_AUDIENCE) or "").strip() or None
|
|
azp = _parse_csv(e.get(ENV_JWT_AUTHORIZED_PARTIES, ""))
|
|
skew_raw = (e.get(ENV_JWT_CLOCK_SKEW) or "").strip()
|
|
skew = int(skew_raw) if skew_raw else DEFAULT_CLOCK_SKEW_SECONDS
|
|
return AuthConfig(
|
|
mode=mode,
|
|
jwt=JWTConfig(
|
|
key=key,
|
|
algorithm=alg,
|
|
issuer=issuer,
|
|
audience=audience,
|
|
authorized_parties=azp,
|
|
clock_skew_seconds=skew,
|
|
),
|
|
)
|