diff --git a/enginetest/queries/priv_auth_queries.go b/enginetest/queries/priv_auth_queries.go index 2b8c69eaf9..4a5a219538 100644 --- a/enginetest/queries/priv_auth_queries.go +++ b/enginetest/queries/priv_auth_queries.go @@ -108,6 +108,29 @@ type ServerAuthenticationTestAssertion struct { // UserPrivTests test the user and privilege systems. These tests always have the root account available, and the root // account is used with any queries in the SetUpScript. var UserPrivTests = []UserPrivilegeTest{ + // https://github.com/dolthub/dolt/issues/10083 + { + Name: "Selecting a view only needs SELECT grants", + SetUpScript: []string{ + "CREATE TABLE perms_t (id INT PRIMARY KEY, val VARCHAR(10));", + "INSERT INTO perms_t VALUES (1, 'a'), (2, 'b');", + "CREATE VIEW perms_v AS SELECT id, val FROM perms_t ORDER BY id;", + "CREATE USER 'view_reader'@'localhost' IDENTIFIED BY 'pw';", + "GRANT SELECT ON perms_t TO 'view_reader'@'localhost';", + "GRANT SELECT ON perms_v TO 'view_reader'@'localhost';", + }, + Assertions: []UserPrivilegeTestAssertion{ + { + User: "view_reader", + Host: "localhost", + Query: "SELECT * FROM perms_v", + Expected: []sql.Row{ + {int64(1), "a"}, + {int64(2), "b"}, + }, + }, + }, + }, { Name: "Create user limits", Assertions: []UserPrivilegeTestAssertion{ diff --git a/sql/base_session.go b/sql/base_session.go index ecb1172119..52f923203d 100644 --- a/sql/base_session.go +++ b/sql/base_session.go @@ -96,7 +96,6 @@ func (s *BaseSession) Client() Client { return s.client } // SetClient implements the Session interface. func (s *BaseSession) SetClient(c Client) { s.client = c - return } // GetAllSessionVariables implements the Session interface. diff --git a/sql/planbuilder/definer.go b/sql/planbuilder/definer.go new file mode 100644 index 0000000000..a3b40dc613 --- /dev/null +++ b/sql/planbuilder/definer.go @@ -0,0 +1,51 @@ +// Copyright 2025 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package planbuilder + +import ( + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/mysql_db" +) + +// mockDefiner temporarily impersonates the definer during binding. It clones the current authorization state +// (when available), adds the requested global privileges (e.g. CREATE VIEW), and updates both the session privilege +// cache and the cached AuthorizationQueryState. Callers must defer the returned restore function. +func (b *Builder) mockDefiner(privileges ...sql.PrivilegeType) func() { + if b == nil || b.ctx == nil || b.ctx.Session == nil { + return func() {} + } + + var privilegeSet mysql_db.PrivilegeSet + if state, ok := b.authQueryState.(defaultAuthorizationQueryState); ok && state.enabled { + privilegeSet = state.privSet.Copy() + } else { + privilegeSet = mysql_db.NewPrivilegeSet() + } + privilegeSet.AddGlobalStatic(privileges...) + + initialAuthQueryState := b.authQueryState + if state, ok := b.authQueryState.(defaultAuthorizationQueryState); ok { + state.privSet = privilegeSet + b.authQueryState = state + } + + initialPrivilegeSet, initialCounter := b.ctx.Session.GetPrivilegeSet() + b.ctx.SetPrivilegeSet(privilegeSet, initialCounter) + + return func() { + b.authQueryState = initialAuthQueryState + b.ctx.SetPrivilegeSet(initialPrivilegeSet, initialCounter) + } +} diff --git a/sql/planbuilder/from.go b/sql/planbuilder/from.go index 8df135a69e..8ef39c0b65 100644 --- a/sql/planbuilder/from.go +++ b/sql/planbuilder/from.go @@ -845,6 +845,9 @@ func (b *Builder) resolveView(name string, database sql.Database, asOf interface if err != nil { b.handleErr(err) } + // TODO: Once view definers are persisted, load the real definer client + restoreInvoker := b.mockDefiner(sql.PrivilegeType_CreateView) + defer restoreInvoker() node, _, err := b.bindOnlyWithDatabase(database, stmt, viewDef.CreateViewStatement) if err != nil { // TODO: Need to account for non-existing functions or