Skip to content

Commit 7daa950

Browse files
committed
fix(trino): allow scoped table and view definition reads
1 parent ca5d7b2 commit 7daa950

4 files changed

Lines changed: 55 additions & 8 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ for deployment requirements, local verification, and recovery.
9696
See the [Trino admin API documentation](controlplane/admin/README.md#trino-cell-views-trinogo--trino_clientgo)
9797
for local verification, compatibility details, and recovery instructions.
9898

99+
Trino `SHOW CREATE TABLE` and `SHOW CREATE VIEW` use the same catalog, schema,
100+
and relation read grants as table reads. Project-scoped logins can inspect only
101+
relations their scope permits; this does not grant access to the `system` catalog.
102+
Run `just test-trino-opa` to check the authorization policy locally.
103+
99104
## Metrics
100105

101106
Duckgres exposes Prometheus metrics on `:9090/metrics`. The metrics port is currently fixed at 9090 and cannot be changed via configuration.

‎controlplane/provisioner/opa/policy.rego‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,16 @@ allow if {
459459
)
460460
}
461461

462+
# Trino uses this operation for both SHOW CREATE TABLE and SHOW CREATE VIEW.
463+
allow if {
464+
input.action.operation == "ShowCreateTable"
465+
readable_table(
466+
input.action.resource.table.catalogName,
467+
input.action.resource.table.schemaName,
468+
input.action.resource.table.tableName,
469+
)
470+
}
471+
462472
allow if {
463473
input.action.operation == "FilterTables"
464474
readable_table(

‎controlplane/provisioner/opa/policy_test.go‎

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ func TestSchemaScopeOps(t *testing.T) {
293293
func TestTableScopeOps(t *testing.T) {
294294
q := preparedPolicy(t, twoOrgFixture())
295295

296-
for _, op := range []string{"SelectFromColumns", "FilterTables", "ShowColumns", "FilterColumns"} {
296+
for _, op := range []string{"SelectFromColumns", "ShowCreateTable", "FilterTables", "ShowColumns", "FilterColumns"} {
297297
if !evalAllow(t, q, buildInput("42", op, tableResource("org_42", "public", "events"))) {
298298
t.Errorf("%s on own catalog must allow", op)
299299
}
@@ -306,6 +306,30 @@ func TestTableScopeOps(t *testing.T) {
306306
}
307307
}
308308

309+
func TestShowCreateTableReadAuthority(t *testing.T) {
310+
q := preparedPolicy(t, twoOrgFixture())
311+
for _, tc := range []struct {
312+
name string
313+
user string
314+
catalog string
315+
allowed bool
316+
}{
317+
{"admin bundle read grant", AdminPrincipal, "org_42", true},
318+
{"admin orphan catalog", AdminPrincipal, "org_99", false},
319+
{"observer", ObserverPrincipal, "org_42", false},
320+
{"tenant system catalog", "42", "system", false},
321+
{"admin system catalog", AdminPrincipal, "system", false},
322+
{"observer system catalog", ObserverPrincipal, "system", false},
323+
} {
324+
t.Run(tc.name, func(t *testing.T) {
325+
got := evalAllow(t, q, buildInput(tc.user, "ShowCreateTable", tableResource(tc.catalog, "metadata", "table_comments")))
326+
if got != tc.allowed {
327+
t.Errorf("ShowCreateTable = %v, want %v", got, tc.allowed)
328+
}
329+
})
330+
}
331+
}
332+
309333
// --------------------------------------------------------------------------
310334
// Tenant writes: a customer has full DuckLake DDL/DML authority inside the
311335
// catalog projected for its org, and no write authority anywhere else.
@@ -853,6 +877,7 @@ func TestIsolationMatrix(t *testing.T) {
853877
{"FilterSchemas", func(c string) map[string]interface{} { return schemaResource(c, "s") }},
854878
{"ShowTables", func(c string) map[string]interface{} { return schemaResource(c, "s") }},
855879
{"SelectFromColumns", func(c string) map[string]interface{} { return tableResource(c, "s", "t") }},
880+
{"ShowCreateTable", func(c string) map[string]interface{} { return tableResource(c, "s", "t") }},
856881
{"FilterTables", func(c string) map[string]interface{} { return tableResource(c, "s", "t") }},
857882
{"ShowColumns", func(c string) map[string]interface{} { return tableResource(c, "s", "t") }},
858883
{"FilterColumns", func(c string) map[string]interface{} { return tableResource(c, "s", "t") }},
@@ -1959,7 +1984,7 @@ func TestScopedGroupReadsOnlyItsOwnSchemas(t *testing.T) {
19591984
{"a shared schema it holds no grant in", "public", "anything", false},
19601985
} {
19611986
t.Run(tc.name, func(t *testing.T) {
1962-
for _, op := range []string{"SelectFromColumns", "FilterTables", "ShowColumns"} {
1987+
for _, op := range []string{"SelectFromColumns", "ShowCreateTable", "FilterTables", "ShowColumns"} {
19631988
got := evalAllow(t, q, tableInput(op, "org_acme", tc.schema, tc.table, scopedIdentity()))
19641989
if got != tc.want {
19651990
t.Errorf("%s on %s.%s = %v, want %v", op, tc.schema, tc.table, got, tc.want)
@@ -1978,11 +2003,13 @@ func TestScopedGroupRelationGrantDoesNotLeakItsSchema(t *testing.T) {
19782003
gc, gs := scopedFixture()
19792004
q := preparedScopedPolicy(t, gc, gs)
19802005

1981-
if !evalAllow(t, q, tableInput("SelectFromColumns", "org_acme", "posthog", "events", scopedIdentity())) {
1982-
t.Error("granted relation posthog.events must be readable")
1983-
}
1984-
if evalAllow(t, q, tableInput("SelectFromColumns", "org_acme", "posthog", "persons", scopedIdentity())) {
1985-
t.Error("posthog.persons was NOT granted and must not be readable")
2006+
for _, op := range []string{"SelectFromColumns", "ShowCreateTable"} {
2007+
if !evalAllow(t, q, tableInput(op, "org_acme", "posthog", "events", scopedIdentity())) {
2008+
t.Errorf("%s must allow the granted relation posthog.events", op)
2009+
}
2010+
if evalAllow(t, q, tableInput(op, "org_acme", "posthog", "persons", scopedIdentity())) {
2011+
t.Errorf("%s must deny the ungranted relation posthog.persons", op)
2012+
}
19862013
}
19872014
}
19882015

@@ -2013,7 +2040,7 @@ func TestScopedGroupStillCannotCrossTenants(t *testing.T) {
20132040
gc, gs := scopedFixture()
20142041
q := preparedScopedPolicy(t, gc, gs)
20152042

2016-
for _, op := range []string{"SelectFromColumns", "FilterTables", "ShowColumns"} {
2043+
for _, op := range []string{"SelectFromColumns", "ShowCreateTable", "FilterTables", "ShowColumns"} {
20172044
if evalAllow(t, q, tableInput(op, "org_other", "posthog_7", "events", scopedIdentity())) {
20182045
t.Errorf("%s reached another tenant's catalog", op)
20192046
}

‎justfile‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,11 @@ test-controlplane:
333333
test-configstore-integration:
334334
go test -v -count=1 ./tests/configstore/...
335335

336+
# Test Trino authorization policy, including tenant and project isolation
337+
[group('test')]
338+
test-trino-opa pattern="":
339+
go test -v -count=1 -run '{{pattern}}' ./controlplane/provisioner/opa
340+
336341
# Run Kubernetes-only control plane package tests
337342
[group('test')]
338343
test-trino pattern="Trino":

0 commit comments

Comments
 (0)