From 545e6925650b9722258d642d706f0802b9f65d47 Mon Sep 17 00:00:00 2001 From: ghreprimand Date: Fri, 5 Jun 2026 13:27:10 -0500 Subject: [PATCH] fix(auth): distinguish empty model allowlists (#2938) Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com> --- core/auth.py | 2 ++ routes/chat_helpers.py | 8 +++-- static/js/admin.js | 26 +++++++++------- tests/test_chat_helpers.py | 64 +++++++++++++++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/core/auth.py b/core/auth.py index 3c7669de4..ed083b008 100644 --- a/core/auth.py +++ b/core/auth.py @@ -30,10 +30,12 @@ DEFAULT_PRIVILEGES = { "can_manage_memory": True, "max_messages_per_day": 0, "allowed_models": [], + "allowed_models_restricted": False, } # Admins get everything ADMIN_PRIVILEGES = {k: (True if isinstance(v, bool) else (0 if isinstance(v, int) else [])) for k, v in DEFAULT_PRIVILEGES.items()} +ADMIN_PRIVILEGES["allowed_models_restricted"] = False DEFAULT_AUTH_PATH = os.path.join( Path(__file__).parent.parent, "data", "auth.json" diff --git a/routes/chat_helpers.py b/routes/chat_helpers.py index 0929b699d..c62d3452d 100644 --- a/routes/chat_helpers.py +++ b/routes/chat_helpers.py @@ -75,7 +75,7 @@ def _enforce_chat_privileges(request, sess) -> None: allowlist, or HTTPException(429) if the user has hit their daily message cap. No-op for unauthenticated callers or when auth_manager is absent (single-user mode). Admins receive ADMIN_PRIVILEGES from get_privileges, - which means empty allowed_models / zero cap → no-op for them. + which means unrestricted allowed_models / zero cap -> no-op for them. """ try: user = get_current_user(request) @@ -88,8 +88,10 @@ def _enforce_chat_privileges(request, sess) -> None: return privs = auth_manager.get_privileges(user) or {} - allowed = privs.get("allowed_models") or [] - if allowed and sess.model and sess.model not in allowed: + allowed_raw = privs.get("allowed_models") + allowed = allowed_raw if isinstance(allowed_raw, list) else [] + restricted = bool(privs.get("allowed_models_restricted")) or bool(allowed) + if restricted and sess.model and sess.model not in allowed: raise HTTPException(403, f"Your account is not allowed to use model '{sess.model}'.") cap = int(privs.get("max_messages_per_day") or 0) diff --git a/static/js/admin.js b/static/js/admin.js index 5019096af..5211bf62d 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -87,8 +87,11 @@ async function loadUsers() { `; // Allowed models — checkbox list - const allowedSet = new Set((u.privileges && u.privileges.allowed_models) || []); - const allEmpty = allowedSet.size === 0; + const allowedModels = Array.isArray(u.privileges && u.privileges.allowed_models) + ? u.privileges.allowed_models + : []; + const allowedSet = new Set(allowedModels); + const modelsRestricted = !!(u.privileges && u.privileges.allowed_models_restricted); html += `
Allowed models @@ -97,7 +100,7 @@ async function loadUsers() { None
-
${allEmpty ? 'All models allowed (no restrictions)' : allowedSet.size + ' model(s) allowed'}
+
${!modelsRestricted ? 'All models allowed (no restrictions)' : (allowedSet.size === 0 ? 'No models allowed' : allowedSet.size + ' model(s) allowed')}
Loading models...
@@ -119,7 +122,7 @@ async function loadUsers() { // Load models list on first expand if (!_modelsLoaded && !privPanel.classList.contains('hidden')) { _modelsLoaded = true; - _loadModelsForUser(u.username, allowedSet, privPanel); + _loadModelsForUser(u.username, allowedSet, modelsRestricted, privPanel); } }); @@ -199,7 +202,7 @@ async function loadUsers() { } catch (e) { list.innerHTML = '
Failed to load users
'; } } -async function _loadModelsForUser(username, allowedSet, privPanel) { +async function _loadModelsForUser(username, allowedSet, modelsRestricted, privPanel) { const listEl = privPanel.querySelector(`.priv-models-list[data-user="${username}"]`); if (!listEl) return; try { @@ -216,9 +219,9 @@ async function _loadModelsForUser(username, allowedSet, privPanel) { listEl.innerHTML = 'No models available'; return; } - const allEmpty = allowedSet.size === 0; + let restricted = modelsRestricted; listEl.innerHTML = sortModelObjects(allModels).map(m => { - const checked = allEmpty || allowedSet.has(m.mid) ? 'checked' : ''; + const checked = !restricted || allowedSet.has(m.mid) ? 'checked' : ''; return `