From 855808572bed46cff53397e352025d3f58eafaa7 Mon Sep 17 00:00:00 2001 From: Daylon Wilkins Date: Tue, 29 Sep 2026 03:37:06 -0700 Subject: [PATCH 1/2] Fixed relation privilege checks --- go.mod | 4 +- go.sum | 8 +- server/auth/auth_handler.go | 79 +++++++++-- server/auth/auth_information.go | 1 + testing/go/auth_test.go | 242 ++++++++++++++++++++++++++++++++ 5 files changed, 319 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 8885f0b2af..cc8fd4243d 100644 --- a/go.mod +++ b/go.mod @@ -11,10 +11,10 @@ require ( github.com/dolthub/dolt/go v0.40.5-0.20260929214903-5a7f241b2a54 github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 - github.com/dolthub/go-mysql-server v0.20.1-0.20260929213643-0c0ebff14743 + github.com/dolthub/go-mysql-server v0.20.1-0.20260930085504-71ba14497def github.com/dolthub/pg_query_go/v6 v6.0.0-20260922094746-ae7577e21dfe github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 - github.com/dolthub/vitess v0.0.0-20260916192104-15c5c4158b37 + github.com/dolthub/vitess v0.0.0-20260930074555-77cf155d2465 github.com/fatih/color v1.13.0 github.com/go-sql-driver/mysql v1.9.3 github.com/goccy/go-json v0.10.2 diff --git a/go.sum b/go.sum index 31464e72ae..94f73399cd 100644 --- a/go.sum +++ b/go.sum @@ -260,8 +260,8 @@ github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1G github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83 h1:FEMjCGEroDnY/BXyAffVZxUpXhP2GpoUJyyq5KaLn8c= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE= -github.com/dolthub/go-mysql-server v0.20.1-0.20260929213643-0c0ebff14743 h1:4iyHwLCKpommapKwAEhr59r/JeoC6TBIGxNI8c9rr78= -github.com/dolthub/go-mysql-server v0.20.1-0.20260929213643-0c0ebff14743/go.mod h1:Qnp0PJNtR8JPjkmRDMShYh7Fy9jiSYLFgfQCe6Gmnxg= +github.com/dolthub/go-mysql-server v0.20.1-0.20260930085504-71ba14497def h1:RaEFafMdBdCe4VmnBW2tdD1P6J9QfxQBPVh8CQxAzBU= +github.com/dolthub/go-mysql-server v0.20.1-0.20260930085504-71ba14497def/go.mod h1:HJmzcei8r2MpvfOZ+slXBJUGNNf3D8xKpkWH7yKCrX8= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= github.com/dolthub/ishell v0.0.0-20260414231531-5f031e3e9037 h1:oIW9HwuWrhxv+4HZxA+QQSKHLqWFyXZ2FmNjUYwkdiM= @@ -272,8 +272,8 @@ github.com/dolthub/pg_query_go/v6 v6.0.0-20260922094746-ae7577e21dfe h1:5QQHZM+Z github.com/dolthub/pg_query_go/v6 v6.0.0-20260922094746-ae7577e21dfe/go.mod h1:qnrZP3/1slFl2Bq5yw38HLOsArZareGwdpEceriblLc= github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 h1:fdexJBpLDyAGnFbviZYfZ0jhLMnbTWTh39CyQ1EhA28= github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166/go.mod h1:e/FIZVvT2IR53HBCAo41NjqgtEnjMJGKca3Y/dAmZaA= -github.com/dolthub/vitess v0.0.0-20260916192104-15c5c4158b37 h1:F8kNW/lTJQ70FMP/s9MtQpGjURvbwE/B318Q6uEFG30= -github.com/dolthub/vitess v0.0.0-20260916192104-15c5c4158b37/go.mod h1:5SVEJgAhw5nnQUFnGgKI1Svqes1Mw+zKUsalyxmOuG0= +github.com/dolthub/vitess v0.0.0-20260930074555-77cf155d2465 h1:inhFizBUDk1No+zUSijSgqIOuDCTfKIwzQZn1+7eIDE= +github.com/dolthub/vitess v0.0.0-20260930074555-77cf155d2465/go.mod h1:5SVEJgAhw5nnQUFnGgKI1Svqes1Mw+zKUsalyxmOuG0= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= diff --git a/server/auth/auth_handler.go b/server/auth/auth_handler.go index 32f104c89b..673eeef192 100644 --- a/server/auth/auth_handler.go +++ b/server/auth/auth_handler.go @@ -20,6 +20,7 @@ import ( "github.com/cockroachdb/errors" "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/plan" vitess "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/dolthub/doltgresql/core" @@ -63,6 +64,7 @@ type AuthorizationHandler struct { } var _ sql.AuthorizationHandler = (*AuthorizationHandler)(nil) +var _ sql.ResolvedTableAuthorizationHandler = (*AuthorizationHandler)(nil) // NewQueryState implements the sql.AuthorizationHandler interface. func (h *AuthorizationHandler) NewQueryState(ctx *sql.Context) sql.AuthorizationQueryState { @@ -167,7 +169,7 @@ func (h *AuthorizationHandler) HandleAuth(ctx *sql.Context, aqs sql.Authorizatio } for _, privilege := range privileges { if !HasDatabasePrivilege(roleDatabaseKey, privilege) && !HasDatabasePrivilege(publicDatabaseKey, privilege) { - return errors.Errorf("permission denied for database %s", database) + return pgerror.Newf(pgcode.InsufficientPrivilege, "permission denied for database %s", database) } } } @@ -188,10 +190,14 @@ func (h *AuthorizationHandler) HandleAuth(ctx *sql.Context, aqs sql.Authorizatio return err } } - case AuthTargetType_TableIdentifiers: + case AuthTargetType_TableIdentifiers, AuthTargetType_ViewIdentifiers: if len(auth.TargetNames)%3 != 0 { return errors.Errorf("table identifiers has an unsupported count: %d", len(auth.TargetNames)) } + relationKind := "table" + if auth.TargetType == AuthTargetType_ViewIdentifiers { + relationKind = "view" + } for i := 0; i < len(auth.TargetNames); i += 3 { // TODO: handle database schemaName, err := core.GetSchemaName(ctx, nil, auth.TargetNames[i+1]) @@ -200,7 +206,7 @@ func (h *AuthorizationHandler) HandleAuth(ctx *sql.Context, aqs sql.Authorizatio // This will error later in the process, so we'll pass auth for now. return nil } - err = checkPrivilegeOnTable(state, schemaName, auth.TargetNames[i+2], privileges) + err = checkPrivilegeOnTable(state, relationKind, schemaName, auth.TargetNames[i+2], privileges) if err != nil { return err } @@ -304,6 +310,26 @@ func (h *AuthorizationHandler) CheckTable(ctx *sql.Context, aqs sql.Authorizatio return nil } +// HandleResolvedTableAuth implements the sql.ResolvedTableAuthorizationHandler interface. +func (h *AuthorizationHandler) HandleResolvedTableAuth(ctx *sql.Context, aqs sql.AuthorizationQueryState, auth vitess.AuthInformation, node sql.Node) error { + if auth.TargetType == AuthTargetType_TableIdentifiers { + switch node := node.(type) { + case *plan.ResolvedTable: + schemaName := auth.TargetNames[1] + if schemaTable, ok := node.UnderlyingTable().(sql.DatabaseSchemaTable); ok { + schemaName = schemaTable.DatabaseSchema().SchemaName() + } else if node.Database().Name() == sql.InformationSchemaDatabaseName { + schemaName = sql.InformationSchemaDatabaseName + } + auth.TargetNames = []string{auth.TargetNames[0], schemaName, auth.TargetNames[2]} + case *plan.SubqueryAlias: + // This case will always resolve to a view + auth.TargetType = AuthTargetType_ViewIdentifiers + } + } + return h.HandleAuth(ctx, aqs, auth) +} + // dbName uses the current database from the context if a database is not specified, otherwise it returns the given // database name. func (h *AuthorizationHandler) dbName(ctx *sql.Context, dbName string) string { @@ -327,14 +353,14 @@ func checkPrivilegeOnSchema(state AuthorizationQueryState, schemaName string, pr } for _, privilege := range privileges { if !HasSchemaPrivilege(roleSchemaKey, privilege) && !HasSchemaPrivilege(publicSchemaKey, privilege) { - return errors.Errorf("permission denied for schema %s", schemaName) + return pgerror.Newf(pgcode.InsufficientPrivilege, "permission denied for schema %s", schemaName) } } return nil } -// checkPrivilegeOnTable checks privileges for given table provided with schema name. -func checkPrivilegeOnTable(state AuthorizationQueryState, schemaName, tableName string, privileges []Privilege) error { +// checkPrivilegeOnTable checks privileges for given table or view provided with schema name. +func checkPrivilegeOnTable(state AuthorizationQueryState, relationKind, schemaName, tableName string, privileges []Privilege) error { roleTableKey := TablePrivilegeKey{ Role: state.role.ID(), Table: doltdb.TableName{Name: tableName, Schema: schemaName}, @@ -345,12 +371,47 @@ func checkPrivilegeOnTable(state AuthorizationQueryState, schemaName, tableName } for _, privilege := range privileges { if !HasTablePrivilege(roleTableKey, privilege) && !HasTablePrivilege(publicTableKey, privilege) { - return errors.Errorf("permission denied for table %s", tableName) + if privilege == Privilege_SELECT && isPublicCatalogTable(schemaName, tableName) { + //TODO: catalogs are granted to PUBLIC by default, so deny them once REVOKE ... FROM PUBLIC is supported + continue + } + return pgerror.Newf(pgcode.InsufficientPrivilege, "permission denied for %s %s", relationKind, tableName) } } return nil } +// restrictedCatalogTables are the pg_catalog relations that Postgres does not grant SELECT on to PUBLIC. +var restrictedCatalogTables = map[string]struct{}{ + "pg_authid": {}, + "pg_backend_memory_contexts": {}, + "pg_config": {}, + "pg_file_settings": {}, + "pg_hba_file_rules": {}, + "pg_ident_file_mappings": {}, + "pg_largeobject": {}, + "pg_replication_origin_status": {}, + "pg_shadow": {}, + "pg_shmem_allocations": {}, + "pg_statistic": {}, + "pg_statistic_ext_data": {}, + "pg_subscription": {}, + "pg_user_mapping": {}, +} + +// isPublicCatalogTable returns whether the given relation is a system catalog that every role may read. +func isPublicCatalogTable(schemaName string, tableName string) bool { + switch schemaName { + case "pg_catalog": + _, restricted := restrictedCatalogTables[tableName] + return !restricted + case "information_schema": + return true + default: + return false + } +} + // checkPrivilegeOnSequence checks privileges for given sequence provided with schema name. func checkPrivilegeOnSequence(state AuthorizationQueryState, schemaName, seqName string, privileges []Privilege) error { roleSequenceKey := SequencePrivilegeKey{ @@ -365,7 +426,7 @@ func checkPrivilegeOnSequence(state AuthorizationQueryState, schemaName, seqName } for _, privilege := range privileges { if !HasSequencePrivilege(roleSequenceKey, privilege) && !HasSequencePrivilege(publicSequenceKey, privilege) { - return errors.Errorf("permission denied for sequence %s", seqName) + return pgerror.Newf(pgcode.InsufficientPrivilege, "permission denied for sequence %s", seqName) } } return nil @@ -402,7 +463,7 @@ func checkPrivilegeOnRoutine(ctx *sql.Context, state AuthorizationQueryState, sc //TODO: built-in routines are granted to PUBLIC by default, so deny them once REVOKE ... FROM PUBLIC is supported return nil } - return errors.Errorf("permission denied for routine %s", routineName) + return pgerror.Newf(pgcode.InsufficientPrivilege, "permission denied for routine %s", routineName) } } return nil diff --git a/server/auth/auth_information.go b/server/auth/auth_information.go index 42c2711f6c..20398eb301 100644 --- a/server/auth/auth_information.go +++ b/server/auth/auth_information.go @@ -42,6 +42,7 @@ const ( AuthTargetType_DatabaseIdentifiers = "DB_IDENTS" AuthTargetType_SchemaIdentifiers = "DB_SCH_IDENTS" AuthTargetType_TableIdentifiers = "DB_SCH_TABLE_IDENTS" + AuthTargetType_ViewIdentifiers = "DB_SCH_VIEW_IDENTS" AuthTargetType_FunctionIdentifiers = "DB_SCH_FUNCTION_IDENTS" AuthTargetType_SequenceIdentifiers = "DB_SCH_SEQUENCE_IDENTS" AuthTargetType_TODO = "TODO" diff --git a/testing/go/auth_test.go b/testing/go/auth_test.go index 1b0b6990fa..3949f29b46 100644 --- a/testing/go/auth_test.go +++ b/testing/go/auth_test.go @@ -1357,6 +1357,248 @@ func TestAuthTests(t *testing.T) { }, }, }, + { + Name: "CTE names are not checked as tables", + SetUpScript: []string{ + "CREATE TABLE edges (src TEXT, dst TEXT, project_id TEXT);", + "INSERT INTO edges VALUES ('a', 'b', 'p'), ('b', 'c', 'p');", + "CREATE TABLE secret (x INT);", + "INSERT INTO secret VALUES (1);", + "CREATE TABLE target (src TEXT, dst TEXT);", + "CREATE ROLE reader LOGIN PASSWORD 'password';", + "GRANT SELECT ON edges TO reader;", + "GRANT SELECT, INSERT, UPDATE, DELETE ON target TO reader;", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "WITH traversal AS (SELECT src, dst FROM edges) SELECT * FROM traversal ORDER BY src;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"a", "b"}, {"b", "c"}}, + }, + { + Query: "WITH RECURSIVE r(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM r WHERE n < 3) SELECT * FROM r;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{1}, {2}, {3}}, + }, + { + Query: "WITH RECURSIVE walk(node) AS (SELECT 'a'::TEXT UNION SELECT e.dst FROM edges e JOIN walk w ON e.src = w.node) SELECT * FROM walk ORDER BY node;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"a"}, {"b"}, {"c"}}, + }, + { + Query: "WITH s AS (SELECT * FROM secret) SELECT * FROM s;", + Username: "reader", + Password: "password", + ExpectedErr: "permission denied for table secret", + ExpectedErrCode: "42501", + }, + { + Query: "WITH secret AS (SELECT 5 AS x) SELECT * FROM secret;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{5}}, + }, + { + Query: "WITH t AS (SELECT src FROM edges) SELECT * FROM edges WHERE src IN (SELECT src FROM t) ORDER BY src;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"a", "b", "p"}, {"b", "c", "p"}}, + }, + { + Query: "WITH t AS (SELECT src, dst FROM edges) SELECT a.src FROM t a JOIN t b ON a.dst = b.src;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"a"}}, + }, + { + Query: "WITH a AS (SELECT src FROM edges), b AS (SELECT * FROM a) SELECT COUNT(*) FROM b;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{2}}, + }, + { + Query: "WITH t AS (SELECT src, dst FROM edges) INSERT INTO target SELECT * FROM t;", + Username: "reader", + Password: "password", + Expected: []sql.Row{}, + }, + { + Query: "INSERT INTO target WITH t AS (SELECT src, dst FROM edges) SELECT * FROM t;", + Username: "reader", + Password: "password", + Expected: []sql.Row{}, + }, + { + Query: "WITH t AS (SELECT src FROM edges) UPDATE target SET dst = 'z' WHERE src IN (SELECT src FROM t);", + Username: "reader", + Password: "password", + Expected: []sql.Row{}, + }, + { + Query: "SELECT * FROM target ORDER BY src;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"a", "z"}, {"a", "z"}, {"b", "z"}, {"b", "z"}}, + }, + { + Query: "WITH t AS (SELECT src FROM edges) DELETE FROM target WHERE src IN (SELECT src FROM t);", + Username: "reader", + Password: "password", + Expected: []sql.Row{}, + }, + { + Query: "SELECT COUNT(*) FROM target;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{0}}, + }, + }, + }, + { + Name: "Reading as a role with only table privileges", + SetUpScript: []string{ + "CREATE TABLE edges (src TEXT, dst TEXT);", + "INSERT INTO edges VALUES ('a', 'b');", + "CREATE TABLE secret (x INT);", + "INSERT INTO secret VALUES (1);", + "CREATE VIEW v_edges AS SELECT src, dst FROM edges;", + "CREATE VIEW v_secret AS SELECT x FROM secret;", + "CREATE ROLE reader LOGIN PASSWORD 'password';", + "GRANT SELECT ON edges TO reader;", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT COUNT(*) > 0 FROM pg_catalog.pg_class;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"t"}}, + }, + { + Query: "SELECT COUNT(*) > 0 FROM information_schema.tables;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"t"}}, + }, + { + Query: "SELECT COUNT(*) > 0 FROM pg_tables;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"t"}}, + }, + { + Query: "SELECT * FROM v_edges;", + Username: "reader", + Password: "password", + ExpectedErr: "permission denied for view v_edges", + ExpectedErrCode: "42501", + }, + { + Query: "SELECT * FROM missing_table;", + Username: "reader", + Password: "password", + ExpectedErr: "table not found: missing_table", + ExpectedErrCode: "42P01", + }, + { + Query: "SELECT rolname FROM pg_authid;", + Username: "reader", + Password: "password", + ExpectedErr: "permission denied for table pg_authid", + ExpectedErrCode: "42501", + }, + { + Query: "SELECT COUNT(*) > 0 FROM pg_roles;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"t"}}, + }, + { + Query: "UPDATE secret SET x = 2;", + Username: "reader", + Password: "password", + ExpectedErr: "permission denied for table secret", + ExpectedErrCode: "42501", + }, + { + Query: "DELETE FROM secret;", + Username: "reader", + Password: "password", + ExpectedErr: "permission denied for table secret", + ExpectedErrCode: "42501", + }, + { + Query: "SELECT * FROM public.secret;", + Username: "reader", + Password: "password", + ExpectedErr: "permission denied for table secret", + ExpectedErrCode: "42501", + }, + { + Query: "GRANT SELECT ON v_edges, v_secret TO reader;", + Expected: []sql.Row{}, + }, + { + Query: "SELECT * FROM v_edges;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{"a", "b"}}, + }, + { + Query: "SELECT * FROM v_secret;", + Username: "reader", + Password: "password", + Expected: []sql.Row{{1}}, + Skip: true, + }, + { + Query: "INSERT INTO missing_table VALUES (1);", + Username: "reader", + Password: "password", + ExpectedErr: "table not found: missing_table", + ExpectedErrCode: "42P01", + }, + { + Query: "INSERT INTO v_edges VALUES ('x', 'y');", + Username: "reader", + Password: "password", + ExpectedErr: "permission denied for view v_edges", + ExpectedErrCode: "42501", + }, + { + Query: "TRUNCATE missing_table;", + Username: "reader", + Password: "password", + ExpectedErr: "table not found: missing_table", + ExpectedErrCode: "42P01", + }, + { + Query: "TRUNCATE secret;", + Username: "reader", + Password: "password", + ExpectedErr: "permission denied for table secret", + ExpectedErrCode: "42501", + }, + { + Query: "DROP TABLE missing_table;", + Username: "reader", + Password: "password", + ExpectedErr: "table \"missing_table\" does not exist", + ExpectedErrCode: "42P01", + Skip: true, + }, + { + Query: "DROP TABLE secret;", + Username: "reader", + Password: "password", + ExpectedErr: "must be owner of table secret", + ExpectedErrCode: "42501", + Skip: true, + }, + }, + }, }) } From b2c4d44014d1e14fcc082ee842c1aa9279d5a1f6 Mon Sep 17 00:00:00 2001 From: Daylon Wilkins Date: Tue, 29 Sep 2026 04:32:56 -0700 Subject: [PATCH 2/2] PR feedback --- go.mod | 2 +- go.sum | 4 +- testing/go/auth_test.go | 130 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cc8fd4243d..e1eeb032d9 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/antchfx/xpath v1.3.3 github.com/cockroachdb/apd/v3 v3.2.3 github.com/cockroachdb/errors v1.7.5 - github.com/dolthub/dolt/go v0.40.5-0.20260929214903-5a7f241b2a54 + github.com/dolthub/dolt/go v0.40.5-0.20260930093343-863fefb78c63 github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 github.com/dolthub/go-mysql-server v0.20.1-0.20260930085504-71ba14497def diff --git a/go.sum b/go.sum index 94f73399cd..35d3ed4924 100644 --- a/go.sum +++ b/go.sum @@ -250,8 +250,8 @@ github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12 h1:I github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12/go.mod h1:rN7X8BHwkjPcfMQQ2QTAq/xM3leUSGLfb+1Js7Y6TVo= github.com/dolthub/dolt-mcp v0.3.4 h1:AyG5cw+fNWXDHXujtQnqUPZrpWtPg6FN6yYtjv1pP44= github.com/dolthub/dolt-mcp v0.3.4/go.mod h1:bCZ7KHvDYs+M0e+ySgmGiNvLhcwsN7bbf5YCyillLrk= -github.com/dolthub/dolt/go v0.40.5-0.20260929214903-5a7f241b2a54 h1:7CSe8xzJkUFzOfCEaDBwIyhZSHc3x5RHFFEPnduXT2I= -github.com/dolthub/dolt/go v0.40.5-0.20260929214903-5a7f241b2a54/go.mod h1:wfLKZ2n/LHLqZHM8NFkHAZxo77A+SBNC4Hb2sbz9Neo= +github.com/dolthub/dolt/go v0.40.5-0.20260930093343-863fefb78c63 h1:WzC64UZKfXI0NkdTK9K8Uv/SA7CxcUj8Ry1WiIq+CgE= +github.com/dolthub/dolt/go v0.40.5-0.20260930093343-863fefb78c63/go.mod h1:L+OuMUB/S45GM2NLmK40slCKtX20FLG8Euwlrv4kmjI= github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 h1:0mg9QEFdkkBwJMxvz1tCjHYmfG2iIC6aShj1InDq9/M= github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4/go.mod h1:SSLraQS/jGLYFgff3vuZ+JbVUct6vyEeMzjLBqWqoyM= github.com/dolthub/file-locks v0.1.1 h1:8oKr20PflAVHkroB5Kq1BrtzesvKop9onekWEse1Qr8= diff --git a/testing/go/auth_test.go b/testing/go/auth_test.go index 3949f29b46..3f0910a802 100644 --- a/testing/go/auth_test.go +++ b/testing/go/auth_test.go @@ -1599,6 +1599,136 @@ func TestAuthTests(t *testing.T) { }, }, }, + { + Name: "Views are read with the privileges of their owner", + SetUpScript: []string{ + "CREATE SCHEMA reporting;", + "CREATE TABLE reporting.orders (id INT);", + "INSERT INTO reporting.orders VALUES (1);", + "CREATE VIEW reporting.order_view AS SELECT id FROM reporting.orders;", + "CREATE TABLE items (id INT);", + "INSERT INTO items VALUES (1);", + "CREATE VIEW items_view AS SELECT id FROM items;", + "CREATE ROLE analyst LOGIN PASSWORD 'password';", + "GRANT USAGE ON SCHEMA reporting TO analyst;", + "GRANT SELECT ON reporting.order_view TO analyst;", + "CREATE ROLE analyst2 LOGIN PASSWORD 'password';", + "GRANT USAGE ON SCHEMA reporting TO analyst2;", + "GRANT SELECT ON reporting.order_view, reporting.orders TO analyst2;", + "CREATE ROLE writer LOGIN PASSWORD 'password';", + "GRANT SELECT ON items_view TO writer;", + "CREATE ROLE writer2 LOGIN PASSWORD 'password';", + "GRANT SELECT ON items_view, items TO writer2;", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT * FROM reporting.order_view;", + Username: "analyst", + Password: "password", + Expected: []sql.Row{{1}}, + Skip: true, + }, + { + Query: "SELECT * FROM reporting.order_view;", + Username: "analyst2", + Password: "password", + Expected: []sql.Row{{1}}, + }, + { + Query: "SELECT * FROM items_view;", + Username: "writer", + Password: "password", + Expected: []sql.Row{{1}}, + Skip: true, + }, + { + Query: "INSERT INTO items_view VALUES (2);", + Username: "writer", + Password: "password", + ExpectedErr: "permission denied for view items_view", + ExpectedErrCode: "42501", + Skip: true, + }, + { + Query: "UPDATE items_view SET id = 3;", + Username: "writer", + Password: "password", + ExpectedErr: "permission denied for view items_view", + ExpectedErrCode: "42501", + Skip: true, + }, + { + Query: "DELETE FROM items_view WHERE id = 1;", + Username: "writer", + Password: "password", + ExpectedErr: "permission denied for view items_view", + ExpectedErrCode: "42501", + Skip: true, + }, + { + Query: "INSERT INTO items_view VALUES (2);", + Username: "writer2", + Password: "password", + ExpectedErr: "permission denied for view items_view", + ExpectedErrCode: "42501", + }, + { + Query: "UPDATE items_view SET id = 3;", + Username: "writer2", + Password: "password", + ExpectedErr: "permission denied for view items_view", + ExpectedErrCode: "42501", + }, + { + Query: "DELETE FROM items_view WHERE id = 1;", + Username: "writer2", + Password: "password", + ExpectedErr: "permission denied for view items_view", + ExpectedErrCode: "42501", + }, + }, + }, + { + Name: "Upserts check INSERT and UPDATE on the resolved table", + SetUpScript: []string{ + "CREATE TABLE counts (id INT PRIMARY KEY, n INT);", + "INSERT INTO counts VALUES (1, 1);", + "CREATE ROLE inserter LOGIN PASSWORD 'password';", + "GRANT SELECT, INSERT ON counts TO inserter;", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "INSERT INTO counts VALUES (1, 2) ON CONFLICT (id) DO UPDATE SET n = excluded.n;", + Username: "inserter", + Password: "password", + ExpectedErr: "permission denied for table counts", + ExpectedErrCode: "42501", + }, + { + Query: "INSERT INTO missing_table VALUES (1) ON CONFLICT (id) DO UPDATE SET id = 1;", + Username: "inserter", + Password: "password", + ExpectedErr: "table not found: missing_table", + ExpectedErrCode: "42P01", + }, + { + Query: "GRANT UPDATE ON counts TO inserter;", + Expected: []sql.Row{}, + }, + { + Query: "INSERT INTO counts VALUES (1, 2) ON CONFLICT (id) DO UPDATE SET n = excluded.n;", + Username: "inserter", + Password: "password", + Expected: []sql.Row{}, + }, + { + Query: "SELECT * FROM counts;", + Username: "inserter", + Password: "password", + Expected: []sql.Row{{1, 2}}, + }, + }, + }, }) }