Skip to content

Commit 8393571

Browse files
Add flag to skip OpenSearch security setup
Introduce OPENRAG_SKIP_OS_SECURITY_SETUP (env var, default false) in settings to allow deployments to opt out of OpenSearch security configuration when the platform manages roles/role-mappings externally. Use this flag in startup_orchestrator.startup_tasks and utils.opensearch_init.init_index to skip calling setup_opensearch_security and emit informative log lines; index creation still runs in init_index. Add unit tests to verify both the skipped and non-skipped behaviors and to ensure indices are still created when security setup is skipped.
1 parent bd4e7fc commit 8393571

6 files changed

Lines changed: 332 additions & 12 deletions

File tree

src/config/settings.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,27 @@
8080
"yes",
8181
)
8282

83+
# Skip the OpenSearch security context setup (roles, role mappings,
84+
# all_access admin pin). When true, OpenRAG assumes the security context
85+
# is managed externally (e.g., by Traefik in CPD or by a SaaS platform
86+
# operator).
87+
#
88+
# Default depends on OPENRAG_RUN_MODE:
89+
# * saas / on_prem (CPD) -> "true" (the platform owns the security context)
90+
# * anything else (oss) -> "false" (today's behaviour preserved)
91+
# An explicit OPENRAG_SKIP_OS_SECURITY_SETUP value always wins, so an
92+
# operator can force-enable the setup in SaaS for a one-off bootstrap.
93+
def _resolve_skip_os_security_default() -> str:
94+
run_mode = os.getenv("OPENRAG_RUN_MODE", "").strip().lower()
95+
if run_mode in ("saas", "on_prem"):
96+
return "true"
97+
return "false"
98+
99+
100+
OPENRAG_SKIP_OS_SECURITY_SETUP = os.getenv(
101+
"OPENRAG_SKIP_OS_SECURITY_SETUP", _resolve_skip_os_security_default()
102+
).lower() in ("true", "1", "yes")
103+
83104
# Enable FastAPI's `debug` mode (verbose tracebacks in HTTP error responses
84105
# on the FastAPI app instance). Named explicitly so it isn't confused with
85106
# logging-level "debug" or other unrelated debug flags.

src/services/startup_orchestrator.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from config.settings import (
1010
DISABLE_INGEST_WITH_LANGFLOW,
1111
FETCH_OPENRAG_DOCS_AT_STARTUP,
12+
OPENRAG_SKIP_OS_SECURITY_SETUP,
1213
clients,
1314
get_openrag_config,
1415
)
@@ -65,17 +66,26 @@ async def startup_tasks(services):
6566
# Index will be created after onboarding when we know the embedding model
6667
await wait_for_opensearch()
6768

68-
# Setup OpenSearch security (roles and mappings) after connection is established
69-
try:
70-
from utils.opensearch_utils import setup_opensearch_security
71-
72-
await setup_opensearch_security(clients.opensearch)
73-
logger.info("OpenSearch security configuration completed successfully")
74-
except Exception as e:
75-
logger.warning(
76-
"Failed to setup OpenSearch security configuration - continuing anyway",
77-
error=str(e),
69+
# Setup OpenSearch security (roles and mappings) after connection is established.
70+
# Skip entirely when the platform manages the security context externally
71+
# (SaaS / CPD): the call would otherwise either fail with 403/401 or
72+
# overwrite a curated config.
73+
if OPENRAG_SKIP_OS_SECURITY_SETUP:
74+
logger.info(
75+
"Skipping OpenSearch security setup at startup "
76+
"(OPENRAG_SKIP_OS_SECURITY_SETUP=true)"
7877
)
78+
else:
79+
try:
80+
from utils.opensearch_utils import setup_opensearch_security
81+
82+
await setup_opensearch_security(clients.opensearch)
83+
logger.info("OpenSearch security configuration completed successfully")
84+
except Exception as e:
85+
logger.warning(
86+
"Failed to setup OpenSearch security configuration - continuing anyway",
87+
error=str(e),
88+
)
7989

8090
if DISABLE_INGEST_WITH_LANGFLOW:
8191
await _ensure_opensearch_index()

src/utils/opensearch_init.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
API_KEYS_INDEX_NAME,
1111
IBM_AUTH_ENABLED,
1212
INDEX_BODY,
13+
OPENRAG_SKIP_OS_SECURITY_SETUP,
1314
PLATFORM_AUTH_DEV_MODE,
1415
clients,
1516
get_index_name,
@@ -99,9 +100,20 @@ async def init_index(opensearch_client=None, admin_username: str = None):
99100
try:
100101
await wait_for_opensearch(opensearch_client)
101102

102-
from utils.opensearch_utils import setup_opensearch_security
103+
# Skip security setup when the platform manages it externally
104+
# (SaaS / CPD). Index creation below still runs — SaaS / CPD
105+
# deployments still need indices, they just don't want OpenRAG
106+
# touching roles or role mappings.
107+
if OPENRAG_SKIP_OS_SECURITY_SETUP:
108+
logger.info(
109+
"Skipping OpenSearch security setup during init_index "
110+
"(OPENRAG_SKIP_OS_SECURITY_SETUP=true)",
111+
admin_username=admin_username,
112+
)
113+
else:
114+
from utils.opensearch_utils import setup_opensearch_security
103115

104-
await setup_opensearch_security(os_client, admin_username=admin_username)
116+
await setup_opensearch_security(os_client, admin_username=admin_username)
105117

106118
config = get_openrag_config()
107119
embedding_model = config.knowledge.embedding_model
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""OPENRAG_SKIP_OS_SECURITY_SETUP gates the startup_orchestrator call to
2+
setup_opensearch_security.
3+
4+
Two cases:
5+
* Flag false (default): setup_opensearch_security is invoked.
6+
* Flag true: it is NOT invoked, skip log line is emitted.
7+
"""
8+
9+
import sys
10+
from pathlib import Path
11+
from unittest.mock import AsyncMock, MagicMock, patch
12+
13+
import pytest
14+
15+
ROOT = Path(__file__).resolve().parent.parent.parent.parent
16+
SRC = ROOT / "src"
17+
if str(SRC) not in sys.path:
18+
sys.path.insert(0, str(SRC))
19+
20+
21+
def _services_stub():
22+
"""Minimal services dict — only models_service is touched before the
23+
OpenSearch security block, and we want its call to fail loudly so the
24+
test fails fast if it changes."""
25+
models = MagicMock()
26+
models.update_model_registry = AsyncMock()
27+
return {"models_service": models}
28+
29+
30+
@pytest.mark.asyncio
31+
async def test_setup_runs_when_flag_false(monkeypatch):
32+
"""Default (flag false): setup_opensearch_security is called."""
33+
import services.startup_orchestrator as orchestrator
34+
35+
monkeypatch.setattr(orchestrator, "OPENRAG_SKIP_OS_SECURITY_SETUP", False)
36+
monkeypatch.setattr(orchestrator, "DISABLE_INGEST_WITH_LANGFLOW", False)
37+
# IBM_AUTH_ENABLED is imported lazily inside startup_tasks().
38+
monkeypatch.setattr("config.settings.IBM_AUTH_ENABLED", False, raising=False)
39+
40+
setup_mock = AsyncMock()
41+
with patch(
42+
"utils.opensearch_utils.setup_opensearch_security", setup_mock
43+
), patch.object(orchestrator, "wait_for_opensearch", AsyncMock()), patch.object(
44+
orchestrator, "init_index", AsyncMock()
45+
), patch.object(
46+
orchestrator, "configure_alerting_security", AsyncMock()
47+
), patch.object(
48+
orchestrator, "_reingest_default_docs_on_upgrade_if_needed",
49+
AsyncMock(return_value=False),
50+
), patch.object(
51+
orchestrator, "_update_mcp_server_urls", AsyncMock()
52+
):
53+
# Force the post-security work to exit early — config.edited=False
54+
# short-circuits both the recovery init_index and the flow check.
55+
with patch.object(
56+
orchestrator, "get_openrag_config",
57+
MagicMock(return_value=MagicMock(edited=False, knowledge=MagicMock(embedding_model=None))),
58+
):
59+
services = _services_stub()
60+
services["task_service"] = MagicMock()
61+
services["document_service"] = MagicMock()
62+
services["langflow_file_service"] = MagicMock()
63+
services["session_manager"] = MagicMock()
64+
services["langflow_mcp_service"] = MagicMock()
65+
services["flows_service"] = MagicMock(
66+
ensure_flows_exist=AsyncMock(return_value=set())
67+
)
68+
await orchestrator.startup_tasks(services)
69+
70+
assert setup_mock.await_count == 1, (
71+
"setup_opensearch_security must run when OPENRAG_SKIP_OS_SECURITY_SETUP is false"
72+
)
73+
74+
75+
@pytest.mark.asyncio
76+
async def test_setup_skipped_when_flag_true(monkeypatch):
77+
"""Flag true: setup_opensearch_security is NOT called."""
78+
import services.startup_orchestrator as orchestrator
79+
80+
monkeypatch.setattr(orchestrator, "OPENRAG_SKIP_OS_SECURITY_SETUP", True)
81+
monkeypatch.setattr(orchestrator, "DISABLE_INGEST_WITH_LANGFLOW", False)
82+
monkeypatch.setattr("config.settings.IBM_AUTH_ENABLED", False, raising=False)
83+
84+
setup_mock = AsyncMock()
85+
# Spy on the orchestrator's bound logger so we can assert the skip line
86+
# was emitted without depending on caplog propagation through the
87+
# project's custom logger wrapper.
88+
logger_spy = MagicMock()
89+
monkeypatch.setattr(orchestrator, "logger", logger_spy)
90+
91+
with patch(
92+
"utils.opensearch_utils.setup_opensearch_security", setup_mock
93+
), patch.object(orchestrator, "wait_for_opensearch", AsyncMock()), patch.object(
94+
orchestrator, "init_index", AsyncMock()
95+
), patch.object(
96+
orchestrator, "configure_alerting_security", AsyncMock()
97+
), patch.object(
98+
orchestrator, "_reingest_default_docs_on_upgrade_if_needed",
99+
AsyncMock(return_value=False),
100+
), patch.object(
101+
orchestrator, "_update_mcp_server_urls", AsyncMock()
102+
):
103+
with patch.object(
104+
orchestrator, "get_openrag_config",
105+
MagicMock(return_value=MagicMock(edited=False, knowledge=MagicMock(embedding_model=None))),
106+
):
107+
services = _services_stub()
108+
services["task_service"] = MagicMock()
109+
services["document_service"] = MagicMock()
110+
services["langflow_file_service"] = MagicMock()
111+
services["session_manager"] = MagicMock()
112+
services["langflow_mcp_service"] = MagicMock()
113+
services["flows_service"] = MagicMock(
114+
ensure_flows_exist=AsyncMock(return_value=set())
115+
)
116+
await orchestrator.startup_tasks(services)
117+
118+
assert setup_mock.await_count == 0, (
119+
"setup_opensearch_security must NOT run when OPENRAG_SKIP_OS_SECURITY_SETUP is true"
120+
)
121+
info_messages = [call.args[0] for call in logger_spy.info.call_args_list if call.args]
122+
assert any(
123+
"Skipping OpenSearch security setup at startup" in msg
124+
for msg in info_messages
125+
), f"expected skip log line not emitted; got: {info_messages}"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""The OPENRAG_SKIP_OS_SECURITY_SETUP default flips with OPENRAG_RUN_MODE.
2+
3+
* saas / on_prem (CPD) -> default "true" (platform owns security context)
4+
* anything else -> default "false" (today's behaviour)
5+
6+
An explicit OPENRAG_SKIP_OS_SECURITY_SETUP always wins.
7+
"""
8+
9+
import sys
10+
from pathlib import Path
11+
12+
import pytest
13+
14+
ROOT = Path(__file__).resolve().parent.parent.parent
15+
SRC = ROOT / "src"
16+
if str(SRC) not in sys.path:
17+
sys.path.insert(0, str(SRC))
18+
19+
from config.settings import _resolve_skip_os_security_default # noqa: E402
20+
21+
22+
@pytest.mark.parametrize(
23+
"run_mode, expected",
24+
[
25+
("", "false"), # unset -> default oss path
26+
("oss", "false"),
27+
("OSS", "false"),
28+
("saas", "true"),
29+
("SaaS", "true"),
30+
("on_prem", "true"),
31+
("ON_PREM", "true"),
32+
("unknown-mode", "false"), # unrecognised falls back to false
33+
],
34+
)
35+
def test_default_resolves_from_run_mode(monkeypatch, run_mode, expected):
36+
if run_mode:
37+
monkeypatch.setenv("OPENRAG_RUN_MODE", run_mode)
38+
else:
39+
monkeypatch.delenv("OPENRAG_RUN_MODE", raising=False)
40+
assert _resolve_skip_os_security_default() == expected
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""OPENRAG_SKIP_OS_SECURITY_SETUP gates the init_index() call to
2+
setup_opensearch_security, but does NOT skip index creation.
3+
4+
Three cases:
5+
* Flag false (default): security setup IS called.
6+
* Flag true: security setup is NOT called.
7+
* Flag true: index creation still runs (docs / knowledge_filters /
8+
api_keys indices) so SaaS / CPD deployments still
9+
get usable indices.
10+
"""
11+
12+
import sys
13+
from pathlib import Path
14+
from unittest.mock import AsyncMock, MagicMock, patch
15+
16+
import pytest
17+
18+
ROOT = Path(__file__).resolve().parent.parent.parent
19+
SRC = ROOT / "src"
20+
if str(SRC) not in sys.path:
21+
sys.path.insert(0, str(SRC))
22+
23+
24+
def _fake_os_client():
25+
client = MagicMock()
26+
# Treat every indices.exists() as False so the create branch runs.
27+
client.indices.exists = AsyncMock(return_value=False)
28+
client.indices.create = AsyncMock()
29+
client.indices.get_settings = AsyncMock(return_value={})
30+
client.indices.put_settings = AsyncMock()
31+
client.cluster.put_settings = AsyncMock()
32+
return client
33+
34+
35+
@pytest.mark.asyncio
36+
async def test_security_setup_called_when_flag_false(monkeypatch):
37+
import utils.opensearch_init as init_mod
38+
39+
monkeypatch.setattr(init_mod, "OPENRAG_SKIP_OS_SECURITY_SETUP", False)
40+
monkeypatch.setattr(init_mod, "IBM_AUTH_ENABLED", False)
41+
monkeypatch.setattr(init_mod, "PLATFORM_AUTH_DEV_MODE", False)
42+
43+
os_client = _fake_os_client()
44+
setup_mock = AsyncMock()
45+
46+
with patch(
47+
"utils.opensearch_utils.setup_opensearch_security", setup_mock
48+
), patch.object(init_mod, "wait_for_opensearch", AsyncMock()), patch.object(
49+
init_mod, "create_index_body", AsyncMock(return_value={"settings": {}, "mappings": {}})
50+
), patch.object(init_mod, "get_index_name", return_value="documents"):
51+
await init_mod.init_index(opensearch_client=os_client, admin_username="alice")
52+
53+
assert setup_mock.await_count == 1
54+
args, kwargs = setup_mock.await_args
55+
assert args[0] is os_client
56+
assert kwargs.get("admin_username") == "alice"
57+
58+
59+
@pytest.mark.asyncio
60+
async def test_security_setup_skipped_when_flag_true(monkeypatch):
61+
import utils.opensearch_init as init_mod
62+
63+
monkeypatch.setattr(init_mod, "OPENRAG_SKIP_OS_SECURITY_SETUP", True)
64+
monkeypatch.setattr(init_mod, "IBM_AUTH_ENABLED", False)
65+
monkeypatch.setattr(init_mod, "PLATFORM_AUTH_DEV_MODE", False)
66+
67+
os_client = _fake_os_client()
68+
setup_mock = AsyncMock()
69+
# Spy the bound logger; the project's wrapper doesn't propagate to caplog.
70+
logger_spy = MagicMock()
71+
monkeypatch.setattr(init_mod, "logger", logger_spy)
72+
73+
with patch(
74+
"utils.opensearch_utils.setup_opensearch_security", setup_mock
75+
), patch.object(init_mod, "wait_for_opensearch", AsyncMock()), patch.object(
76+
init_mod, "create_index_body", AsyncMock(return_value={"settings": {}, "mappings": {}})
77+
), patch.object(init_mod, "get_index_name", return_value="documents"):
78+
await init_mod.init_index(opensearch_client=os_client, admin_username="bob")
79+
80+
assert setup_mock.await_count == 0
81+
info_messages = [call.args[0] for call in logger_spy.info.call_args_list if call.args]
82+
assert any(
83+
"Skipping OpenSearch security setup during init_index" in msg
84+
for msg in info_messages
85+
), f"expected skip log line not emitted; got: {info_messages}"
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_index_creation_still_runs_when_flag_true(monkeypatch):
90+
"""Skipping security setup must NOT skip index creation."""
91+
import utils.opensearch_init as init_mod
92+
93+
monkeypatch.setattr(init_mod, "OPENRAG_SKIP_OS_SECURITY_SETUP", True)
94+
monkeypatch.setattr(init_mod, "IBM_AUTH_ENABLED", False)
95+
monkeypatch.setattr(init_mod, "PLATFORM_AUTH_DEV_MODE", False)
96+
97+
os_client = _fake_os_client()
98+
99+
with patch(
100+
"utils.opensearch_utils.setup_opensearch_security", AsyncMock()
101+
), patch.object(init_mod, "wait_for_opensearch", AsyncMock()), patch.object(
102+
init_mod, "create_index_body", AsyncMock(return_value={"settings": {}, "mappings": {}})
103+
), patch.object(init_mod, "get_index_name", return_value="documents"):
104+
await init_mod.init_index(opensearch_client=os_client)
105+
106+
# The three indices: documents, knowledge_filters, api_keys.
107+
created_indices = {call.kwargs.get("index") for call in os_client.indices.create.await_args_list}
108+
assert "documents" in created_indices
109+
assert "knowledge_filters" in created_indices
110+
assert "api_keys" in created_indices, (
111+
"api_keys index creation must still run when security setup is skipped"
112+
)

0 commit comments

Comments
 (0)