Skip to content
Open
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
5 changes: 4 additions & 1 deletion apps/vedana/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ BASE_VERSION=$(shell uv version --short | sed 's/[\+\/]/-/g')
BRANCH=$(shell git rev-parse --abbrev-ref HEAD | sed 's/[\+\/]/-/g')
GIT_HASH=$(shell git rev-parse --short HEAD)

REPO=ghcr.io/epoch8/ai-assistants-oss/vedana
REPO=ghcr.io/epoch8/vedana/vedana

ifeq ($(BRANCH),master)
VERSION=$(BASE_VERSION)
Expand All @@ -18,3 +18,6 @@ build:

docker-run-it:
docker run --rm -it ${IMAGE} bash

upload:
docker push ${IMAGE}
19 changes: 16 additions & 3 deletions libs/jims-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,24 @@ Environment variables:
- `JIMS_APP` - JIMS app import path (`module:attr`)
- `JIMS_PORT` - HTTP port
- `JIMS_HOST` - HTTP host
- `JIMS_API_KEY` - optional bearer token for auth
- `JIMS_API_KEY` - optional static bearer token for auth
- `JIMS_AUTHENTIK_URL` - Authentik instance base URL (e.g. `https://authentik.epoch8.dev`)
- `JIMS_AUTHENTIK_APP_SLUG` - Authentik application slug to check access against

## Authentication

Auth is checked in the following order:

1. If `JIMS_API_KEY` is set and the provided bearer token matches — allow
2. If `JIMS_AUTHENTIK_URL` and `JIMS_AUTHENTIK_APP_SLUG` are set — validate the bearer token against Authentik's `/api/v3/core/applications/{slug}/check_access/` endpoint. If 200 — allow
3. If no auth is configured (neither static key nor Authentik) — open access
4. Otherwise — 401 Unauthorized

Send the token as `Authorization: Bearer <token>`.

## Endpoints

- `GET /health`
- `GET /healthz`
- `POST /api/v1/chat`

### `POST /api/v1/chat`
Expand Down Expand Up @@ -52,4 +65,4 @@ Response:
}
```

If `JIMS_API_KEY` is set, send it as `Authorization: Bearer <token>`
Send the API token as `Authorization: Bearer <token>` (see [Authentication](#authentication) above).
1 change: 1 addition & 0 deletions libs/jims-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies = [
"jims-core>=0.5.1",
"loguru>=0.7.3",
"uvicorn>=0.35.0",
"httpx>=0.28.0",
]

[tool.uv.sources]
Expand Down
31 changes: 24 additions & 7 deletions libs/jims-api/src/jims_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from uuid import UUID

import click
import httpx
import uvicorn
from fastapi import Depends, FastAPI, Header, HTTPException
from jims_core.app import JimsApp
Expand Down Expand Up @@ -49,14 +50,24 @@ def _extract_token(authorization: str | None) -> str | None:
return authorization


def _auth_dependency(api_key: str | None):
def _auth_dependency(api_key: str | None, authentik_url: str | None, authentik_app_slug: str | None):
async def require_auth(authorization: str | None = Header(default=None, alias="Authorization")) -> None:
if api_key is None:
if api_key is None and authentik_url is None:
return

token = _extract_token(authorization)
if token != api_key:
raise HTTPException(status_code=401, detail="Unauthorized")

if api_key is not None and token == api_key:
return

if authentik_url is not None and authentik_app_slug is not None and token is not None:
url = f"{authentik_url}/api/v3/core/applications/{authentik_app_slug}/check_access/"
async with httpx.AsyncClient() as client:
resp = await client.get(url, headers={"Authorization": f"Bearer {token}"})
if resp.status_code == 200 and resp.json().get("passing") is True:
return

raise HTTPException(status_code=401, detail="Unauthorized")

return require_auth

Expand All @@ -68,9 +79,11 @@ async def _resolve_jims_app(app_name: str) -> JimsApp:
return loaded_app


def create_api(jims_app: JimsApp, api_key: str | None) -> FastAPI:
def create_api(
jims_app: JimsApp, api_key: str | None, authentik_url: str | None = None, authentik_app_slug: str | None = None
) -> FastAPI:
app = FastAPI(title="JIMS API", version="0.1.0")
require_auth = _auth_dependency(api_key)
require_auth = _auth_dependency(api_key, authentik_url, authentik_app_slug)

@app.get("/healthz")
async def health() -> dict[str, str]:
Expand Down Expand Up @@ -141,6 +154,8 @@ async def chat(req: ChatRequest) -> ChatResponse:
@click.option("--host", type=click.STRING, default="0.0.0.0")
@click.option("--port", type=click.INT, default=8080)
@click.option("--api-key", type=click.STRING, default=None, help="Optional bearer token for API auth.")
@click.option("--authentik-url", type=click.STRING, default=None, help="Authentik instance base URL.")
@click.option("--authentik-app-slug", type=click.STRING, default=None, help="Authentik application slug.")
@click.option("--enable-sentry", is_flag=True, help="Enable tracing to Sentry", default=False)
@click.option("--metrics-port", type=click.INT, default=8000)
@click.option("--verbose", is_flag=True, default=False)
Expand All @@ -149,6 +164,8 @@ def cli(
host: str,
port: int,
api_key: str | None,
authentik_url: str | None,
authentik_app_slug: str | None,
enable_sentry: bool,
metrics_port: int,
verbose: bool,
Expand All @@ -163,7 +180,7 @@ def cli(

async def run_api() -> None:
jims_app = await _resolve_jims_app(app)
api = create_api(jims_app, api_key=api_key)
api = create_api(jims_app, api_key=api_key, authentik_url=authentik_url, authentik_app_slug=authentik_app_slug)
server = uvicorn.Server(uvicorn.Config(api, host=host, port=port, log_level="info"))
await server.serve()

Expand Down