Skip to content

adapter: narrow ALTER CONNECTION authorization to dependent secrets - #1

Draft
jasonhernandez wants to merge 2 commits into
bosconi:jc/issue-SQL-662from
jasonhernandez:jason/sql-662-scope-fix
Draft

jasonhernandez wants to merge 2 commits into
bosconi:jc/issue-SQL-662from
jasonhernandez:jason/sql-662-scope-fix

Conversation

@jasonhernandez

Copy link
Copy Markdown

Stacked on MaterializeInc#38798 (targets jc/issue-SQL-662, not main). Two commits, both
droppable independently. Net effect on the eventual main diff is -24 lines,
while adding a regression test.

Motivation

I reviewed MaterializeInc#38798 from an RBAC angle. The rule it establishes is the right one:
redirecting a route should require a privilege that already lets you exfiltrate
the credentials flowing through it, so the check grants no new capability. Two
things looked worth changing before it lands.

1. The walk reaches past the credentials it protects

check_alter_connection_usage seeds from item_dependents(id), and
item_dependents includes id itself. Popping id expands the altered
connection's own resolved_ids, so the walk descends into the connections it
references and harvests their secrets.

Concretely, on the current branch:

-- joe owns kafka_conn, which names aws_conn; aws_conn holds a secret joe cannot use
ALTER CONNECTION kafka_conn SET (BROKER 'broker2:9092');
ERROR: permission denied for SECRET "materialize.public.aws_secret"

Before MaterializeInc#38798 that needed only USAGE on aws_conn. It closes no part of
SQL-662 (nothing here is a dependent), and it contradicts the delegation rule
stated two paragraphs above the new text in guide-adapter.md: USAGE on a
fixed connection runs VALIDATE CONNECTION without access to the secrets behind
it. It also lands in an odd middle ground, requiring USAGE on the leaf secret
but not on the intermediate connection holding it, since intermediates are
traversed and never added to usage_ids.

The fix pre-visits id so the walk never descends through it. That also retires
the dependency_id == id substitution, since the altered connection's final
definition is already in usage_ids. Removing an inaccessible dependency still
works, and dependent connections are still traversed in full.

2. The user doc leads with the remediation that undoes the fix

The remediation list opened with "Grant the route administrator access to those
secrets." Under the availability pressure of a shared tunnel that cannot be
altered, that is the path of least resistance, and it ends with route admins
holding USAGE on every team's secrets, which is the capability this change
exists to deny. Reordered so delegating the ALTER comes first and the broad
grant comes last with a caveat.

While in there, documented two boundaries the check deliberately leaves alone,
since both are load-bearing and neither was written down:

  • ROTATE KEYS stays ownership-only. It does not point dependents at a new
    endpoint, so it is correctly out of scope, but that carve-out is unpinned.
  • DROP ... CASCADE still needs no ownership of descendants
    (rbac.rs, Plan::DropObjects). A route owner cannot redirect a dependent's
    credentials but can still destroy the dependent. That is defensible, the
    boundary being confidentiality rather than availability, but the user doc now
    tells people to "remove the dependency", which is exactly that power.

Verification

test/sqllogictest/privilege_checks.slt gains a case for the delegation path:
joe owns ssh_route_kafka, holds USAGE on ssh_route_aws, and cannot use the
secret behind it. Confirmed red before the fix and green after:

# with the fix
PASS: success=699 total=699

# with the fix reverted
FAIL: output-failure=2 success=697 total=699
  [conn=joe] ALTER CONNECTION ssh_route_kafka SET (BROKER 'broker2:9092' USING SSH TUNNEL ssh_route)
    expected: COMPLETE 0
    actually: permission denied for SECRET "materialize.public.ssh_route_password"

The second failure in that reverted run is alter_conn_secret, the pre-existing
retained-dependency case from main, which is a useful check that the fix keeps
that property rather than trading one for the other.

The second commit collapses the AWS PrivateLink block, which repeats the SSH
block statement for statement. The walk never inspects a route's kind, so both
resolve through identical code. PrivateLink keeps the denial, the unchanged
catalog, and the success after the dependency is gone. Drop this commit if you
would rather keep the symmetry with the doc, which names both route types.

bin/fmt, cargo clippy -p mz-adapter --all-targets -D warnings, and bin/lint
are clean apart from the pre-existing zizmor and check-helm-docs failures you
already flagged.

Left for you

Not addressed here, listed so they are not lost:

  • The stage_finish re-check is the TOCTOU defense and nothing exercises it. With
    VALIDATE = true the request already fails at stage_ready, so showing the
    window needs a Rust integration test that revokes during validation.
  • item_dependents walks the entire downstream graph, every source, table, view,
    MV and index, then discards all non-connections, twice per ALTER. A used_by
    BFS restricted to connections is equivalent, since nothing but a connection can
    be used_by a connection, and it would share code with the walk in
    catalog_implications.rs::handle_alter_connection. Those two walks have to
    agree or a redirect propagates somewhere it was never authorized. I left this
    alone to keep the diff small.
  • The error names a fully-qualified secret the caller has no USAGE on, possibly
    in a schema they cannot see. Naming the dependent connection instead would say
    as much without the enumeration oracle.
  • No release note. One team owning a shared tunnel while another owns the app
    secrets is a real deployment shape, and this is the first rule where an action
    on your own object is blocked by another role's object.

🤖 Generated with Claude Code

jasonhernandez and others added 2 commits September 11, 2026 17:35
`check_alter_connection_usage` seeds its worklist from `item_dependents(id)`,
which includes `id` itself. Popping `id` expanded the altered connection's own
`resolved_ids`, so the walk descended into the connections it references and
collected their secrets.

That reaches past the credentials the check exists to protect. Altering a Kafka
connection that names an AWS connection now demanded USAGE on the AWS
connection's secret, where USAGE on the AWS connection alone had been enough.
It is the same delegation that lets `USAGE` on a fixed connection run
`VALIDATE CONNECTION` without access to the secrets behind it, which
`guide-adapter.md` states two paragraphs above the rule this added.

Pre-visit `id` so the walk never descends through it. Its final definition is
already in `usage_ids`, so a caller can still remove a dependency it cannot
use, and dependent connections are still traversed in full.

Also document the boundaries the check deliberately leaves alone: ROTATE KEYS
does not point dependents at a new endpoint, and DROP ... CASCADE still needs
no ownership of descendants. The user-facing remediation list no longer leads
with granting the route owner USAGE on the secrets, which resolves the error by
handing over the access the restriction exists to withhold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The AWS PrivateLink block repeats the SSH block statement for statement. The
authorization walk never inspects a route's kind, so the transitive, inherited,
ownership, and validation cases resolve through identical code either way.

Keep the exhaustive sequence once, on SSH, and reduce PrivateLink to the denial,
the unchanged catalog, and the success after the dependency is gone, which is
what shows the path is reached at all. Net effect is 45 fewer lines in a file
whose objects persist for the rest of the run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant