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
88 changes: 86 additions & 2 deletions internal/migration_acceptance_tests/named_schema_cases_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package migration_acceptance_tests

import "testing"
import (
"testing"

"github.com/stripe/pg-schema-diff/pkg/diff"
)

var namedSchemaAcceptanceTestCases = []acceptanceTestCase{
{
Expand Down Expand Up @@ -29,12 +33,92 @@ var namedSchemaAcceptanceTestCases = []acceptanceTestCase{
name: "Drop schema",
oldSchemaDDL: []string{`
CREATE SCHEMA "schema 1";
CREATE SCHEMA "schema 2";
CREATE SCHEMA "schema 2";
`},
newSchemaDDL: []string{`
CREATE SCHEMA "schema 1";
`},
},
{
name: "Grant usage on existing schema",
roles: []string{"app_user"},
oldSchemaDDL: []string{`
CREATE SCHEMA app_schema;
`},
newSchemaDDL: []string{`
CREATE SCHEMA app_schema;
GRANT USAGE ON SCHEMA app_schema TO app_user;
`},
expectedHazardTypes: []diff.MigrationHazardType{
diff.MigrationHazardTypeAuthzUpdate,
},
expectedPlanDDL: []string{`GRANT USAGE ON SCHEMA "app_schema" TO "app_user"`},
},
{
name: "Revoke usage on existing schema",
roles: []string{"app_user"},
oldSchemaDDL: []string{`
CREATE SCHEMA app_schema;
GRANT USAGE ON SCHEMA app_schema TO app_user;
`},
newSchemaDDL: []string{`
CREATE SCHEMA app_schema;
`},
expectedHazardTypes: []diff.MigrationHazardType{
diff.MigrationHazardTypeAuthzUpdate,
},
expectedPlanDDL: []string{`REVOKE USAGE ON SCHEMA "app_schema" FROM "app_user"`},
},
{
name: "Change schema grant option",
roles: []string{"app_user"},
oldSchemaDDL: []string{`
CREATE SCHEMA app_schema;
GRANT USAGE ON SCHEMA app_schema TO app_user;
`},
newSchemaDDL: []string{`
CREATE SCHEMA app_schema;
GRANT USAGE ON SCHEMA app_schema TO app_user WITH GRANT OPTION;
`},
expectedHazardTypes: []diff.MigrationHazardType{
diff.MigrationHazardTypeAuthzUpdate,
},
},
{
name: "Revoke create when target only grants usage",
roles: []string{"app_user"},
oldSchemaDDL: []string{`
CREATE SCHEMA app_schema;
GRANT USAGE, CREATE ON SCHEMA app_schema TO app_user;
`},
newSchemaDDL: []string{`
CREATE SCHEMA app_schema;
GRANT USAGE ON SCHEMA app_schema TO app_user;
`},
expectedHazardTypes: []diff.MigrationHazardType{
diff.MigrationHazardTypeAuthzUpdate,
},
expectedPlanDDL: []string{
`REVOKE CREATE ON SCHEMA "app_schema" FROM "app_user"`,
},
},
{
name: "Do not grant usage to existing schema owner",
roles: []string{"service"},
oldSchemaDDL: []string{`
CREATE SCHEMA casino_wager_stats AUTHORIZATION service;
GRANT USAGE, CREATE ON SCHEMA casino_wager_stats TO service;
`},
newSchemaDDL: []string{`
CREATE SCHEMA casino_wager_stats;
GRANT USAGE ON SCHEMA casino_wager_stats TO service;
`},
expectedDBSchemaDDL: []string{`
CREATE SCHEMA casino_wager_stats AUTHORIZATION service;
GRANT USAGE, CREATE ON SCHEMA casino_wager_stats TO service;
`},
expectEmptyPlan: true,
},
}

func TestNamedSchemaTestCases(t *testing.T) {
Expand Down
42 changes: 41 additions & 1 deletion internal/queries/queries.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
-- name: GetSchemas :many
SELECT nspname::TEXT AS schema_name
SELECT
pg_namespace.nspname::TEXT AS schema_name,
owner_role.rolname::TEXT AS owner
FROM pg_catalog.pg_namespace
INNER JOIN pg_catalog.pg_roles AS owner_role
ON pg_namespace.nspowner = owner_role.oid
WHERE
nspname NOT IN ('pg_catalog', 'information_schema')
AND nspname !~ '^pg_toast'
Expand All @@ -15,6 +19,42 @@ WHERE
AND depend.deptype = 'e'
);

-- name: GetSchemaPrivileges :many
WITH parsed_acl AS (
SELECT
n.nspname AS schema_name,
n.nspowner AS owner_oid,
(ACLEXPLODE(n.nspacl)).grantee AS grantee_oid,
(ACLEXPLODE(n.nspacl)).privilege_type AS privilege_type,
(ACLEXPLODE(n.nspacl)).is_grantable AS is_grantable
FROM pg_catalog.pg_namespace AS n
WHERE
n.nspname NOT IN ('pg_catalog', 'information_schema')
AND n.nspname !~ '^pg_toast'
AND n.nspname !~ '^pg_temp'
-- Exclude schemas owned by extensions
AND NOT EXISTS (
SELECT depend.objid
FROM pg_catalog.pg_depend AS depend
WHERE
depend.classid = 'pg_namespace'::REGCLASS
AND depend.objid = n.oid
AND depend.deptype = 'e'
)
)

SELECT
pa.schema_name::TEXT AS schema_name,
COALESCE(grantee_role.rolname, '')::TEXT AS grantee,
pa.privilege_type::TEXT AS privilege,
pa.is_grantable
FROM parsed_acl AS pa
LEFT JOIN pg_catalog.pg_roles AS grantee_role
ON pa.grantee_oid = grantee_role.oid
-- Exclude privileges granted to the schema owner (these are implicit)
WHERE pa.grantee_oid != pa.owner_oid OR pa.grantee_oid = 0
ORDER BY pa.schema_name, grantee, pa.privilege_type;

-- name: GetTables :many
SELECT
c.oid,
Expand Down
93 changes: 87 additions & 6 deletions internal/queries/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading