Repository navigation
ThinkWatch v2.2.0
Images
ghcr.io/thinkwatchproject/think-watch-server:2.2.0ghcr.io/thinkwatchproject/think-watch-web:2.2.0
Helm
helm install think-watch ./think-watch-2.2.0.tgz \
--namespace thinkwatch --create-namespaceThis release fixes authorization. The gateways never checked
ai_gateway:use or mcp_gateway:use, so a user with no role, or with
only roles that grant neither, could call every model and MCP tool; a
role granted at the scope of one team widened model and tool access
platform-wide; and an API key whose allow-list came out empty could
call any model. The gateways now require the permission and count only
the roles that grant it. The seeded team_manager role now works when
granted at team scope, and seven permissions that nothing ever checked
are retired. Some users lose gateway access on upgrade: read the first
section before deploying. There is one manual database script and no
schema, setting, environment variable or Helm value change.
Read before upgrading
-
Users without a role that grants gateway use lose gateway access.
A request to the AI gateway is refused with403unless a role held
by the key's owner grantsai_gateway:use, and a request to the MCP
gateway unless one grantsmcp_gateway:use. The body names the
missing permission, in the error format of the caller's API. The
built-indeveloper,admin,super_adminandteam_managerroles
grant both;viewergrants neither. Three groups of users are
refused after the upgrade where 2.1.0 let them through:- users with no role at all;
- users whose roles are only
viewer, or custom roles without these
actions; - users whose only gateway role is assigned at team scope (see the
third item below).
Find them before upgrading. This query lists every active user with a
live key for a gateway that none of their roles will grant (it counts
global assignments and roles attached to the user's teams, which is
what the gateways now read; it does not account forDeny
statements):WITH grants AS ( SELECT id AS role_id, jsonb_path_exists(policy_document, '$.Statement[*] ? (@.Effect == "Allow").Action[*] ? (@ == "*" || @ == "ai_gateway:*" || @ == "ai_gateway:use")') AS ai, jsonb_path_exists(policy_document, '$.Statement[*] ? (@.Effect == "Allow").Action[*] ? (@ == "*" || @ == "mcp_gateway:*" || @ == "mcp_gateway:use")') AS mcp FROM rbac_roles ), held AS ( SELECT user_id, role_id FROM rbac_role_assignments WHERE scope_kind = 'global' UNION SELECT tm.user_id, tra.role_id FROM team_members tm JOIN team_role_assignments tra USING (team_id) ), access AS ( SELECT h.user_id, bool_or(g.ai) AS ai, bool_or(g.mcp) AS mcp FROM held h JOIN grants g USING (role_id) GROUP BY h.user_id ) SELECT u.email, s.surface FROM api_keys k JOIN users u ON u.id = k.user_id CROSS JOIN LATERAL unnest(k.surfaces) AS s(surface) LEFT JOIN access a ON a.user_id = u.id WHERE k.is_active AND k.deleted_at IS NULL AND u.is_active AND u.deleted_at IS NULL AND NOT COALESCE(CASE s.surface WHEN 'ai_gateway' THEN a.ai WHEN 'mcp_gateway' THEN a.mcp END, false) GROUP BY u.email, s.surface ORDER BY u.email, s.surface;
Give each of them
developer(or a custom role with the actions)
either at global scope (Users → edit the user's roles, scope
Global), or by attaching the role to a team they belong to (Teams →
the team → Roles), which every member of that team inherits. The
change takes effect within a minute; each user's permissions are
cached for 60 seconds.If SSO users are meant to use the gateway from their first sign-in,
set Default Role for New Users (auth.default_role) in Settings to
developer. It is empty by default, and it applies only to accounts
created after it is set; existing users need the grant above. -
A role that does not grant gateway use no longer widens model or
tool access. Model and MCP tool scopes are now the union over the
roles that grantai_gateway:use/mcp_gateway:useonly. A role
without those actions (such asviewer) used to count as
"unrestricted", so adding it to a user limited to some models opened
every model to them. Users relying on that lose the extra models. -
A role assigned at team scope no longer grants gateway access. An
assignment with scopeteam:<id>administers that team from the
console (team roster, team limits); it no longer contributes models,
MCP tools or gateway use, which it used to do for every request the
user made, member of the team or not. Roles attached to a team itself
(Teams → Roles), which every member inherits, still count. A user
whose only gateway role was a team-scopeddeveloperor
team_managerneeds that role at global scope, or attached to their
team. The role assignment editor now says this under the scope
picker. -
An empty model allow-list allows nothing. A key whose
allowed_modelsis[], or whose list shares no model with what its
owner's roles grant, used to call any model; it now calls none, as
allowed_mcp_tools: []already did on the MCP gateway. The console
never saves[](clearing the picker sendsnull), so only keys
written through the API are affected. To find them:
SELECT id, name, user_id FROM api_keys WHERE allowed_models = '{}' AND is_active AND deleted_at IS NULL;
Set such a key's list tonullto leave it bounded by its owner's
roles only. Model entries still match by prefix, and a key narrowed to
gpt-4o-miniunder a role grantinggpt-4okeepsgpt-4o-mini. -
Run
db/release_migrations/2026-09-30_retire_unchecked_permissions.sql
once, after deploying. The server does not apply it. It adds
teams:readto theteam_managerrole and removes the seven retired
permissions (see Changed) from every role, system and custom. It is
idempotent and changes no access for any other role, since nothing
checked the removed keys. Without it, the server still starts and
works, but:team_managerkeeps the oldteam:readit cannot use, so a team
manager who is not a member of the team they manage still gets
403opening it, its roster or its roles;- every start logs a warning listing the roles that still name
retired permissions.
Reset to defaults on a system role in the console has the same
effect for that one role, but does not clean custom roles. -
A team-scoped
rate_limits:writenow takes effect. It used to
require global scope for every subject, so the seededteam_manager
granted at team scope could not touch any limit. It now covers the
rate limits and budget caps of users in that team and of the API keys
they own, never the holder's own user or keys; role subjects still
need global scope. Anyone holdingteam_manager(or a custom role
withrate_limits:write) at team scope can now change, lift or
delete the limits of every member of that team, administrators
included. Review team-scoped assignments of these roles before
upgrading.
Security
- Gateway use is checked, and a missing grant means no access. See
the first three items above: a user with no role, or only roles
without gateway use, could call every model and MCP tool; a
viewer-style role widened a restricted user to every model; a role
granted at the scope of any team, even one the user was not a member
of, widened model and tool access platform-wide. An explicitDeny
onai_gateway:useormcp_gateway:usenow closes that gateway.
Action wildcards (ai_gateway:*,*) grant it, as they already did
for console permissions. - A key narrowed inside a prefix grant is no longer unrestricted.
The key's allow-list was intersected with its owner's role grants
entry by entry, as literal strings. A key limited togpt-4o-mini
under a role grantinggpt-4o(a prefix) came out with an empty list,
which the gateway read as "no restriction", so the key could call
every model. The intersection now keeps an entry of either side that
the other side covers, by prefix for models and by<server>__*
pattern for MCP tools, and an empty result allows nothing.
Fixed
- The seeded
team_managerrole works at team scope. It granted
team:read, but the team handlers checkteams:read, so a team
manager got403listing teams and opening the team, its roster or
its roles. It now grantsteams:read, and opening a team accepts
teams:readscoped to that team, where it used to require global
scope. Existing installations need the release migration above. - Bulk disable and delete work on API keys' limits. The bulk
disable and delete routes for rate-limit rules and budget caps
rejected every row stored for an API key (api_key_lineage, the kind
a key's limits are stored under) as an unknown subject kind. - The console's effective-permissions preview shows real gateway
access. It now counts only roles that grant gateway use and skips
team-scoped assignments, as the gateways do, where it used to count
every assigned role.
Changed
- Seven permissions that nothing checked are retired:
team:read,
team:write,logs:read_own,logs:read_team,
audit_logs:read_own,audit_logs:read_teamand
audit_logs:read_all. Every log endpoint, audit logs included, is
gated onlogs:read_allat global scope, and no own- or team-filtered
log view exists, so these grants never did anything. They are gone
from the permission catalog, the role editor and the seeded roles.
Roles that still name them load, and the server logs a warning at
start instead of refusing to boot. - Deleting one rate-limit rule or budget cap is bound to the subject
in the path.DELETEon
/api/admin/limits/{kind}/{id}/rules/{rule_id}and
/api/admin/limits/{kind}/{id}/budgets/{cap_id}now answers404when the rule or cap belongs to a different
subject; it used to delete any row id once the path's subject was
authorized. - Creating or editing a key with an explicit allow-list checks it
against gateway-granting roles only. Anallowed_modelsor
allowed_mcp_toolsentry is refused with400when no role of the
owner that grants the gateway covers it, so a user without a gateway
role can no longer save a non-empty list.