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
2 changes: 1 addition & 1 deletion examples/deployments/basic-ollama/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ services:
port: 7889
external_port: 7889
auth:
enabled: false # set to true and provide DM_API_TOKEN in .env for production
enabled: false # set to true for protected direct access; DM_API_TOKEN optionally overrides the default internal service token

data_manager:
sources:
Expand Down
2 changes: 1 addition & 1 deletion examples/deployments/basic-openai/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ services:
port: 7889
external_port: 7889
auth:
enabled: false # set to true and provide DM_API_TOKEN in .env for production
enabled: false # set to true for protected direct access; DM_API_TOKEN optionally overrides the default internal service token

data_manager:
sources:
Expand Down
11 changes: 8 additions & 3 deletions src/interfaces/chat_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from src.utils.logging import get_logger
from src.utils.config_access import get_full_config, get_services_config, get_global_config, get_dynamic_config
from src.utils.config_service import ConfigService
from src.utils.internal_auth import build_data_manager_auth_headers, resolve_data_manager_service_token
from src.utils.sql import (
SQL_INSERT_CONVO, SQL_INSERT_FEEDBACK, SQL_INSERT_TIMING, SQL_QUERY_CONVO,
SQL_CREATE_CONVERSATION, SQL_UPDATE_CONVERSATION_TIMESTAMP,
Expand Down Expand Up @@ -2153,9 +2154,13 @@ def __init__(self, app, **configs):
dm_host = dm_config.get("hostname") or dm_config.get("host", "localhost")
dm_port = dm_config.get("port", 5001)
self.data_manager_url = f"http://{dm_host}:{dm_port}"
# API token for service-to-service auth with data-manager
dm_token = read_secret("DM_API_TOKEN") or None
self._dm_headers = {"Authorization": f"Bearer {dm_token}"} if dm_token else {}
# Internal auth for service-to-service calls into data-manager
self._dm_headers = build_data_manager_auth_headers()
_, dm_auth_source = resolve_data_manager_service_token()
if self._dm_headers:
logger.info("Data manager proxy auth initialized via %s", dm_auth_source)
else:
logger.warning("Data manager proxy auth unavailable; uploads may fail if data-manager auth is enabled")
logger.info(f"Data manager service URL: {self.data_manager_url}")

# Initialize authentication methods
Expand Down
8 changes: 7 additions & 1 deletion src/interfaces/uploader_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from src.data_manager.vectorstore.loader_utils import load_text_from_path
from src.interfaces.chat_app.document_utils import check_credentials
from src.utils.env import read_secret
from src.utils.internal_auth import resolve_data_manager_service_token
from src.utils.logging import get_logger
from src.data_manager.collectors.utils.catalog_postgres import _METADATA_COLUMN_MAP
from src.utils.config_access import get_full_config
Expand Down Expand Up @@ -58,7 +59,8 @@ def __init__(

self.auth_config = (self.services_config or {}).get("data_manager", {}).get("auth", {}) or {}
self.auth_enabled = bool(self.auth_config.get("enabled", False))
self.api_token = read_secret("DM_API_TOKEN") or None
self.api_token, self.api_token_source = resolve_data_manager_service_token()
self.api_token = self.api_token or None
self.admin_users = {
user.strip().lower()
for user in (self.auth_config.get("admins") or [])
Expand All @@ -69,6 +71,10 @@ def __init__(
self.salt = read_secret("UPLOADER_SALT")
self.accounts_path = self.global_config.get("ACCOUNTS_PATH")
if self.auth_enabled:
if self.api_token:
logger.info("Data-manager API auth initialized via %s", self.api_token_source)
else:
logger.warning("Data-manager auth is enabled but no internal API token could be resolved")
if not self.accounts_path:
logger.warning("ACCOUNTS_PATH not configured; only default auth account avilable. Set is as DM_ADMIN_PASSWD in your secrets file.")
self.auth_enabled = True
Expand Down
33 changes: 33 additions & 0 deletions src/utils/internal_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

import hashlib
from typing import Dict, Tuple

from src.utils.env import read_secret


def resolve_data_manager_service_token() -> Tuple[str, str]:
"""Return the internal token used by chat-app -> data-manager requests.

Prefer an explicit DM_API_TOKEN when provided. If it is absent, derive a
deterministic internal-only token from PG_PASSWORD so paired services can
still authenticate without extra configuration.
"""
explicit_token = read_secret("DM_API_TOKEN")
if explicit_token:
return explicit_token, "DM_API_TOKEN"

pg_password = read_secret("PG_PASSWORD")
if not pg_password:
return "", "missing"

digest = hashlib.sha256()
digest.update(b"archi:data-manager:")
digest.update(pg_password.encode("utf-8"))
return digest.hexdigest(), "derived-from-PG_PASSWORD"


def build_data_manager_auth_headers() -> Dict[str, str]:
"""Build auth headers for requests to the data-manager service."""
token, _ = resolve_data_manager_service_token()
return {"Authorization": f"Bearer {token}"} if token else {}
61 changes: 61 additions & 0 deletions tests/unit/test_internal_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import sys
from unittest.mock import MagicMock, patch

from flask import Flask


def _secret_reader(values):
def _read_secret(name, default=""):
return values.get(name, default)

return _read_secret


def test_resolve_data_manager_service_token_prefers_explicit_token():
from src.utils.internal_auth import resolve_data_manager_service_token

with patch("src.utils.internal_auth.read_secret", side_effect=_secret_reader({
"DM_API_TOKEN": "dm-explicit-token",
"PG_PASSWORD": "postgres-password",
})):
token, source = resolve_data_manager_service_token()

assert token == "dm-explicit-token"
assert source == "DM_API_TOKEN"


def test_resolve_data_manager_service_token_falls_back_to_pg_password():
from src.utils.internal_auth import resolve_data_manager_service_token

with patch("src.utils.internal_auth.read_secret", side_effect=_secret_reader({
"PG_PASSWORD": "postgres-password",
})):
token, source = resolve_data_manager_service_token()

assert token == "33d35de0f59af35976eb8d71af05e4da047068346ee196f078f0e7113ecd2328"
assert source == "derived-from-PG_PASSWORD"


def test_require_admin_accepts_internal_bearer_token():
from src.utils.internal_auth import resolve_data_manager_service_token

app = Flask(__name__)
app.secret_key = "test-secret"

with patch.dict(sys.modules, {"spacy": MagicMock()}):
from src.interfaces.uploader_app.app import FlaskAppWrapper

dummy_wrapper = type("DummyWrapper", (), {})()
dummy_wrapper.auth_enabled = True
with patch("src.utils.internal_auth.read_secret", side_effect=_secret_reader({
"PG_PASSWORD": "postgres-password",
})):
dummy_wrapper.api_token, dummy_wrapper.api_token_source = resolve_data_manager_service_token()

def handler():
return "ok"

protected = FlaskAppWrapper.require_admin(dummy_wrapper, handler)

with app.test_request_context("/", headers={"Authorization": f"Bearer {dummy_wrapper.api_token}"}):
assert protected() == "ok"