diff --git a/src/graphql.rs b/src/graphql.rs index fb7b4339..b2415eca 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -4754,7 +4754,8 @@ impl ___Type for AggregateNumericType { let mut fields = Vec::new(); for col in self.table.columns.iter() { - if is_aggregatable(col, &self.aggregate_op) + if col.permissions.is_selectable + && is_aggregatable(col, &self.aggregate_op) && let Some(scalar_type) = aggregate_result_type(col, &self.aggregate_op) { let field_name = self.schema.graphql_column_field_name(col); diff --git a/test/expected/permissions_aggregate_column.out b/test/expected/permissions_aggregate_column.out new file mode 100644 index 00000000..7c63451f --- /dev/null +++ b/test/expected/permissions_aggregate_column.out @@ -0,0 +1,144 @@ +begin; + comment on schema public is e'@graphql({"inflect_names": true, "introspection": true})'; + create table account( + id serial primary key, + balance numeric not null, + score int not null + ); + -- Aggregates are opt-in per table. + comment on table account is e'@graphql({"aggregate": {"enabled": true}})'; + insert into public.account(balance, score) + values + (100.00, 5), + (250.00, 9); + -- Superuser sees every aggregatable column, including `balance`. + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "AccountSumAggregateResult") { + kind + fields { name } + } + } + $$) + ); + jsonb_pretty +--------------------------------------- + { + + "data": { + + "__type": { + + "kind": "OBJECT", + + "fields": [ + + { + + "name": "id" + + }, + + { + + "name": "balance"+ + }, + + { + + "name": "score" + + } + + ] + + } + + } + + } +(1 row) + + create role api; + -- Grant access to GQL + grant usage on schema graphql to api; + -- `api` may read id + score, but NOT balance. + grant usage on schema public to api; + grant all on all tables in schema public to api; + revoke select on public.account from api; + grant select (id, score) on public.account to api; + set role api; + -- The aggregate result types must not expose the non-selectable `balance` + -- column (schema/introspection leak). Only `score` should appear. + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "AccountSumAggregateResult") { + kind + fields { name } + } + } + $$) + ); + jsonb_pretty +------------------------------------- + { + + "data": { + + "__type": { + + "kind": "OBJECT", + + "fields": [ + + { + + "name": "id" + + }, + + { + + "name": "score"+ + } + + ] + + } + + } + + } +(1 row) + + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "AccountMaxAggregateResult") { + kind + fields { name } + } + } + $$) + ); + jsonb_pretty +------------------------------------- + { + + "data": { + + "__type": { + + "kind": "OBJECT", + + "fields": [ + + { + + "name": "id" + + }, + + { + + "name": "score"+ + } + + ] + + } + + } + + } +(1 row) + + -- Aggregating a permitted column still works. + select jsonb_pretty( + graphql.resolve($$ + { + accountCollection { + aggregate { + sum { score } + } + } + } + $$) + ); + jsonb_pretty +--------------------------------- + { + + "data": { + + "accountCollection": { + + "aggregate": { + + "sum": { + + "score": 14+ + } + + } + + } + + } + + } +(1 row) + + reset role; +rollback; diff --git a/test/sql/permissions_aggregate_column.sql b/test/sql/permissions_aggregate_column.sql new file mode 100644 index 00000000..96b3d99f --- /dev/null +++ b/test/sql/permissions_aggregate_column.sql @@ -0,0 +1,81 @@ +begin; + comment on schema public is e'@graphql({"inflect_names": true, "introspection": true})'; + + create table account( + id serial primary key, + balance numeric not null, + score int not null + ); + + -- Aggregates are opt-in per table. + comment on table account is e'@graphql({"aggregate": {"enabled": true}})'; + + insert into public.account(balance, score) + values + (100.00, 5), + (250.00, 9); + + -- Superuser sees every aggregatable column, including `balance`. + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "AccountSumAggregateResult") { + kind + fields { name } + } + } + $$) + ); + + create role api; + + -- Grant access to GQL + grant usage on schema graphql to api; + + -- `api` may read id + score, but NOT balance. + grant usage on schema public to api; + grant all on all tables in schema public to api; + revoke select on public.account from api; + grant select (id, score) on public.account to api; + + set role api; + + -- The aggregate result types must not expose the non-selectable `balance` + -- column (schema/introspection leak). Only `score` should appear. + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "AccountSumAggregateResult") { + kind + fields { name } + } + } + $$) + ); + + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "AccountMaxAggregateResult") { + kind + fields { name } + } + } + $$) + ); + + -- Aggregating a permitted column still works. + select jsonb_pretty( + graphql.resolve($$ + { + accountCollection { + aggregate { + sum { score } + } + } + } + $$) + ); + + reset role; +rollback;