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: 5 additions & 0 deletions agents/langgraph/templates/agentic_rag/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ MODEL_ID=ollama/Llama3.1:8B
# Deployment
CONTAINER_IMAGE=

# Auth (local default off; comma-separate multiple allowlist entries)
# AUTH_ENABLED=false
# AUTH_AUDIENCE=langgraph-agentic-rag
# AUTH_ALLOWED_SERVICEACCOUNTS=ci-testing:langgraph-agentic-rag-caller

# RAG-specific Configuration
EMBEDDING_MODEL=
EMBEDDING_DIMENSION=768
Expand Down
7 changes: 5 additions & 2 deletions agents/langgraph/templates/agentic_rag/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ COPY --from=ghcr.io/astral-sh/uv@sha256:fc93e9ecd7218e9ec8fba117af89348eef8fd246
# Copy project files for dependency installation
COPY pyproject.toml .
COPY src/ ./src/
COPY components/auth/ ./components/auth/

# Install the project and its dependencies using uv
RUN uv pip install --no-cache ".[tracing]"
# Install auth component first, then the project (--no-sources skips [tool.uv.sources]
# which uses relative paths valid only for local dev)
RUN uv pip install --no-cache ./components/auth/ && \
uv pip install --no-cache --no-sources ".[tracing]"

# Copy the application entrypoint, playground UI, data, and images
COPY main.py .
Expand Down
42 changes: 40 additions & 2 deletions agents/langgraph/templates/agentic_rag/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ build: ## Build container image locally (podman/docker)
@[ -n "$(CONTAINER_CLI)" ] || { echo "ERROR: neither podman nor docker found in PATH"; exit 1; } && \
source .env && \
[ -n "$${CONTAINER_IMAGE}" ] || { echo "ERROR: CONTAINER_IMAGE is not set in .env"; exit 1; } && \
rm -rf ./images && cp -r ../../../../images ./images && trap 'rm -rf ./images' EXIT && \
rm -rf ./images ./components/auth && \
trap 'rm -rf ./images ./components/auth; rmdir ./components 2>/dev/null || true' EXIT && \
cp -r ../../../../images ./images && \
mkdir -p ./components && cp -r ../../../../components/auth ./components/auth && \
$(CONTAINER_CLI) build --platform linux/amd64 -t "$${CONTAINER_IMAGE}" -f Dockerfile .

push: ## Push container image to registry
Expand All @@ -105,7 +108,10 @@ push: ## Push container image to registry
$(CONTAINER_CLI) push "$${CONTAINER_IMAGE}"

build-openshift: ## Build image in-cluster via OpenShift BuildConfig (no podman/docker needed)
@rm -rf ./images && cp -r ../../../../images ./images && trap 'rm -rf ./images' EXIT && \
@rm -rf ./images ./components/auth && \
trap 'rm -rf ./images ./components/auth; rmdir ./components 2>/dev/null || true' EXIT && \
cp -r ../../../../images ./images && \
mkdir -p ./components && cp -r ../../../../components/auth ./components/auth && \
oc new-build --strategy=docker --binary --name=$(AGENT_NAME) --to=$(AGENT_NAME):latest 2>/dev/null || true && \
oc start-build $(AGENT_NAME) --from-dir=. --follow && \
NS=$$(oc project -q) && \
Expand All @@ -124,6 +130,17 @@ _check-env:

deploy: _check-env ## Deploy to OpenShift/K8s via Helm
@source .env && \
AUTH_ALLOWLIST="$${AUTH_ALLOWED_SERVICEACCOUNTS:-$${AUTH_ALLOWED_SERVICEACCOUNT}}" && \
AUTH_ALLOWLIST_ARGS="" && \
if [ -n "$$AUTH_ALLOWLIST" ]; then \
IFS=',' read -r -a AUTH_ALLOWLIST_ITEMS <<< "$$AUTH_ALLOWLIST"; \
for idx in "$${!AUTH_ALLOWLIST_ITEMS[@]}"; do \
account="$${AUTH_ALLOWLIST_ITEMS[$$idx]}"; \
account="$$(echo -n "$$account" | tr -d '[:space:]')"; \
[ -n "$$account" ] || continue; \
AUTH_ALLOWLIST_ARGS="$$AUTH_ALLOWLIST_ARGS --set-string auth.allowedServiceAccounts[$$idx]=$$account"; \
done; \
fi && \
[ -n "$${CONTAINER_IMAGE}" ] || { echo "ERROR: CONTAINER_IMAGE is not set in .env"; exit 1; } && \
LAST_SEG="$${CONTAINER_IMAGE##*/}" && if [[ "$$LAST_SEG" == *:* ]]; then IMAGE_REPO="$${CONTAINER_IMAGE%:*}"; IMAGE_TAG="$${LAST_SEG##*:}"; else IMAGE_REPO="$${CONTAINER_IMAGE}"; IMAGE_TAG="latest"; fi && \
trap 'rm -f .helm-secrets.yaml' EXIT && \
Expand All @@ -138,6 +155,11 @@ deploy: _check-env ## Deploy to OpenShift/K8s via Helm
--set image.tag="$${IMAGE_TAG}" \
--set env.BASE_URL="$${BASE_URL}" \
--set env.MODEL_ID="$${MODEL_ID}" \
$${AUTH_ENABLED:+--set "auth.enabled=$${AUTH_ENABLED}"} \
$${AUTH_ENABLED:+--set "serviceAccount.create=$${AUTH_ENABLED}"} \
$${AUTH_ENABLED:+--set "auth.createAuthDelegatorBinding=$${AUTH_ENABLED}"} \
$${AUTH_AUDIENCE:+--set-string "auth.audience=$${AUTH_AUDIENCE}"} \
$${AUTH_ALLOWLIST_ARGS} \
Comment on lines +158 to +162

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail fast when auth is enabled without an audience.

AUTH_ENABLED=true currently enables the chart even when AUTH_AUDIENCE is empty, because the audience value is silently omitted. This violates the middleware contract and can leave protected routes unusable. Add the same preflight validation before both Helm commands.

Suggested validation
+	  if [ "$${AUTH_ENABLED:-}" = "true" ] && [ -z "$${AUTH_AUDIENCE:-}" ]; then \
+	    echo "ERROR: AUTH_AUDIENCE is required when AUTH_ENABLED=true" >&2; exit 1; \
+	  fi && \

As per path instructions: “Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity.”

Also applies to: 205-209

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/langgraph/templates/agentic_rag/Makefile` around lines 158 - 162,
Update the Makefile target containing the Helm commands to add preflight
validation before both commands: when AUTH_ENABLED is true, require
AUTH_AUDIENCE to be non-empty and fail with a clear error otherwise. Preserve
the existing Helm argument construction and ensure the validation runs before
either deployment path executes.

Source: Path instructions

$${EMBEDDING_MODEL:+--set env.EMBEDDING_MODEL="$${EMBEDDING_MODEL}"} \
$${EMBEDDING_DIMENSION:+--set env.EMBEDDING_DIMENSION="$${EMBEDDING_DIMENSION}"} \
$${VECTOR_STORE_ID:+--set env.VECTOR_STORE_ID="$${VECTOR_STORE_ID}"} \
Expand All @@ -160,6 +182,17 @@ deploy: _check-env ## Deploy to OpenShift/K8s via Helm

dry-run: _check-env ## Render Helm templates without deploying
@source .env && \
AUTH_ALLOWLIST="$${AUTH_ALLOWED_SERVICEACCOUNTS:-$${AUTH_ALLOWED_SERVICEACCOUNT}}" && \
AUTH_ALLOWLIST_ARGS="" && \
if [ -n "$$AUTH_ALLOWLIST" ]; then \
IFS=',' read -r -a AUTH_ALLOWLIST_ITEMS <<< "$$AUTH_ALLOWLIST"; \
for idx in "$${!AUTH_ALLOWLIST_ITEMS[@]}"; do \
account="$${AUTH_ALLOWLIST_ITEMS[$$idx]}"; \
account="$$(echo -n "$$account" | tr -d '[:space:]')"; \
[ -n "$$account" ] || continue; \
AUTH_ALLOWLIST_ARGS="$$AUTH_ALLOWLIST_ARGS --set-string auth.allowedServiceAccounts[$$idx]=$$account"; \
done; \
fi && \
[ -n "$${CONTAINER_IMAGE}" ] || { echo "ERROR: CONTAINER_IMAGE is not set in .env"; exit 1; } && \
LAST_SEG="$${CONTAINER_IMAGE##*/}" && if [[ "$$LAST_SEG" == *:* ]]; then IMAGE_REPO="$${CONTAINER_IMAGE%:*}"; IMAGE_TAG="$${LAST_SEG##*:}"; else IMAGE_REPO="$${CONTAINER_IMAGE}"; IMAGE_TAG="latest"; fi && \
helm template $(AGENT_NAME) $(CHART_DIR) \
Expand All @@ -169,6 +202,11 @@ dry-run: _check-env ## Render Helm templates without deploying
--set image.tag="$${IMAGE_TAG}" \
--set env.BASE_URL="$${BASE_URL}" \
--set env.MODEL_ID="$${MODEL_ID}" \
$${AUTH_ENABLED:+--set "auth.enabled=$${AUTH_ENABLED}"} \
$${AUTH_ENABLED:+--set "serviceAccount.create=$${AUTH_ENABLED}"} \
$${AUTH_ENABLED:+--set "auth.createAuthDelegatorBinding=$${AUTH_ENABLED}"} \
$${AUTH_AUDIENCE:+--set-string "auth.audience=$${AUTH_AUDIENCE}"} \
$${AUTH_ALLOWLIST_ARGS} \
$${EMBEDDING_MODEL:+--set env.EMBEDDING_MODEL="$${EMBEDDING_MODEL}"} \
$${EMBEDDING_DIMENSION:+--set env.EMBEDDING_DIMENSION="$${EMBEDDING_DIMENSION}"} \
$${VECTOR_STORE_ID:+--set env.VECTOR_STORE_ID="$${VECTOR_STORE_ID}"} \
Expand Down
124 changes: 124 additions & 0 deletions agents/langgraph/templates/agentic_rag/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,130 @@ make undeploy

See [OpenShift Deployment](../../../../docs/openshift-deployment.md) for more details.

## Authentication

The agent supports optional native OpenShift authentication using Kubernetes
ServiceAccount tokens. When enabled, every request (except `/health`) must
carry a valid `Authorization: Bearer <token>` header. The token is verified
in-cluster via the Kubernetes `TokenReview` API.

Authentication is **disabled by default**.

### Prerequisites

- The agent must be deployed on OpenShift (TokenReview requires in-cluster access)
- `helm` and `oc` CLI tools

### Step 1 — Deploy the agent without auth

Make sure the agent is running first:

```bash
make build-openshift
# Set CONTAINER_IMAGE in .env to the value printed by the build
make deploy
```

Verify it works:

```bash
ROUTE=$(oc get route langgraph-agentic-rag -o jsonpath='{.spec.host}')
curl -s https://$ROUTE/health
curl -s https://$ROUTE/chat/completions \
-X POST -H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"hello"}]}'
```

### Step 2 — Create a caller ServiceAccount

Create a ServiceAccount that will act as the authorized client:

```bash
oc create serviceaccount my-caller
```

### Step 3 — Enable auth

Upgrade the Helm release with auth flags. Do **not** pass `-f values.yaml` here —
`--reuse-values` preserves the existing configuration from `make deploy`:

```bash
helm upgrade langgraph-agentic-rag ../../deployment \
--reuse-values \
--set auth.enabled=true \
--set auth.audience="langgraph-agentic-rag" \
--set-string "auth.allowedServiceAccounts[0]=<namespace>:my-caller" \
--set serviceAccount.create=true \
--set auth.createAuthDelegatorBinding=true
```

Replace `<namespace>` with your OpenShift project name
(e.g. `myproject:my-caller`).

Wait for the rollout:

```bash
oc rollout status deployment/langgraph-agentic-rag
```

### Step 4 — Test auth enforcement

```bash
ROUTE=$(oc get route langgraph-agentic-rag -o jsonpath='{.spec.host}')

# 1. Health endpoint — always open (excluded from auth)
curl -s https://$ROUTE/health
# Expected: 200 {"status": "healthy", ...}

# 2. Without token — should return 401
curl -s https://$ROUTE/chat/completions \
-X POST -H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"hello"}]}'
# Expected: 401 {"detail": "Missing Bearer token"}

# 3. With a valid SA token — should return 200
TOKEN=$(oc create token my-caller --audience=langgraph-agentic-rag)
curl -s https://$ROUTE/chat/completions \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"hello"}]}'
# Expected: 200 with assistant response
```

### Step 5 — Disable auth if needed

Disable auth and confirm the agent works without tokens:

```bash
helm upgrade langgraph-agentic-rag ../../deployment \
--reuse-values \
--set auth.enabled=false
```

### Auth configuration reference

| Helm value | Description |
|---|---|
| `auth.enabled` | Enable/disable auth (`false` by default) |
| `auth.audience` | Token audience the agent expects (e.g. `langgraph-agentic-rag`) |
| `auth.allowedServiceAccounts` | Comma-separated list of `namespace:sa-name` pairs allowed to call the agent |
| `serviceAccount.create` | Create a ServiceAccount for the agent pod |
| `auth.createAuthDelegatorBinding` | Grant the agent's SA permission to call the TokenReview API |

### Local development with auth

To test the auth middleware locally (outside the cluster):

```bash
uv sync --extra auth
python -c "from agent_auth.middleware import SATokenAuthMiddleware; print('OK')"
```

> **Note:** The middleware calls the Kubernetes TokenReview API, so
> `AUTH_ENABLED=true` only works when running inside an OpenShift cluster.
> For local development, leave `AUTH_ENABLED` unset or set to `false`.

## Tests

```bash
Expand Down
26 changes: 26 additions & 0 deletions agents/langgraph/templates/agentic_rag/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,28 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
)


def _auth_enabled() -> bool:
return getenv("AUTH_ENABLED", "false").strip().lower() == "true"


def _configure_auth_middleware() -> None:
if not _auth_enabled():
return

try:
from agent_auth.middleware import SATokenAuthMiddleware
except ModuleNotFoundError as exc:
raise RuntimeError(
"AUTH_ENABLED=true, but auth middleware dependencies are not installed. "
"Run `uv sync --extra auth` to enable ServiceAccount token auth locally."
) from exc

app.add_middleware(SATokenAuthMiddleware)


_configure_auth_middleware()


def _build_langchain_messages(messages: list[ChatMessage]) -> list[HumanMessage]:
"""Extract the last user message from the OpenAI-format messages list."""
for msg in reversed(messages):
Expand Down Expand Up @@ -442,12 +464,16 @@ async def health():
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def playground():
"""Serve the playground chat UI."""
if _auth_enabled():
raise HTTPException(status_code=404, detail="Not found")
return FileResponse(_PLAYGROUND_HTML)


@app.get("/images/{filename:path}", include_in_schema=False)
async def serve_image(filename: str):
"""Serve images from the project-level images directory."""
if _auth_enabled():
raise HTTPException(status_code=404, detail="Not found")
base = _IMAGES_DIR.resolve()
file_path = (base / filename).resolve()
try:
Expand Down
6 changes: 6 additions & 0 deletions agents/langgraph/templates/agentic_rag/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ dependencies = [
]

[project.optional-dependencies]
auth = [
"agent-auth",
]
dev = [
"pytest>=9.0.2",
]
Expand All @@ -36,6 +39,9 @@ tracing = [
[tool.setuptools.packages.find]
where = ["src"]

[tool.uv.sources]
agent-auth = { path = "../../../../components/auth" }

[build-system]
requires = ["setuptools>=80.9.0"]
build-backend = "setuptools.build_meta"
Loading
Loading