From 01b5f80ef9392458942128dcb6c2a4c74f0f11b8 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Sun, 12 Apr 2026 14:23:44 +0200 Subject: [PATCH 1/4] fix: restrict script deploy/execute to analyst role, undeploy to admin --- tests/test_security.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_security.py b/tests/test_security.py index a2d242b..a446100 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -281,6 +281,50 @@ class TestAuthSecurity: assert resp.status_code == 401 +# ---- Script RBAC ---- + +@pytest.fixture +def viewer_client(tmp_path, monkeypatch): + """TestClient with a viewer-role user seeded.""" + monkeypatch.setenv("DATA_DIR", str(tmp_path)) + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-32chars-minimum!!!!!") + monkeypatch.setenv("SCRIPT_TIMEOUT", "5") + + from app.main import create_app + from src.db import get_system_db + from src.repositories.users import UserRepository + from app.auth.jwt import create_access_token + from fastapi.testclient import TestClient + + conn = get_system_db() + UserRepository(conn).create(id="viewer1", email="viewer@test.com", name="Viewer", role="viewer") + conn.close() + + app = create_app() + c = TestClient(app) + token = create_access_token(user_id="viewer1", email="viewer@test.com", role="viewer") + return c, token + + +class TestScriptRBAC: + def test_viewer_cannot_run_scripts(self, viewer_client): + """Viewers should not be able to execute scripts.""" + c, token = viewer_client + headers = {"Authorization": f"Bearer {token}"} + resp = c.post("/api/scripts/run", json={ + "name": "test", "source": "print('hi')" + }, headers=headers) + assert resp.status_code == 403 + + def test_viewer_cannot_deploy_scripts(self, viewer_client): + c, token = viewer_client + headers = {"Authorization": f"Bearer {token}"} + resp = c.post("/api/scripts/deploy", json={ + "name": "test", "source": "print('hi')", "schedule": "" + }, headers=headers) + assert resp.status_code == 403 + + # ---- JWT Claims ---- class TestJwtClaims: From 31e210c7e3cc7c69772c456a586efaa0f9482b78 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Sun, 12 Apr 2026 14:23:47 +0200 Subject: [PATCH 2/4] fix: require admin/km_admin role for web admin pages --- tests/test_web_ui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_web_ui.py b/tests/test_web_ui.py index bcc9f59..8cf0daf 100644 --- a/tests/test_web_ui.py +++ b/tests/test_web_ui.py @@ -109,3 +109,7 @@ class TestAdminRoleGuards: def test_admin_can_access_admin_permissions(self, web_client, admin_cookie): resp = web_client.get("/admin/permissions", cookies=admin_cookie) assert resp.status_code == 200 + + def test_analyst_cannot_access_corporate_memory_admin(self, web_client, admin_cookie, analyst_cookie): + resp = web_client.get("/corporate-memory/admin", cookies=analyst_cookie) + assert resp.status_code == 403 From 209643becb482c7b1308e5d5082ac6abb8be181d Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Sun, 12 Apr 2026 14:23:51 +0200 Subject: [PATCH 3/4] fix: return filename instead of absolute path in upload responses --- tests/test_api_complete.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_api_complete.py b/tests/test_api_complete.py index 56595fb..e906644 100644 --- a/tests/test_api_complete.py +++ b/tests/test_api_complete.py @@ -237,3 +237,16 @@ class TestUpload: headers=_h(client["admin"]), ) assert resp.status_code == 413 + + def test_upload_does_not_leak_absolute_path(self, client): + """Upload response should not contain absolute filesystem paths.""" + import io + resp = client["client"].post( + "/api/upload/artifacts", + files={"file": ("test.txt", io.BytesIO(b"hello"), "text/plain")}, + headers=_h(client["admin"]), + ) + assert resp.status_code == 200 + data = resp.json() + assert not data.get("path", "").startswith("/"), "Response should not leak absolute path" + assert "filename" in data, "Response should contain filename" From 325f785ef418b54b64061f79db2139cbccd8f772 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Sun, 12 Apr 2026 14:23:54 +0200 Subject: [PATCH 4/4] fix: get_instance_name reads nested instance.name from YAML --- tests/test_instance_config.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_instance_config.py b/tests/test_instance_config.py index 62f197d..9b4d1ff 100644 --- a/tests/test_instance_config.py +++ b/tests/test_instance_config.py @@ -10,3 +10,27 @@ class TestInstanceConfig: from app.instance_config import get_instance_name name = get_instance_name() assert isinstance(name, str) + + def test_reads_nested_instance_name(self, tmp_path, monkeypatch): + """get_instance_name should read instance.name from YAML, not flat instance_name.""" + monkeypatch.setenv("DATA_DIR", str(tmp_path)) + monkeypatch.setenv("TESTING", "1") + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-min-32-characters!!") + + state_dir = tmp_path / "state" + state_dir.mkdir(exist_ok=True) + (state_dir / "instance.yaml").write_text( + "instance:\n name: Acme Analytics\n subtitle: Data Team\n" + ) + + import importlib + import app.instance_config as mod + # Reset cached config to force reload + mod._instance_config = None + importlib.reload(mod) + + assert mod.get_instance_name() == "Acme Analytics" + assert mod.get_instance_subtitle() == "Data Team" + + # Cleanup: reset cache after test + mod._instance_config = None