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:
@@ -837,6 +837,11 @@ pub async fn available(
|
||||
.ok_or_else(|| ApiError::unauthorized("unknown user"))?;
|
||||
let manages_catalog =
|
||||
role_capabilities::has(skald.db(), &user.role_id, role_capabilities::MANAGE_CATALOG).await?;
|
||||
// Admins hold every connector implicitly and are deliberately never given grant
|
||||
// rows (see `mcp_catalog_access::effective_access`), so every "may I use this"
|
||||
// answer below has to OR this in — otherwise the page shows an admin their own
|
||||
// connectors greyed out as unusable.
|
||||
let is_admin = skald_core::db::users::is_admin(skald.db(), &auth.user_id).await?;
|
||||
|
||||
let granted_catalog: std::collections::HashSet<String> =
|
||||
mcp_catalog_access::catalog_names_for_user(skald.db(), &auth.user_id).await?
|
||||
@@ -863,7 +868,7 @@ pub async fn available(
|
||||
// entry enabled for someone else becomes invisible and unmanageable.
|
||||
.filter(|r| manages_catalog || granted.contains(&r.name))
|
||||
.map(|r| GlobalView {
|
||||
can_use: granted.contains(&r.name),
|
||||
can_use: is_admin || granted.contains(&r.name),
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
catalog_name: r.catalog_name,
|
||||
@@ -919,7 +924,7 @@ pub async fn activate(
|
||||
// Deny-by-default per-user access: the admin must have granted this user
|
||||
// the connector (`mcp_catalog_access`). This is the real boundary — the
|
||||
// `available` list only hides it in the UI.
|
||||
if !mcp_catalog_access::has_access(skald.db(), cat_name, &auth.user_id).await? {
|
||||
if !mcp_catalog_access::effective_access(skald.db(), cat_name, &auth.user_id).await? {
|
||||
return Err(ApiError::forbidden(
|
||||
"you are not authorized to use this connector — ask an admin to enable it for you",
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user