fix: let an admin use the connectors they implicitly hold
Nightly Build / build (push) Successful in 7m50s
Nightly Build / build (push) Successful in 7m50s
Activating a per-user connector as admin failed with "you are not
authorized to use this connector — ask an admin to enable it for you".
`db::access_defaults` deliberately writes no grant rows for admins, and
says why: "they already hold every plugin and connector implicitly, so a
row for them would be noise". That implicit hold was only ever
implemented for plugins (`plugin_access::effective_access`). The two MCP
grant tables had nothing but the raw junction read, so an admin ended up
with no row *and* no short-circuit — denied their own connectors, and
denied more the more the seeding was trusted to skip them.
The reported symptom was the mildest of four:
- `activate` refused, while `available` listed the entry (an admin
holds `mcp.manage_catalog`) — visible but unusable;
- the login-time startup filter dropped an admin's already-activated
catalog connectors, so they silently stopped running;
- `accessible_global` snapshotted an empty set, so an admin's sessions
were offered no shared MCP tools at all — no error, just absence;
- the connector report told the agent an admin's own global connector
was "not granted to you".
`users::is_admin` is now the single predicate behind every "admins hold
it implicitly" short-circuit, and `plugin_access` was moved onto it too:
three tables open-coding the same role lookup is what let one of them be
written without it. Each MCP table grows an `effective_access` beside its
`has_access`, and the distinction is the point — `has_access` stays the
roster question ("what did the admin tick"), which the access-editing
surfaces must keep asking, while the gates ask the authorization one.
Nothing widens for anyone else: deny-by-default is untouched for
non-admins, an unknown user is nobody, a disabled global stays excluded
for admins too, and the `not_granted` report branch survives for a
non-admin who was given the catalog-management capability.
This commit is contained in:
@@ -272,6 +272,24 @@ pub async fn count(pool: &SqlitePool) -> Result<i64> {
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Whether this user holds the admin role — the one predicate behind every
|
||||
/// "admins hold it implicitly" short-circuit (`plugin_access`,
|
||||
/// `mcp_catalog_access`, `mcp_global_access`).
|
||||
///
|
||||
/// It lives here, as one function, because the alternative is what actually
|
||||
/// happened: each grant table open-coded the role lookup, one of them was written
|
||||
/// without it, and admins were denied their own connectors while
|
||||
/// [`super::access_defaults`] skipped seeding them rows on the grounds that the
|
||||
/// short-circuit existed. An unknown user is not an admin; errors propagate so
|
||||
/// callers fail closed.
|
||||
pub async fn is_admin(pool: &SqlitePool, user_id: &str) -> Result<bool> {
|
||||
let role = sqlx::query_as::<_, (String,)>("SELECT role_id FROM users WHERE id = ?")
|
||||
.bind(user_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(matches!(role, Some((r,)) if r == super::roles::ADMIN_ROLE_ID))
|
||||
}
|
||||
|
||||
// ── Writes ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// `id` is supplied by the caller and must be opaque (never the username), so a
|
||||
|
||||
Reference in New Issue
Block a user