Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions doc/developer/guide-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,17 @@ every secret reachable through those connections, including connections with no
active source or sink. Route ownership alone does not delegate authority over
dependent credentials. This check precedes secret reads and validation and runs
again before persistence, because dependents and grants can change during
external validation. Dependency traversal uses the altered connection's final
definition so removing an inaccessible dependency remains possible.
external validation. Authorization covers the altered connection's own final
definition, so removing an inaccessible dependency remains possible, but the
walk does not descend through it. Descending would require `USAGE` on secrets
held by the caller's own dependency connections, which is exactly what the
delegation rule above grants without.

`ALTER CONNECTION ... ROTATE KEYS` is deliberately outside this check. Rotating
a tunnel's key pair does not point dependent connections at a new endpoint, so
ownership remains sufficient. Note that `DROP ... CASCADE` is likewise
unrestricted: a route owner cannot redirect a dependent's credentials but can
still destroy the dependent. The boundary is confidentiality, not availability.

### The catalog is the source of truth for state that gets rebuilt from it

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ headless: true
Owning a shared SSH tunnel or AWS PrivateLink connection does not grant authority
to redirect credentials used by its dependent connections. A dependent connection
whose secrets the route owner cannot use prevents that owner from altering route
options. Grant the route administrator access to those secrets, have an authorized
role alter the route, or remove the dependency before altering it. `USAGE` on an
unchanged connection still permits `VALIDATE CONNECTION` without access to its
secrets.
options. To resolve that, have a role that can use those secrets alter the route,
or remove the dependency first. Granting the route owner `USAGE` on the secrets
also works, but it hands them the access this restriction exists to withhold, so
prefer it only where that access is already intended.

`USAGE` on an unchanged connection still permits `VALIDATE CONNECTION` without
access to its secrets, and altering a connection never requires `USAGE` on
secrets held by the connections it references, only on those its own definition
names.
21 changes: 10 additions & 11 deletions src/adapter/src/coord/sequencer/inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3891,6 +3891,14 @@ impl Coordinator {
// A route's own definition can contain no secrets even though changing
// it redirects credentials in dependent connections. Include unused
// connections because delegated validation can activate them later.
//
// `id` starts out visited so the walk never descends through the
// altered connection itself. Its final definition is already in
// `usage_ids`, and expanding it would demand USAGE on secrets that
// USAGE on a fixed connection deliberately delegates, which is a
// restriction on the caller's own connection rather than on the
// credentials this check exists to protect.
let mut visited = BTreeSet::from([id]);
let mut pending: Vec<_> = catalog
.item_dependents(id)
.into_iter()
Expand All @@ -3901,21 +3909,12 @@ impl Coordinator {
_ => None,
})
.collect();
let mut visited = BTreeSet::new();
while let Some(dependency_id) = pending.pop() {
if !visited.insert(dependency_id) {
continue;
}
let entry = self.catalog().get_entry(&dependency_id);
match entry.item() {
CatalogItem::Connection(conn) => {
let conn = if dependency_id == id {
connection
} else {
conn
};
pending.extend(conn.resolved_ids.items().copied());
}
match self.catalog().get_entry(&dependency_id).item() {
CatalogItem::Connection(conn) => pending.extend(conn.resolved_ids.items().copied()),
CatalogItem::Secret(_) => usage_ids.add_item(dependency_id),
_ => (),
}
Expand Down
81 changes: 22 additions & 59 deletions test/sqllogictest/privilege_checks.slt
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,24 @@ ALTER CONNECTION ssh_route SET (HOST 'redirected.invalid') WITH (VALIDATE = fals
db error: ERROR: permission denied for SECRET "materialize.public.ssh_route_password"
DETAIL: The 'joe' role needs USAGE privileges on SECRET "materialize.public.ssh_route_password"

# Altering a connection does not require USAGE on secrets reached only through
# the connections it references. `USAGE` on a fixed connection delegates those.

simple conn=mz_system,user=mz_system
ALTER CONNECTION ssh_route_kafka OWNER TO joe;
----
COMPLETE 0

simple conn=mz_system,user=mz_system
GRANT USAGE ON CONNECTION ssh_route_aws TO joe;
----
COMPLETE 0

simple conn=joe,user=joe
ALTER CONNECTION ssh_route_kafka SET (BROKER 'broker2:9092' USING SSH TUNNEL ssh_route) WITH (VALIDATE = false);
----
COMPLETE 0

simple conn=mz_system,user=mz_system
DROP CONNECTION ssh_route_kafka;
----
Expand All @@ -285,6 +303,10 @@ DROP SECRET ssh_route_password;
----
COMPLETE 0

# The authorization walk does not inspect the route's kind, so AWS PrivateLink
# only needs to show that it reaches the same path. The transitive, inherited,
# and ownership cases are covered once, above.

simple conn=mz_system,user=mz_system
CREATE CONNECTION private_route TO AWS PRIVATELINK (SERVICE NAME 'com.amazonaws.vpce.us-east-1.vpce-svc-0e123abc123198abc', AVAILABILITY ZONES ('use1-az1'));
----
Expand All @@ -305,82 +327,23 @@ CREATE CONNECTION private_route_pg TO POSTGRES (HOST 'postgres', DATABASE 'postg
----
COMPLETE 0

# This dependent reaches the same secret through a separate AWS connection.
simple conn=mz_system,user=mz_system
CREATE CONNECTION private_route_aws TO AWS (REGION 'us-east-1', ACCESS KEY ID 'AKIAEXAMPLE', SECRET ACCESS KEY SECRET private_route_password) WITH (VALIDATE = false);
----
COMPLETE 0

simple conn=mz_system,user=mz_system
CREATE CONNECTION private_route_kafka TO KAFKA (BROKER 'broker:9092' USING AWS PRIVATELINK private_route, SECURITY PROTOCOL SASL_PLAINTEXT, AWS CONNECTION private_route_aws) WITH (VALIDATE = false);
----
COMPLETE 0

simple conn=joe,user=joe
ALTER CONNECTION private_route SET (SERVICE NAME 'com.amazonaws.vpce.us-east-1.vpce-svc-0e123abc123198abd') WITH (VALIDATE = false);
----
db error: ERROR: permission denied for SECRET "materialize.public.private_route_password"
DETAIL: The 'joe' role needs USAGE privileges on SECRET "materialize.public.private_route_password"

simple conn=joe,user=joe
ALTER CONNECTION private_route SET (SERVICE NAME 'com.amazonaws.vpce.us-east-1.vpce-svc-0e123abc123198abd') WITH (VALIDATE = true);
----
db error: ERROR: permission denied for SECRET "materialize.public.private_route_password"
DETAIL: The 'joe' role needs USAGE privileges on SECRET "materialize.public.private_route_password"

simple conn=mz_system,user=mz_system
SELECT create_sql LIKE '%vpce-svc-0e123abc123198abc%' FROM mz_connections WHERE name = 'private_route';
----
t
COMPLETE 1

simple conn=mz_system,user=mz_system
GRANT USAGE ON SECRET private_route_password TO other;
----
COMPLETE 0

simple conn=child,user=child
ALTER CONNECTION private_route SET (SERVICE NAME 'com.amazonaws.vpce.us-east-1.vpce-svc-0e123abc123198abd') WITH (VALIDATE = false);
----
COMPLETE 0

simple conn=mz_system,user=mz_system
REVOKE USAGE ON SECRET private_route_password FROM other;
----
COMPLETE 0

simple conn=mz_system,user=mz_system
ALTER CONNECTION private_route_pg OWNER TO joe;
----
COMPLETE 0

simple conn=joe,user=joe
ALTER CONNECTION private_route SET (SERVICE NAME 'com.amazonaws.vpce.us-east-1.vpce-svc-0e123abc123198abd') WITH (VALIDATE = false);
----
db error: ERROR: permission denied for SECRET "materialize.public.private_route_password"
DETAIL: The 'joe' role needs USAGE privileges on SECRET "materialize.public.private_route_password"

simple conn=joe,user=joe
DROP CONNECTION private_route_pg;
----
COMPLETE 0

simple conn=joe,user=joe
ALTER CONNECTION private_route SET (SERVICE NAME 'com.amazonaws.vpce.us-east-1.vpce-svc-0e123abc123198abd') WITH (VALIDATE = false);
----
db error: ERROR: permission denied for SECRET "materialize.public.private_route_password"
DETAIL: The 'joe' role needs USAGE privileges on SECRET "materialize.public.private_route_password"

simple conn=mz_system,user=mz_system
DROP CONNECTION private_route_kafka;
----
COMPLETE 0

simple conn=mz_system,user=mz_system
DROP CONNECTION private_route_aws;
----
COMPLETE 0

simple conn=joe,user=joe
ALTER CONNECTION private_route SET (SERVICE NAME 'com.amazonaws.vpce.us-east-1.vpce-svc-0e123abc123198abd') WITH (VALIDATE = false);
----
Expand Down
Loading