github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/dtables/branch_control_table.go (about)

     1  // Copyright 2022 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package dtables
    16  
    17  import (
    18  	"fmt"
    19  	"math"
    20  	"strings"
    21  
    22  	"github.com/dolthub/go-mysql-server/sql"
    23  	"github.com/dolthub/go-mysql-server/sql/types"
    24  	"github.com/dolthub/vitess/go/sqltypes"
    25  
    26  	"github.com/dolthub/dolt/go/libraries/doltcore/branch_control"
    27  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle/index"
    28  )
    29  
    30  const (
    31  	AccessTableName = "dolt_branch_control"
    32  )
    33  
    34  // PermissionsStrings is a slice of strings representing the available branch_control.branch_control.Permissions. The order of the
    35  // strings should exactly match the order of the branch_control.Permissions according to their flag value.
    36  var PermissionsStrings = []string{"admin", "write", "read"}
    37  
    38  // accessSchema is the schema for the "dolt_branch_control" table.
    39  var accessSchema = sql.Schema{
    40  	&sql.Column{
    41  		Name:       "database",
    42  		Type:       types.MustCreateString(sqltypes.VarChar, 16383, sql.Collation_utf8mb4_0900_ai_ci),
    43  		Source:     AccessTableName,
    44  		PrimaryKey: true,
    45  	},
    46  	&sql.Column{
    47  		Name:       "branch",
    48  		Type:       types.MustCreateString(sqltypes.VarChar, 16383, sql.Collation_utf8mb4_0900_ai_ci),
    49  		Source:     AccessTableName,
    50  		PrimaryKey: true,
    51  	},
    52  	&sql.Column{
    53  		Name:       "user",
    54  		Type:       types.MustCreateString(sqltypes.VarChar, 16383, sql.Collation_utf8mb4_0900_bin),
    55  		Source:     AccessTableName,
    56  		PrimaryKey: true,
    57  	},
    58  	&sql.Column{
    59  		Name:       "host",
    60  		Type:       types.MustCreateString(sqltypes.VarChar, 16383, sql.Collation_utf8mb4_0900_ai_ci),
    61  		Source:     AccessTableName,
    62  		PrimaryKey: true,
    63  	},
    64  	&sql.Column{
    65  		Name:       "permissions",
    66  		Type:       types.MustCreateSetType(PermissionsStrings, sql.Collation_utf8mb4_0900_ai_ci),
    67  		Source:     AccessTableName,
    68  		PrimaryKey: false,
    69  	},
    70  }
    71  
    72  // BranchControlTable provides a layer over the branch_control.Access structure, exposing it as a system table.
    73  type BranchControlTable struct {
    74  	*branch_control.Access
    75  }
    76  
    77  var _ sql.Table = BranchControlTable{}
    78  var _ sql.InsertableTable = BranchControlTable{}
    79  var _ sql.ReplaceableTable = BranchControlTable{}
    80  var _ sql.UpdatableTable = BranchControlTable{}
    81  var _ sql.DeletableTable = BranchControlTable{}
    82  var _ sql.RowInserter = BranchControlTable{}
    83  var _ sql.RowReplacer = BranchControlTable{}
    84  var _ sql.RowUpdater = BranchControlTable{}
    85  var _ sql.RowDeleter = BranchControlTable{}
    86  
    87  // NewBranchControlTable returns a new BranchControlTable.
    88  func NewBranchControlTable(access *branch_control.Access) BranchControlTable {
    89  	return BranchControlTable{access}
    90  }
    91  
    92  // Name implements the interface sql.Table.
    93  func (tbl BranchControlTable) Name() string {
    94  	return AccessTableName
    95  }
    96  
    97  // String implements the interface sql.Table.
    98  func (tbl BranchControlTable) String() string {
    99  	return AccessTableName
   100  }
   101  
   102  // Schema implements the interface sql.Table.
   103  func (tbl BranchControlTable) Schema() sql.Schema {
   104  	return accessSchema
   105  }
   106  
   107  // Collation implements the interface sql.Table.
   108  func (tbl BranchControlTable) Collation() sql.CollationID {
   109  	return sql.Collation_Default
   110  }
   111  
   112  // Partitions implements the interface sql.Table.
   113  func (tbl BranchControlTable) Partitions(context *sql.Context) (sql.PartitionIter, error) {
   114  	return index.SinglePartitionIterFromNomsMap(nil), nil
   115  }
   116  
   117  // PartitionRows implements the interface sql.Table.
   118  func (tbl BranchControlTable) PartitionRows(context *sql.Context, partition sql.Partition) (sql.RowIter, error) {
   119  	tbl.RWMutex.RLock()
   120  	defer tbl.RWMutex.RUnlock()
   121  
   122  	var rows []sql.Row
   123  	rowIter := tbl.Iter()
   124  	for value, ok := rowIter.Next(); ok; value, ok = rowIter.Next() {
   125  		rows = append(rows, sql.Row{
   126  			value.Database,
   127  			value.Branch,
   128  			value.User,
   129  			value.Host,
   130  			uint64(value.Permissions),
   131  		})
   132  	}
   133  	return sql.RowsToRowIter(rows...), nil
   134  }
   135  
   136  // Inserter implements the interface sql.InsertableTable.
   137  func (tbl BranchControlTable) Inserter(context *sql.Context) sql.RowInserter {
   138  	return tbl
   139  }
   140  
   141  // Replacer implements the interface sql.ReplaceableTable.
   142  func (tbl BranchControlTable) Replacer(ctx *sql.Context) sql.RowReplacer {
   143  	return tbl
   144  }
   145  
   146  // Updater implements the interface sql.UpdatableTable.
   147  func (tbl BranchControlTable) Updater(ctx *sql.Context) sql.RowUpdater {
   148  	return tbl
   149  }
   150  
   151  // Deleter implements the interface sql.DeletableTable.
   152  func (tbl BranchControlTable) Deleter(context *sql.Context) sql.RowDeleter {
   153  	return tbl
   154  }
   155  
   156  // StatementBegin implements the interface sql.TableEditor.
   157  func (tbl BranchControlTable) StatementBegin(ctx *sql.Context) {
   158  	//TODO: will use the binlog to implement
   159  }
   160  
   161  // DiscardChanges implements the interface sql.TableEditor.
   162  func (tbl BranchControlTable) DiscardChanges(ctx *sql.Context, errorEncountered error) error {
   163  	//TODO: will use the binlog to implement
   164  	return nil
   165  }
   166  
   167  // StatementComplete implements the interface sql.TableEditor.
   168  func (tbl BranchControlTable) StatementComplete(ctx *sql.Context) error {
   169  	//TODO: will use the binlog to implement
   170  	return nil
   171  }
   172  
   173  // Insert implements the interface sql.RowInserter.
   174  func (tbl BranchControlTable) Insert(ctx *sql.Context, row sql.Row) error {
   175  	tbl.RWMutex.Lock()
   176  	defer tbl.RWMutex.Unlock()
   177  
   178  	// Database, Branch, and Host are case-insensitive, while user is case-sensitive
   179  	database := strings.ToLower(branch_control.FoldExpression(row[0].(string)))
   180  	branch := strings.ToLower(branch_control.FoldExpression(row[1].(string)))
   181  	user := branch_control.FoldExpression(row[2].(string))
   182  	host := strings.ToLower(branch_control.FoldExpression(row[3].(string)))
   183  	perms := branch_control.Permissions(row[4].(uint64))
   184  
   185  	// Verify that the lengths of each expression fit within an uint16
   186  	if len(database) > math.MaxUint16 || len(branch) > math.MaxUint16 || len(user) > math.MaxUint16 || len(host) > math.MaxUint16 {
   187  		return branch_control.ErrExpressionsTooLong.New(database, branch, user, host)
   188  	}
   189  
   190  	// A nil session means we're not in the SQL context, so we allow the insertion in such a case
   191  	if branchAwareSession := branch_control.GetBranchAwareSession(ctx); branchAwareSession != nil &&
   192  		// Having the correct database privileges also allows the insertion
   193  		!branch_control.HasDatabasePrivileges(branchAwareSession, database) {
   194  		insertUser := branchAwareSession.GetUser()
   195  		insertHost := branchAwareSession.GetHost()
   196  		// As we've folded the branch expression, we can use it directly as though it were a normal branch name to
   197  		// determine if the user attempting the insertion has permission to perform the insertion.
   198  		_, modPerms := tbl.Match(database, branch, insertUser, insertHost)
   199  		if modPerms&branch_control.Permissions_Admin != branch_control.Permissions_Admin {
   200  			permStr, _ := accessSchema[4].Type.(sql.SetType).BitsToString(uint64(perms))
   201  			return branch_control.ErrInsertingAccessRow.New(insertUser, insertHost, database, branch, user, host, permStr)
   202  		}
   203  	}
   204  
   205  	// We check if we're inserting a subset of an already-existing row. If we are, we deny the insertion as the existing
   206  	// row will already match against ALL possible values for this row.
   207  	if ok, modPerms := tbl.Match(database, branch, user, host); ok {
   208  		permBits := uint64(modPerms)
   209  		permStr, _ := accessSchema[4].Type.(sql.SetType).BitsToString(permBits)
   210  		return sql.NewUniqueKeyErr(
   211  			fmt.Sprintf(`[%q, %q, %q, %q, %q]`, database, branch, user, host, permStr),
   212  			true,
   213  			sql.Row{database, branch, user, host, permBits})
   214  	}
   215  
   216  	tbl.Access.Insert(database, branch, user, host, perms)
   217  	return nil
   218  }
   219  
   220  // Update implements the interface sql.RowUpdater.
   221  func (tbl BranchControlTable) Update(ctx *sql.Context, old sql.Row, new sql.Row) error {
   222  	tbl.RWMutex.Lock()
   223  	defer tbl.RWMutex.Unlock()
   224  
   225  	// Database, Branch, and Host are case-insensitive, while User is case-sensitive
   226  	oldDatabase := strings.ToLower(branch_control.FoldExpression(old[0].(string)))
   227  	oldBranch := strings.ToLower(branch_control.FoldExpression(old[1].(string)))
   228  	oldUser := branch_control.FoldExpression(old[2].(string))
   229  	oldHost := strings.ToLower(branch_control.FoldExpression(old[3].(string)))
   230  	newDatabase := strings.ToLower(branch_control.FoldExpression(new[0].(string)))
   231  	newBranch := strings.ToLower(branch_control.FoldExpression(new[1].(string)))
   232  	newUser := branch_control.FoldExpression(new[2].(string))
   233  	newHost := strings.ToLower(branch_control.FoldExpression(new[3].(string)))
   234  	newPerms := branch_control.Permissions(new[4].(uint64))
   235  
   236  	// Verify that the lengths of each expression fit within an uint16
   237  	if len(newDatabase) > math.MaxUint16 || len(newBranch) > math.MaxUint16 || len(newUser) > math.MaxUint16 || len(newHost) > math.MaxUint16 {
   238  		return branch_control.ErrExpressionsTooLong.New(newDatabase, newBranch, newUser, newHost)
   239  	}
   240  
   241  	// If we're not updating the same row, then we check for a row violation
   242  	if oldDatabase != newDatabase || oldBranch != newBranch || oldUser != newUser || oldHost != newHost {
   243  		if ok, modPerms := tbl.Match(newDatabase, newBranch, newUser, newHost); ok {
   244  			permBits := uint64(modPerms)
   245  			permStr, _ := accessSchema[4].Type.(sql.SetType).BitsToString(permBits)
   246  			return sql.NewUniqueKeyErr(
   247  				fmt.Sprintf(`[%q, %q, %q, %q, %q]`, newDatabase, newBranch, newUser, newHost, permStr),
   248  				true,
   249  				sql.Row{newDatabase, newBranch, newUser, newHost, permBits})
   250  		}
   251  	}
   252  
   253  	// A nil session means we're not in the SQL context, so we'd allow the update in such a case
   254  	if branchAwareSession := branch_control.GetBranchAwareSession(ctx); branchAwareSession != nil {
   255  		insertUser := branchAwareSession.GetUser()
   256  		insertHost := branchAwareSession.GetHost()
   257  		if !branch_control.HasDatabasePrivileges(branchAwareSession, oldDatabase) {
   258  			// As we've folded the branch expression, we can use it directly as though it were a normal branch name to
   259  			// determine if the user attempting the update has permission to perform the update on the old branch name.
   260  			_, modPerms := tbl.Match(oldDatabase, oldBranch, insertUser, insertHost)
   261  			if modPerms&branch_control.Permissions_Admin != branch_control.Permissions_Admin {
   262  				return branch_control.ErrUpdatingRow.New(insertUser, insertHost, oldDatabase, oldBranch, oldUser, oldHost)
   263  			}
   264  		}
   265  		if !branch_control.HasDatabasePrivileges(branchAwareSession, newDatabase) {
   266  			// Similar to the block above, we check if the user has permission to use the new branch name
   267  			_, modPerms := tbl.Match(newDatabase, newBranch, insertUser, insertHost)
   268  			if modPerms&branch_control.Permissions_Admin != branch_control.Permissions_Admin {
   269  				return branch_control.ErrUpdatingToRow.
   270  					New(insertUser, insertHost, oldDatabase, oldBranch, oldUser, oldHost, newDatabase, newBranch)
   271  			}
   272  		}
   273  	}
   274  
   275  	tbl.Access.Delete(oldDatabase, oldBranch, oldUser, oldHost)
   276  	tbl.Access.Insert(newDatabase, newBranch, newUser, newHost, newPerms)
   277  	return nil
   278  }
   279  
   280  // Delete implements the interface sql.RowDeleter.
   281  func (tbl BranchControlTable) Delete(ctx *sql.Context, row sql.Row) error {
   282  	tbl.RWMutex.Lock()
   283  	defer tbl.RWMutex.Unlock()
   284  
   285  	// Database, Branch, and Host are case-insensitive, while User is case-sensitive
   286  	database := strings.ToLower(branch_control.FoldExpression(row[0].(string)))
   287  	branch := strings.ToLower(branch_control.FoldExpression(row[1].(string)))
   288  	user := branch_control.FoldExpression(row[2].(string))
   289  	host := strings.ToLower(branch_control.FoldExpression(row[3].(string)))
   290  
   291  	// A nil session means we're not in the SQL context, so we allow the deletion in such a case
   292  	if branchAwareSession := branch_control.GetBranchAwareSession(ctx); branchAwareSession != nil &&
   293  		// Having the correct database privileges also allows the deletion
   294  		!branch_control.HasDatabasePrivileges(branchAwareSession, database) {
   295  		insertUser := branchAwareSession.GetUser()
   296  		insertHost := branchAwareSession.GetHost()
   297  		// As we've folded the branch expression, we can use it directly as though it were a normal branch name to
   298  		// determine if the user attempting the deletion has permission to perform the deletion.
   299  		_, modPerms := tbl.Match(database, branch, insertUser, insertHost)
   300  		if modPerms&branch_control.Permissions_Admin != branch_control.Permissions_Admin {
   301  			return branch_control.ErrDeletingRow.New(insertUser, insertHost, database, branch, user, host)
   302  		}
   303  	}
   304  
   305  	tbl.Access.Delete(database, branch, user, host)
   306  	return nil
   307  }
   308  
   309  // Close implements the interface sql.Closer.
   310  func (tbl BranchControlTable) Close(context *sql.Context) error {
   311  	return branch_control.SaveData(context)
   312  }