mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-01 18:52:21 +01:00
fix(settings): enforce authoritative global image-gen policy and non-admin preference persistence
- Enforce authoritative administrator global image_gen_enabled disable in get_user_setting, do_generate_image, and image_editing_provider_available, preventing non-admin user preference overrides while preserving user-disabled opt-outs and can_generate_images privilege checks across direct, gallery, and editor paths.
- Filter non-admin preference persistence in saveImagePreferences to user preference keys only (image_model, image_edit_model, image_quality), preventing global policy toggles from leaking into user preferences.
- Return accurate 200 OK Response from saveImagePreferences after fallback persistence succeeds so settings UI displays 'Saved' instead of false 403 error, while retaining visibility of real errors.
- Load per-user preferences via readImagePreferences in initImageSettings so non-admin selections persist and display correctly after page reload.
- Clear per-user model preferences on successful admin save (POST /api/auth/settings) via PUT /api/prefs/{key} with value: null to prevent stale per-user preferences from shadowing updated global settings.
- Support clearing keys in PUT /api/prefs/{key} when value is null.
- Add comprehensive Python regression tests for global-disabled/user-enabled, global-enabled/user-disabled, and authorized generation, plus Node and browser regression tests for non-admin persistence, toggle isolation, reload, and admin clearing.
This commit is contained in:
@@ -293,3 +293,208 @@ def test_is_background_removal_intent_grammar_and_dos_safety():
|
||||
assert not _is_background_removal_intent(evil_invalid)
|
||||
elapsed_invalid = time.perf_counter() - start
|
||||
assert elapsed_invalid < 0.1, f"Invalid intent parsing took too long: {elapsed_invalid:.4f}s"
|
||||
|
||||
|
||||
def test_image_generation_global_disabled_user_enabled_rejected(tmp_path, monkeypatch):
|
||||
import asyncio
|
||||
from fastapi import HTTPException
|
||||
from routes.gallery import gallery_routes as gallery
|
||||
from routes import editor_agent_routes, prefs_routes
|
||||
from src import ai_interaction, settings
|
||||
|
||||
prefs_file = str(tmp_path / "user_prefs.json")
|
||||
monkeypatch.setattr(prefs_routes, "PREFS_FILE", prefs_file)
|
||||
|
||||
# Admin globally disables image generation
|
||||
monkeypatch.setattr(settings, "load_settings", lambda: {"image_gen_enabled": False, "image_model": "gpt-image-1"})
|
||||
|
||||
# User 'alice' has image_gen_enabled = True in her preferences
|
||||
prefs_routes._save_for_user("alice", {"image_gen_enabled": True, "image_model": "alice-model"})
|
||||
|
||||
# 1. get_user_setting must be False despite user preference
|
||||
assert settings.get_user_setting("image_gen_enabled", owner="alice", default=True) is False
|
||||
|
||||
# 2. image_editing_provider_available must be False
|
||||
assert ai_interaction.image_editing_provider_available(owner="alice") is False
|
||||
|
||||
# 3. Direct generation must be rejected with admin disabled message
|
||||
direct_res = asyncio.run(ai_interaction.do_generate_image(
|
||||
json.dumps({"prompt": "a dog"}), owner="alice"
|
||||
))
|
||||
assert "error" in direct_res
|
||||
assert "disabled by the administrator" in direct_res["error"]
|
||||
|
||||
# 4. Gallery route must reject with 503
|
||||
app = FastAPI()
|
||||
app.include_router(gallery.setup_gallery_routes())
|
||||
monkeypatch.setattr(gallery, "require_privilege", lambda request, priv: "alice")
|
||||
with TestClient(app) as client:
|
||||
gal_res = client.post("/api/image/generate", json={"prompt": "a dog"})
|
||||
assert gal_res.status_code == 503
|
||||
assert "switched off" in gal_res.json()["detail"].lower()
|
||||
|
||||
# 5. Editor agent route must be blocked
|
||||
app_editor = FastAPI()
|
||||
app_editor.include_router(editor_agent_routes.setup_editor_agent_routes())
|
||||
monkeypatch.setattr(editor_agent_routes, "_user", lambda request: "alice")
|
||||
routes.set_editor_planner(lambda messages, owner: json.dumps({
|
||||
"reply": "Gen", "steps": [{"op": "generate_image", "params": {"prompt": "a dog"}}]
|
||||
}))
|
||||
try:
|
||||
with TestClient(app_editor) as client:
|
||||
ed_res = client.post("/api/editor/agent", json={"prompt": "create a dog"})
|
||||
assert ed_res.status_code == 200
|
||||
data = ed_res.json()
|
||||
assert data.get("blocked") is True
|
||||
assert data.get("needs_provider") is True
|
||||
finally:
|
||||
routes.set_editor_planner(None)
|
||||
|
||||
|
||||
def test_image_generation_global_enabled_user_disabled_rejected(tmp_path, monkeypatch):
|
||||
import asyncio
|
||||
from routes.gallery import gallery_routes as gallery
|
||||
from routes import editor_agent_routes, prefs_routes
|
||||
from src import ai_interaction, settings
|
||||
|
||||
prefs_file = str(tmp_path / "user_prefs.json")
|
||||
monkeypatch.setattr(prefs_routes, "PREFS_FILE", prefs_file)
|
||||
|
||||
# Admin globally enables image generation
|
||||
monkeypatch.setattr(settings, "load_settings", lambda: {"image_gen_enabled": True, "image_model": "gpt-image-1"})
|
||||
|
||||
# User 'bob' has image_gen_enabled = False in his preferences
|
||||
prefs_routes._save_for_user("bob", {"image_gen_enabled": False, "image_model": "bob-model"})
|
||||
|
||||
# 1. get_user_setting must be False per user preference
|
||||
assert settings.get_user_setting("image_gen_enabled", owner="bob", default=True) is False
|
||||
|
||||
# 2. image_editing_provider_available must be False for bob
|
||||
assert ai_interaction.image_editing_provider_available(owner="bob") is False
|
||||
|
||||
# 3. Direct generation must be rejected with user preference message
|
||||
direct_res = asyncio.run(ai_interaction.do_generate_image(
|
||||
json.dumps({"prompt": "a cat"}), owner="bob"
|
||||
))
|
||||
assert "error" in direct_res
|
||||
assert "disabled by user preference" in direct_res["error"]
|
||||
|
||||
# 4. Gallery route must reject with 503
|
||||
app = FastAPI()
|
||||
app.include_router(gallery.setup_gallery_routes())
|
||||
monkeypatch.setattr(gallery, "require_privilege", lambda request, priv: "bob")
|
||||
with TestClient(app) as client:
|
||||
gal_res = client.post("/api/image/generate", json={"prompt": "a cat"})
|
||||
assert gal_res.status_code == 503
|
||||
|
||||
# 5. Editor agent route must be blocked for bob
|
||||
app_editor = FastAPI()
|
||||
app_editor.include_router(editor_agent_routes.setup_editor_agent_routes())
|
||||
monkeypatch.setattr(editor_agent_routes, "_user", lambda request: "bob")
|
||||
routes.set_editor_planner(lambda messages, owner: json.dumps({
|
||||
"reply": "Gen", "steps": [{"op": "generate_image", "params": {"prompt": "a cat"}}]
|
||||
}))
|
||||
try:
|
||||
with TestClient(app_editor) as client:
|
||||
ed_res = client.post("/api/editor/agent", json={"prompt": "create a cat"})
|
||||
assert ed_res.status_code == 200
|
||||
data = ed_res.json()
|
||||
assert data.get("blocked") is True
|
||||
assert data.get("needs_provider") is True
|
||||
finally:
|
||||
routes.set_editor_planner(None)
|
||||
|
||||
|
||||
def test_image_generation_authorized_generation_and_privilege_checks(tmp_path, monkeypatch):
|
||||
import asyncio
|
||||
from fastapi import HTTPException
|
||||
from routes.gallery import gallery_routes as gallery
|
||||
from routes import editor_agent_routes, prefs_routes
|
||||
from src import ai_interaction, settings
|
||||
|
||||
prefs_file = str(tmp_path / "user_prefs.json")
|
||||
monkeypatch.setattr(prefs_routes, "PREFS_FILE", prefs_file)
|
||||
|
||||
# Admin globally enables image generation
|
||||
monkeypatch.setattr(settings, "load_settings", lambda: {"image_gen_enabled": True, "image_model": "gpt-image-1"})
|
||||
|
||||
# User 'charlie' has image generation enabled (no disabling pref)
|
||||
prefs_routes._save_for_user("charlie", {"image_model": "charlie-gen"})
|
||||
|
||||
assert settings.get_user_setting("image_gen_enabled", owner="charlie", default=True) is True
|
||||
|
||||
# Check privilege enforcement: user lacking can_generate_images is forbidden
|
||||
def require_privilege_mock(request, privilege):
|
||||
if privilege == "can_generate_images":
|
||||
raise HTTPException(403, "Permission denied")
|
||||
return "charlie"
|
||||
|
||||
monkeypatch.setattr("src.auth_helpers.require_privilege", require_privilege_mock)
|
||||
monkeypatch.setattr(gallery, "require_privilege", require_privilege_mock)
|
||||
|
||||
# Gallery checks privilege and returns 403
|
||||
app = FastAPI()
|
||||
app.include_router(gallery.setup_gallery_routes())
|
||||
with TestClient(app) as client:
|
||||
gal_res = client.post("/api/image/generate", json={"prompt": "a bird"})
|
||||
assert gal_res.status_code == 403
|
||||
assert "Permission denied" in gal_res.text
|
||||
|
||||
# Editor checks privilege on AI content and raises 403
|
||||
app_editor = FastAPI()
|
||||
app_editor.include_router(editor_agent_routes.setup_editor_agent_routes())
|
||||
monkeypatch.setattr(editor_agent_routes, "_user", lambda request: "charlie")
|
||||
monkeypatch.setattr(editor_agent_routes, "_image_provider_available", lambda owner: True)
|
||||
routes.set_editor_planner(lambda messages, owner: json.dumps({
|
||||
"reply": "Gen", "steps": [{"op": "generate_image", "params": {"prompt": "a bird"}}]
|
||||
}))
|
||||
try:
|
||||
with TestClient(app_editor) as client:
|
||||
ed_res = client.post("/api/editor/agent", json={"prompt": "create a bird"})
|
||||
assert ed_res.status_code == 403
|
||||
finally:
|
||||
routes.set_editor_planner(None)
|
||||
|
||||
# Now grant privilege: generation proceeds
|
||||
monkeypatch.setattr("src.auth_helpers.require_privilege", lambda request, priv: "charlie")
|
||||
monkeypatch.setattr(gallery, "require_privilege", lambda request, priv: "charlie")
|
||||
seen = []
|
||||
async def fake_do_generate(content, **kwargs):
|
||||
seen.append(kwargs)
|
||||
return {"image_url": "data:image/png;base64,dGVzdA=="}
|
||||
monkeypatch.setattr(ai_interaction, "do_generate_image", fake_do_generate)
|
||||
|
||||
with TestClient(app) as client:
|
||||
gal_ok = client.post("/api/image/generate", json={"prompt": "a bird"})
|
||||
assert gal_ok.status_code == 200
|
||||
assert seen[0]["owner"] == "charlie"
|
||||
|
||||
|
||||
def test_put_pref_clears_key_when_value_is_null(tmp_path, monkeypatch):
|
||||
from routes import prefs_routes
|
||||
from src import settings
|
||||
|
||||
prefs_file = str(tmp_path / "user_prefs.json")
|
||||
monkeypatch.setattr(prefs_routes, "PREFS_FILE", prefs_file)
|
||||
|
||||
# Set initial preference
|
||||
prefs_routes._save_for_user("admin_user", {"image_model": "old-admin-model"})
|
||||
assert prefs_routes._load_for_user("admin_user").get("image_model") == "old-admin-model"
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(prefs_routes.setup_prefs_routes())
|
||||
monkeypatch.setattr("src.auth_helpers.get_current_user", lambda request: "admin_user")
|
||||
monkeypatch.setattr(prefs_routes, "get_current_user", lambda request: "admin_user")
|
||||
|
||||
with TestClient(app) as client:
|
||||
res = client.put("/api/prefs/image_model", json={"value": None})
|
||||
assert res.status_code == 200
|
||||
assert res.json() == {"key": "image_model", "value": None}
|
||||
|
||||
# Verify key is deleted from user's record
|
||||
user_prefs = prefs_routes._load_for_user("admin_user")
|
||||
assert "image_model" not in user_prefs
|
||||
|
||||
# Verify get_user_setting now falls back to global setting
|
||||
monkeypatch.setattr(settings, "load_settings", lambda: {"image_model": "new-global-model"})
|
||||
assert settings.get_user_setting("image_model", owner="admin_user") == "new-global-model"
|
||||
|
||||
Reference in New Issue
Block a user