805 lines
34 KiB
Python
805 lines
34 KiB
Python
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlmodel import Session, SQLModel, create_engine
|
|
|
|
|
|
@pytest.fixture
|
|
def identity_client(tmp_path, monkeypatch):
|
|
from backend.app.core.config import settings
|
|
from backend.app.db import session as database
|
|
from backend.app.main import app
|
|
|
|
engine = create_engine(f"sqlite:///{tmp_path / 'identities.db'}", connect_args={"check_same_thread": False})
|
|
monkeypatch.setattr(database, "engine", engine)
|
|
monkeypatch.setenv("APPOPS_REQUIRE_OPERATOR_AUTH", "false")
|
|
monkeypatch.setattr(settings, "operator_proxy_auth_enabled", False)
|
|
monkeypatch.setattr(settings, "auth_admin_subjects", ["authentik:owner"], raising=False)
|
|
monkeypatch.setattr(settings, "auth_admin_emails", ["jens@example.com"], raising=False)
|
|
SQLModel.metadata.create_all(engine)
|
|
# Avoid unrelated schedulers and external connectors in boundary tests.
|
|
from backend.app.db.init_db import init_db
|
|
init_db()
|
|
yield TestClient(app, client=("198.18.10.20", 42000))
|
|
engine.dispose()
|
|
|
|
|
|
def sign_in(client, subject, **claims):
|
|
from backend.app.db import session as database
|
|
from backend.app.services.user_access import issue_session, provision_user
|
|
|
|
with Session(database.engine) as db:
|
|
user = provision_user(db, "authentik", "https://auth.example/application/o/appops/", {
|
|
"sub": subject, "email": f"{subject}@example.com", **claims,
|
|
})
|
|
cookie = issue_session(db, user)
|
|
user_id = user.id
|
|
client.cookies.set("appops_user_session", cookie)
|
|
return user_id
|
|
|
|
|
|
def test_shared_token_and_disabled_operator_switch_do_not_grant_access(identity_client):
|
|
client = identity_client
|
|
assert client.get("/api/apps").status_code == 401
|
|
assert client.post("/api/actions/plan", json={}, headers={"X-AppOps-Operator-Token": "old-shared-token"}).status_code == 401
|
|
assert client.get("/api/auth/session").json()["authenticated"] is False
|
|
assert client.get("/api/health").status_code == 200
|
|
|
|
|
|
def test_new_user_is_read_only_and_cannot_change_permissions(identity_client):
|
|
user_id = sign_in(identity_client, "reader")
|
|
assert identity_client.get("/api/apps").status_code == 200
|
|
status = identity_client.get("/api/auth/session").json()
|
|
assert status["user"]["role"] == "read_only"
|
|
assert status["permissions"] == ["read"]
|
|
for path in ["/api/actions/execute", "/api/voice/execute", "/api/settings/connectors/portainer", f"/api/auth/users/{user_id}"]:
|
|
assert identity_client.post(path, json={}, headers={"X-AppOps-Session-CSRF": "1"}).status_code == 403
|
|
assert identity_client.get("/api/auth/users").status_code == 403
|
|
|
|
|
|
def test_admin_can_manage_users_without_unlock_and_changes_apply_immediately(identity_client):
|
|
reader_id = sign_in(identity_client, "reader")
|
|
sign_in(identity_client, "owner")
|
|
status = identity_client.get("/api/auth/session").json()
|
|
assert status["user"]["role"] == "admin"
|
|
assert "users.manage" in status["permissions"]
|
|
assert identity_client.get("/api/auth/users").status_code == 200
|
|
update = identity_client.patch(f"/api/auth/users/{reader_id}", json={"role": "custom", "permissions": ["read", "inventory.manage"], "enabled": True}, headers={"X-AppOps-Session-CSRF": "1"})
|
|
assert update.status_code == 200
|
|
sign_in(identity_client, "reader")
|
|
assert identity_client.get("/api/auth/session").json()["permissions"] == ["inventory.manage", "read"]
|
|
assert identity_client.post("/api/actions/execute", json={}, headers={"X-AppOps-Session-CSRF": "1"}).status_code == 403
|
|
|
|
|
|
def test_csrf_logout_revocation_and_last_admin_protection(identity_client):
|
|
admin_id = sign_in(identity_client, "owner")
|
|
assert identity_client.patch(f"/api/auth/users/{admin_id}", json={"role": "read_only", "permissions": [], "enabled": True}, headers={"X-AppOps-Session-CSRF": "1"}).status_code == 409
|
|
assert identity_client.post("/api/auth/logout").status_code == 403
|
|
assert identity_client.post("/api/auth/logout", headers={"X-AppOps-Session-CSRF": "1"}).status_code == 200
|
|
assert identity_client.get("/api/apps").status_code == 401
|
|
|
|
|
|
def test_last_enabled_admin_guard_counts_another_enabled_admin(identity_client):
|
|
admin_id = sign_in(identity_client, "owner")
|
|
second_id = sign_in(identity_client, "second-admin")
|
|
sign_in(identity_client, "owner")
|
|
headers = {"X-AppOps-Session-CSRF": "1"}
|
|
|
|
promoted = identity_client.patch(
|
|
f"/api/auth/users/{second_id}",
|
|
json={"role": "admin", "enabled": True, "permissions": []},
|
|
headers=headers,
|
|
)
|
|
assert promoted.status_code == 200
|
|
|
|
disabled = identity_client.patch(
|
|
f"/api/auth/users/{second_id}",
|
|
json={"role": "admin", "enabled": False, "permissions": []},
|
|
headers=headers,
|
|
)
|
|
assert disabled.status_code == 200
|
|
|
|
last_admin = identity_client.patch(
|
|
f"/api/auth/users/{admin_id}",
|
|
json={"role": "admin", "enabled": False, "permissions": []},
|
|
headers=headers,
|
|
)
|
|
assert last_admin.status_code == 409
|
|
|
|
|
|
def test_session_expiration_and_forgery_fail_closed(identity_client):
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import UserSession
|
|
from backend.app.services.user_access import digest
|
|
sign_in(identity_client, "reader")
|
|
with Session(database.engine) as db:
|
|
record = db.get(UserSession, digest(identity_client.cookies.get("appops_user_session")))
|
|
record.expires_at = 1
|
|
db.add(record)
|
|
db.commit()
|
|
assert identity_client.get("/api/apps").status_code == 401
|
|
identity_client.cookies.set("appops_user_session", "forged")
|
|
assert identity_client.get("/api/apps").status_code == 401
|
|
|
|
|
|
def test_email_bootstrap_requires_verified_claim_and_is_pinned(identity_client):
|
|
sign_in(identity_client, "username-only", email=None, preferred_username="jens@example.com", email_verified=True)
|
|
assert identity_client.get("/api/auth/session").json()["user"]["role"] == "read_only"
|
|
sign_in(identity_client, "unverified", email="jens@example.com")
|
|
assert identity_client.get("/api/auth/session").json()["user"]["role"] == "read_only"
|
|
|
|
sign_in(identity_client, "verified", email="jens@example.com", email_verified=True)
|
|
assert identity_client.get("/api/auth/session").json()["user"]["role"] == "admin"
|
|
sign_in(identity_client, "replacement", email="jens@example.com", email_verified=True)
|
|
assert identity_client.get("/api/auth/session").json()["user"]["role"] == "read_only"
|
|
|
|
|
|
def test_bootstrap_recovers_early_login_without_restoring_demoted_admin(identity_client, monkeypatch):
|
|
from backend.app.core.config import settings
|
|
user_id = sign_in(identity_client, "early")
|
|
monkeypatch.setattr(settings, "auth_admin_subjects", ["authentik:early", "authentik:owner"])
|
|
sign_in(identity_client, "early")
|
|
assert identity_client.get("/api/auth/session").json()["user"]["role"] == "admin"
|
|
sign_in(identity_client, "owner")
|
|
response = identity_client.patch(f"/api/auth/users/{user_id}", json={"role": "read_only", "enabled": True, "permissions": []}, headers={"X-AppOps-Session-CSRF": "1"})
|
|
assert response.status_code == 200
|
|
sign_in(identity_client, "early")
|
|
assert identity_client.get("/api/auth/session").json()["user"]["role"] == "read_only"
|
|
|
|
monkeypatch.setattr(settings, "auth_admin_subjects", ["authentik:owner"])
|
|
sign_in(identity_client, "early", email="jens@example.com", email_verified=True)
|
|
assert identity_client.get("/api/auth/session").json()["user"]["role"] == "read_only"
|
|
|
|
|
|
def test_expired_disabled_and_forged_sessions_fail_closed(identity_client):
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import GatewayUser
|
|
user_id = sign_in(identity_client, "reader")
|
|
with Session(database.engine) as db:
|
|
user = db.get(GatewayUser, user_id)
|
|
user.enabled = False
|
|
db.add(user)
|
|
db.commit()
|
|
assert identity_client.get("/api/apps").status_code == 401
|
|
|
|
|
|
def test_proxy_headers_require_trusted_peer_and_fixed_uid(identity_client, monkeypatch):
|
|
from fastapi import HTTPException
|
|
from starlette.requests import Request
|
|
|
|
from backend.app.core.config import settings
|
|
from backend.app.services.user_access import current_user
|
|
monkeypatch.setattr(settings, "operator_proxy_auth_enabled", True)
|
|
monkeypatch.setattr(settings, "operator_proxy_trusted_networks", ["203.0.113.8/32"])
|
|
headers = [(b"x-authentik-uid", b"owner"), (b"x-authentik-username", b"jens"), (b"x-authentik-email", b"jens@example.com")]
|
|
def request(peer, supplied_headers):
|
|
return Request({"type": "http", "method": "GET", "path": "/api/apps", "headers": supplied_headers, "client": (peer, 1234)})
|
|
with pytest.raises(HTTPException) as rejected:
|
|
current_user(request("198.18.10.20", headers))
|
|
assert rejected.value.status_code == 401
|
|
assert current_user(request("203.0.113.8", headers)).role == "admin"
|
|
with pytest.raises(HTTPException) as rejected:
|
|
current_user(request("203.0.113.8", headers[1:]))
|
|
assert rejected.value.status_code == 401
|
|
|
|
|
|
def test_custom_voice_and_approval_permissions_do_not_grant_execution(identity_client):
|
|
reader_id = sign_in(identity_client, "reader")
|
|
sign_in(identity_client, "owner")
|
|
response = identity_client.patch(f"/api/auth/users/{reader_id}", json={"role": "custom", "enabled": True, "permissions": ["voice.use", "approvals.manage"]}, headers={"X-AppOps-Session-CSRF": "1"})
|
|
assert response.status_code == 200
|
|
sign_in(identity_client, "reader")
|
|
for path in ["/api/voice/actions/container/plex/restart", "/api/approvals/approval-1/execute", "/api/connectors/portainer/bind", "/api/ai/rag/reindex"]:
|
|
assert identity_client.post(path, json={}, headers={"X-AppOps-Session-CSRF": "1"}).status_code == 403
|
|
|
|
|
|
def test_operational_users_cannot_change_enrollment_trust(identity_client):
|
|
reader_id = sign_in(identity_client, "reader")
|
|
sign_in(identity_client, "owner")
|
|
headers = {"X-AppOps-Session-CSRF": "1"}
|
|
assert identity_client.patch(f"/api/auth/users/{reader_id}", json={"role": "operator", "enabled": True}, headers=headers).status_code == 200
|
|
sign_in(identity_client, "reader")
|
|
assert identity_client.put("/api/enrollments/trust", json={}, headers=headers).status_code == 403
|
|
sign_in(identity_client, "owner")
|
|
# Administrator reaches schema validation; an operator is rejected first.
|
|
assert identity_client.put("/api/enrollments/trust", json={}, headers=headers).status_code == 422
|
|
|
|
|
|
def _enrollment_trust_payload(*, appdata_root: str, backup_acknowledged: bool) -> dict:
|
|
return {
|
|
"controller_identity": "unraid-controller",
|
|
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
|
|
"trusted_appdata_root": appdata_root,
|
|
"managed_appdata_backup_acknowledged": backup_acknowledged,
|
|
"approved_networks": ["itworx-mcp-hub-npm"],
|
|
"allowed_domain_suffixes": ["example.com"],
|
|
"autonomous_profiles": ["repopulse-authenticated"],
|
|
"repopulse_proxy_access_list_id": 42,
|
|
"oidc_issuer": None,
|
|
"oidc_role_claim": "roles",
|
|
"administrator_group": None,
|
|
"support_agent_group": None,
|
|
"read_only_group": None,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("initial_root", "initial_ack", "requested_root", "requested_ack"),
|
|
[
|
|
(
|
|
"/srv/example/appdata",
|
|
False,
|
|
"/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
True,
|
|
),
|
|
(
|
|
"/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
True,
|
|
"/srv/example/appdata",
|
|
False,
|
|
),
|
|
],
|
|
ids=["initial-managed-selection", "managed-root-reset"],
|
|
)
|
|
def test_settings_manage_put_cannot_change_storage_root(
|
|
identity_client, initial_root, initial_ack, requested_root, requested_ack
|
|
):
|
|
from sqlmodel import Session, select
|
|
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import AppSetting, AuditEvent
|
|
|
|
reader_id = sign_in(identity_client, "reader")
|
|
sign_in(identity_client, "owner")
|
|
headers = {"X-AppOps-Session-CSRF": "1"}
|
|
assert identity_client.patch(
|
|
f"/api/auth/users/{reader_id}",
|
|
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
|
|
headers=headers,
|
|
).status_code == 200
|
|
|
|
before = _enrollment_trust_payload(
|
|
appdata_root=initial_root, backup_acknowledged=initial_ack
|
|
)
|
|
with Session(database.engine) as db:
|
|
db.add(AppSetting(key="deployment_enrollment:trust", value_json=before, is_secret=False))
|
|
db.commit()
|
|
audit_ids_before = set(db.exec(select(AuditEvent.id)).all())
|
|
|
|
sign_in(identity_client, "reader")
|
|
response = identity_client.put(
|
|
"/api/enrollments/trust",
|
|
json=_enrollment_trust_payload(
|
|
appdata_root=requested_root, backup_acknowledged=requested_ack
|
|
),
|
|
headers=headers,
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
with Session(database.engine) as db:
|
|
saved = db.get(AppSetting, "deployment_enrollment:trust")
|
|
assert saved is not None and saved.value_json == before
|
|
assert set(db.exec(select(AuditEvent.id)).all()) == audit_ids_before
|
|
|
|
|
|
def test_settings_manage_put_cannot_create_initial_managed_trust(identity_client):
|
|
from sqlmodel import Session, select
|
|
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import AppSetting, AuditEvent
|
|
|
|
reader_id = sign_in(identity_client, "reader")
|
|
sign_in(identity_client, "owner")
|
|
headers = {"X-AppOps-Session-CSRF": "1"}
|
|
assert identity_client.patch(
|
|
f"/api/auth/users/{reader_id}",
|
|
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
|
|
headers=headers,
|
|
).status_code == 200
|
|
with Session(database.engine) as db:
|
|
audit_ids_before = set(db.exec(select(AuditEvent.id)).all())
|
|
|
|
sign_in(identity_client, "reader")
|
|
response = identity_client.put(
|
|
"/api/enrollments/trust",
|
|
json=_enrollment_trust_payload(
|
|
appdata_root="/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
backup_acknowledged=True,
|
|
),
|
|
headers=headers,
|
|
)
|
|
assert response.status_code == 403
|
|
with Session(database.engine) as db:
|
|
assert db.get(AppSetting, "deployment_enrollment:trust") is None
|
|
assert set(db.exec(select(AuditEvent.id)).all()) == audit_ids_before
|
|
|
|
|
|
def test_settings_manage_put_can_create_initial_legacy_trust(identity_client):
|
|
from sqlmodel import Session
|
|
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import AppSetting
|
|
|
|
reader_id = sign_in(identity_client, "reader")
|
|
sign_in(identity_client, "owner")
|
|
headers = {"X-AppOps-Session-CSRF": "1"}
|
|
assert identity_client.patch(
|
|
f"/api/auth/users/{reader_id}",
|
|
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
|
|
headers=headers,
|
|
).status_code == 200
|
|
sign_in(identity_client, "reader")
|
|
|
|
legacy = _enrollment_trust_payload(
|
|
appdata_root="/srv/example/appdata", backup_acknowledged=False
|
|
)
|
|
response = identity_client.put("/api/enrollments/trust", json=legacy, headers=headers)
|
|
assert response.status_code == 200
|
|
with Session(database.engine) as db:
|
|
saved = db.get(AppSetting, "deployment_enrollment:trust")
|
|
assert saved is not None
|
|
assert saved.value_json["trusted_appdata_root"] == "/srv/example/appdata"
|
|
assert saved.value_json["managed_appdata_backup_acknowledged"] is False
|
|
|
|
|
|
def test_settings_manage_put_keeps_other_trust_edits_and_admin_can_select_managed_root(identity_client):
|
|
from sqlmodel import Session
|
|
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import AppSetting
|
|
|
|
reader_id = sign_in(identity_client, "reader")
|
|
sign_in(identity_client, "owner")
|
|
headers = {"X-AppOps-Session-CSRF": "1"}
|
|
assert identity_client.patch(
|
|
f"/api/auth/users/{reader_id}",
|
|
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
|
|
headers=headers,
|
|
).status_code == 200
|
|
legacy = _enrollment_trust_payload(
|
|
appdata_root="/srv/example/appdata", backup_acknowledged=False
|
|
)
|
|
with Session(database.engine) as db:
|
|
db.add(AppSetting(key="deployment_enrollment:trust", value_json=legacy, is_secret=False))
|
|
db.commit()
|
|
|
|
sign_in(identity_client, "reader")
|
|
unrelated_update = {
|
|
**legacy,
|
|
"approved_networks": ["itworx-mcp-hub-npm", "itworx-npm"],
|
|
}
|
|
response = identity_client.put(
|
|
"/api/enrollments/trust", json=unrelated_update, headers=headers
|
|
)
|
|
assert response.status_code == 200
|
|
with Session(database.engine) as db:
|
|
saved = db.get(AppSetting, "deployment_enrollment:trust")
|
|
assert saved is not None
|
|
assert saved.value_json["approved_networks"] == ["itworx-mcp-hub-npm", "itworx-npm"]
|
|
assert saved.value_json["trusted_appdata_root"] == "/srv/example/appdata"
|
|
assert saved.value_json["managed_appdata_backup_acknowledged"] is False
|
|
|
|
sign_in(identity_client, "owner")
|
|
managed = _enrollment_trust_payload(
|
|
appdata_root="/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
backup_acknowledged=True,
|
|
)
|
|
response = identity_client.put("/api/enrollments/trust", json=managed, headers=headers)
|
|
assert response.status_code == 200
|
|
with Session(database.engine) as db:
|
|
saved = db.get(AppSetting, "deployment_enrollment:trust")
|
|
assert saved is not None
|
|
assert saved.value_json["trusted_appdata_root"] == managed["trusted_appdata_root"]
|
|
assert saved.value_json["managed_appdata_backup_acknowledged"] is True
|
|
|
|
|
|
def test_settings_manage_put_cas_preserves_concurrent_admin_storage_selection(identity_client):
|
|
from sqlalchemy import event
|
|
from sqlmodel import Session, select
|
|
|
|
from backend.app.core.time import utc_now
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import AppSetting, AuditEvent
|
|
from backend.app.services.enrollment_plane import TRUST_SETTING_KEY
|
|
|
|
reader_id = sign_in(identity_client, "reader")
|
|
sign_in(identity_client, "owner")
|
|
headers = {"X-AppOps-Session-CSRF": "1"}
|
|
assert identity_client.patch(
|
|
f"/api/auth/users/{reader_id}",
|
|
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
|
|
headers=headers,
|
|
).status_code == 200
|
|
|
|
original = _enrollment_trust_payload(
|
|
appdata_root="/srv/example/appdata", backup_acknowledged=False
|
|
)
|
|
with database.engine.connect() as connection:
|
|
connection.exec_driver_sql("PRAGMA journal_mode=WAL")
|
|
with Session(database.engine) as db:
|
|
db.add(AppSetting(key=TRUST_SETTING_KEY, value_json=original, is_secret=False))
|
|
db.commit()
|
|
audit_ids_before = set(db.exec(select(AuditEvent.id)).all())
|
|
|
|
sign_in(identity_client, "reader")
|
|
injected = False
|
|
|
|
def select_managed_root_before_put_write(
|
|
_connection, _cursor, statement, parameters, _context, _executemany
|
|
):
|
|
nonlocal injected
|
|
normalized = statement.lstrip().upper()
|
|
if (
|
|
not injected
|
|
and normalized.startswith("UPDATE APP_SETTINGS")
|
|
and TRUST_SETTING_KEY in str(parameters)
|
|
):
|
|
injected = True
|
|
with Session(database.engine) as concurrent_session:
|
|
row = concurrent_session.get(AppSetting, TRUST_SETTING_KEY)
|
|
assert row is not None
|
|
row.value_json = {
|
|
**row.value_json,
|
|
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
"managed_appdata_backup_acknowledged": True,
|
|
}
|
|
row.updated_at = utc_now()
|
|
concurrent_session.add(row)
|
|
concurrent_session.commit()
|
|
|
|
event.listen(database.engine, "before_cursor_execute", select_managed_root_before_put_write)
|
|
try:
|
|
response = identity_client.put(
|
|
"/api/enrollments/trust",
|
|
json={
|
|
**original,
|
|
"approved_networks": ["itworx-mcp-hub-npm", "itworx-npm"],
|
|
},
|
|
headers=headers,
|
|
)
|
|
finally:
|
|
event.remove(database.engine, "before_cursor_execute", select_managed_root_before_put_write)
|
|
|
|
assert injected is True
|
|
assert response.status_code == 409
|
|
with Session(database.engine) as db:
|
|
saved = db.get(AppSetting, TRUST_SETTING_KEY)
|
|
assert saved is not None
|
|
assert saved.value_json["trusted_appdata_root"] == "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
|
assert saved.value_json["managed_appdata_backup_acknowledged"] is True
|
|
assert saved.value_json["approved_networks"] == original["approved_networks"]
|
|
assert set(db.exec(select(AuditEvent.id)).all()) == audit_ids_before
|
|
|
|
|
|
def test_settings_manage_initial_legacy_put_loses_if_admin_creates_trust_concurrently(identity_client):
|
|
from sqlalchemy import event
|
|
from sqlmodel import Session, select
|
|
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import AppSetting, AuditEvent
|
|
from backend.app.services.enrollment_plane import TRUST_SETTING_KEY
|
|
|
|
reader_id = sign_in(identity_client, "reader")
|
|
sign_in(identity_client, "owner")
|
|
headers = {"X-AppOps-Session-CSRF": "1"}
|
|
assert identity_client.patch(
|
|
f"/api/auth/users/{reader_id}",
|
|
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
|
|
headers=headers,
|
|
).status_code == 200
|
|
with database.engine.connect() as connection:
|
|
connection.exec_driver_sql("PRAGMA journal_mode=WAL")
|
|
with Session(database.engine) as db:
|
|
audit_ids_before = set(db.exec(select(AuditEvent.id)).all())
|
|
|
|
sign_in(identity_client, "reader")
|
|
admin_value = _enrollment_trust_payload(
|
|
appdata_root="/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
backup_acknowledged=True,
|
|
)
|
|
injected = False
|
|
|
|
def create_admin_trust_before_put_insert(
|
|
_connection, _cursor, statement, parameters, _context, _executemany
|
|
):
|
|
nonlocal injected
|
|
normalized = statement.lstrip().upper()
|
|
if (
|
|
not injected
|
|
and normalized.startswith("INSERT INTO APP_SETTINGS")
|
|
and TRUST_SETTING_KEY in str(parameters)
|
|
):
|
|
injected = True
|
|
with Session(database.engine) as concurrent_session:
|
|
concurrent_session.add(
|
|
AppSetting(key=TRUST_SETTING_KEY, value_json=admin_value, is_secret=False)
|
|
)
|
|
concurrent_session.commit()
|
|
|
|
event.listen(database.engine, "before_cursor_execute", create_admin_trust_before_put_insert)
|
|
try:
|
|
response = identity_client.put(
|
|
"/api/enrollments/trust",
|
|
json=_enrollment_trust_payload(
|
|
appdata_root="/srv/example/appdata", backup_acknowledged=False
|
|
),
|
|
headers=headers,
|
|
)
|
|
finally:
|
|
event.remove(database.engine, "before_cursor_execute", create_admin_trust_before_put_insert)
|
|
|
|
assert injected is True
|
|
assert response.status_code == 409
|
|
with Session(database.engine) as db:
|
|
saved = db.get(AppSetting, TRUST_SETTING_KEY)
|
|
assert saved is not None and saved.value_json == admin_value
|
|
assert set(db.exec(select(AuditEvent.id)).all()) == audit_ids_before
|
|
|
|
|
|
def test_storage_root_patch_is_admin_only_and_preserves_other_trust_fields(identity_client):
|
|
from sqlmodel import Session, select
|
|
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import AppSetting, AuditEvent
|
|
|
|
sign_in(identity_client, "owner")
|
|
original = {
|
|
"controller_identity": "unraid-controller",
|
|
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
|
|
"trusted_appdata_root": "/srv/example/appdata",
|
|
"managed_appdata_backup_acknowledged": False,
|
|
"approved_networks": ["itworx-mcp-hub-npm"],
|
|
"allowed_domain_suffixes": ["example.com"],
|
|
"autonomous_profiles": ["repopulse-authenticated"],
|
|
"repopulse_proxy_access_list_id": 42,
|
|
"oidc_issuer": None,
|
|
"oidc_role_claim": "roles",
|
|
"administrator_group": None,
|
|
"support_agent_group": None,
|
|
"read_only_group": None,
|
|
}
|
|
with Session(database.engine) as db:
|
|
db.add(AppSetting(key="deployment_enrollment:trust", value_json=original, is_secret=False))
|
|
db.commit()
|
|
|
|
headers = {"X-AppOps-Session-CSRF": "1"}
|
|
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
|
updated = identity_client.patch(
|
|
"/api/enrollments/trust/storage-root",
|
|
json={"trusted_appdata_root": managed_root, "managed_appdata_backup_acknowledged": True},
|
|
headers=headers,
|
|
)
|
|
assert updated.status_code == 200
|
|
assert updated.json()["trustedAppdataRoot"] == managed_root
|
|
assert updated.json()["managedAppdataBackupAcknowledged"] is True
|
|
|
|
with Session(database.engine) as db:
|
|
saved = db.get(AppSetting, "deployment_enrollment:trust")
|
|
assert saved is not None
|
|
assert saved.value_json == {
|
|
**original,
|
|
"trusted_appdata_root": managed_root,
|
|
"managed_appdata_backup_acknowledged": True,
|
|
}
|
|
event = db.exec(
|
|
select(AuditEvent).where(AuditEvent.action_id == "enrollment.trust.storage_root.update")
|
|
).one()
|
|
assert event.before_state == {
|
|
"trustedAppdataRoot": "/srv/example/appdata",
|
|
"managedAppdataBackupAcknowledged": False,
|
|
}
|
|
assert event.after_state == {
|
|
"trustedAppdataRoot": managed_root,
|
|
"managedAppdataBackupAcknowledged": True,
|
|
}
|
|
assert event.redacted_metadata == {
|
|
"backupLocation": "dockerRootDir",
|
|
"requestSource": "manual",
|
|
}
|
|
assert "bootstrap_secret_ref" not in str(event.before_state)
|
|
|
|
assert identity_client.patch(
|
|
"/api/enrollments/trust/storage-root",
|
|
json={"trusted_appdata_root": managed_root, "managed_appdata_backup_acknowledged": False},
|
|
headers=headers,
|
|
).status_code == 422
|
|
assert identity_client.patch(
|
|
"/api/enrollments/trust/storage-root",
|
|
json={
|
|
"trusted_appdata_root": "/srv/example/other",
|
|
"managed_appdata_backup_acknowledged": False,
|
|
},
|
|
headers=headers,
|
|
).status_code == 422
|
|
assert identity_client.patch(
|
|
"/api/enrollments/trust/storage-root",
|
|
json={
|
|
"trusted_appdata_root": managed_root,
|
|
"managed_appdata_backup_acknowledged": True,
|
|
"bootstrap_secret_ref": "LEAKED_SECRET_REFERENCE",
|
|
},
|
|
headers=headers,
|
|
).status_code == 422
|
|
|
|
legacy = identity_client.patch(
|
|
"/api/enrollments/trust/storage-root",
|
|
json={"trusted_appdata_root": "/srv/example/appdata", "managed_appdata_backup_acknowledged": False},
|
|
headers=headers,
|
|
)
|
|
assert legacy.status_code == 200
|
|
assert legacy.json()["trustedAppdataRoot"] == "/srv/example/appdata"
|
|
assert legacy.json()["managedAppdataBackupAcknowledged"] is False
|
|
|
|
|
|
def test_storage_root_patch_returns_conflict_after_concurrent_trust_change(identity_client, monkeypatch):
|
|
from sqlmodel import Session, select
|
|
|
|
from backend.app.core.time import utc_now
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import AppSetting, AuditEvent
|
|
from backend.app.schemas.enrollment import EnrollmentTrustConfigV1
|
|
from backend.app.services.enrollment_plane import EnrollmentPlane
|
|
|
|
sign_in(identity_client, "owner")
|
|
original = {
|
|
"controller_identity": "unraid-controller",
|
|
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
|
|
"trusted_appdata_root": "/srv/example/appdata",
|
|
"managed_appdata_backup_acknowledged": False,
|
|
"approved_networks": ["itworx-mcp-hub-npm"],
|
|
"allowed_domain_suffixes": ["example.com"],
|
|
"autonomous_profiles": ["repopulse-authenticated"],
|
|
"repopulse_proxy_access_list_id": 42,
|
|
"oidc_issuer": None,
|
|
"oidc_role_claim": "roles",
|
|
"administrator_group": None,
|
|
"support_agent_group": None,
|
|
"read_only_group": None,
|
|
}
|
|
with Session(database.engine) as db:
|
|
db.add(AppSetting(key="deployment_enrollment:trust", value_json=original, is_secret=False))
|
|
db.commit()
|
|
|
|
stale_trust = EnrollmentTrustConfigV1.model_validate(original)
|
|
|
|
def read_stale_after_concurrent_update(_session):
|
|
with Session(database.engine) as concurrent_session:
|
|
row = concurrent_session.get(AppSetting, "deployment_enrollment:trust")
|
|
assert row is not None
|
|
row.value_json = {**row.value_json, "approved_networks": ["itworx-mcp-hub-npm", "itworx-npm"]}
|
|
row.updated_at = utc_now()
|
|
concurrent_session.add(row)
|
|
concurrent_session.commit()
|
|
return stale_trust
|
|
|
|
monkeypatch.setattr(
|
|
EnrollmentPlane,
|
|
"get_trust_config",
|
|
staticmethod(read_stale_after_concurrent_update),
|
|
)
|
|
response = identity_client.patch(
|
|
"/api/enrollments/trust/storage-root",
|
|
json={
|
|
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
"managed_appdata_backup_acknowledged": True,
|
|
},
|
|
headers={"X-AppOps-Session-CSRF": "1"},
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
with Session(database.engine) as db:
|
|
saved = db.get(AppSetting, "deployment_enrollment:trust")
|
|
assert saved is not None
|
|
assert saved.value_json["approved_networks"] == ["itworx-mcp-hub-npm", "itworx-npm"]
|
|
assert saved.value_json["trusted_appdata_root"] == "/srv/example/appdata"
|
|
assert db.exec(
|
|
select(AuditEvent).where(AuditEvent.action_id == "enrollment.trust.storage_root.update")
|
|
).all() == []
|
|
|
|
|
|
def test_storage_root_patch_cas_rejects_change_after_snapshot(identity_client, monkeypatch):
|
|
from sqlmodel import Session, select
|
|
|
|
from backend.app.api import routes_enrollments
|
|
from backend.app.core.time import utc_now
|
|
from backend.app.db import session as database
|
|
from backend.app.db.models import AppSetting, AuditEvent
|
|
|
|
sign_in(identity_client, "owner")
|
|
original = {
|
|
"controller_identity": "unraid-controller",
|
|
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
|
|
"trusted_appdata_root": "/srv/example/appdata",
|
|
"managed_appdata_backup_acknowledged": False,
|
|
"approved_networks": ["itworx-mcp-hub-npm"],
|
|
"allowed_domain_suffixes": ["example.com"],
|
|
"autonomous_profiles": ["repopulse-authenticated"],
|
|
"repopulse_proxy_access_list_id": 42,
|
|
"oidc_issuer": None,
|
|
"oidc_role_claim": "roles",
|
|
"administrator_group": None,
|
|
"support_agent_group": None,
|
|
"read_only_group": None,
|
|
}
|
|
with database.engine.connect() as connection:
|
|
connection.exec_driver_sql("PRAGMA journal_mode=WAL")
|
|
with Session(database.engine) as db:
|
|
db.add(AppSetting(key="deployment_enrollment:trust", value_json=original, is_secret=False))
|
|
db.commit()
|
|
|
|
actual_sql_update = routes_enrollments.sql_update
|
|
concurrent_write_done = False
|
|
|
|
def write_concurrently_after_snapshot(model):
|
|
nonlocal concurrent_write_done
|
|
if model is AppSetting and not concurrent_write_done:
|
|
concurrent_write_done = True
|
|
with Session(database.engine) as concurrent_session:
|
|
row = concurrent_session.get(AppSetting, "deployment_enrollment:trust")
|
|
assert row is not None
|
|
row.value_json = {
|
|
**row.value_json,
|
|
"approved_networks": ["itworx-mcp-hub-npm", "itworx-npm"],
|
|
}
|
|
row.updated_at = utc_now()
|
|
concurrent_session.add(row)
|
|
concurrent_session.commit()
|
|
return actual_sql_update(model)
|
|
|
|
# The endpoint has already read and validated its selector snapshot when it
|
|
# calls sql_update. Commit an independent trust edit at that boundary so the
|
|
# route's compare-and-swap must reject the now-stale write.
|
|
monkeypatch.setattr(routes_enrollments, "sql_update", write_concurrently_after_snapshot)
|
|
response = identity_client.patch(
|
|
"/api/enrollments/trust/storage-root",
|
|
json={
|
|
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
"managed_appdata_backup_acknowledged": True,
|
|
},
|
|
headers={"X-AppOps-Session-CSRF": "1"},
|
|
)
|
|
|
|
assert concurrent_write_done is True
|
|
assert response.status_code == 409
|
|
with Session(database.engine) as db:
|
|
saved = db.get(AppSetting, "deployment_enrollment:trust")
|
|
assert saved is not None
|
|
assert saved.value_json["approved_networks"] == ["itworx-mcp-hub-npm", "itworx-npm"]
|
|
assert saved.value_json["trusted_appdata_root"] == "/srv/example/appdata"
|
|
assert db.exec(
|
|
select(AuditEvent).where(AuditEvent.action_id == "enrollment.trust.storage_root.update")
|
|
).all() == []
|
|
|
|
|
|
def test_read_only_and_operator_cannot_change_storage_root(identity_client):
|
|
reader_id = sign_in(identity_client, "reader")
|
|
sign_in(identity_client, "owner")
|
|
headers = {"X-AppOps-Session-CSRF": "1"}
|
|
assert identity_client.patch(
|
|
f"/api/auth/users/{reader_id}",
|
|
json={"role": "operator", "enabled": True, "permissions": []},
|
|
headers=headers,
|
|
).status_code == 200
|
|
|
|
selection = {
|
|
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
"managed_appdata_backup_acknowledged": True,
|
|
}
|
|
sign_in(identity_client, "reader")
|
|
assert identity_client.patch(
|
|
"/api/enrollments/trust/storage-root", json=selection, headers=headers
|
|
).status_code == 403
|
|
sign_in(identity_client, "owner")
|
|
assert identity_client.patch(
|
|
f"/api/auth/users/{reader_id}",
|
|
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
|
|
headers=headers,
|
|
).status_code == 200
|
|
sign_in(identity_client, "reader")
|
|
assert identity_client.patch(
|
|
"/api/enrollments/trust/storage-root", json=selection, headers=headers
|
|
).status_code == 403
|
|
sign_in(identity_client, "read-only")
|
|
assert identity_client.patch(
|
|
"/api/enrollments/trust/storage-root", json=selection, headers=headers
|
|
).status_code == 403
|