github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/branch_control/access.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 branch_control
    16  
    17  import (
    18  	"fmt"
    19  	"math"
    20  	"strings"
    21  	"sync"
    22  
    23  	flatbuffers "github.com/dolthub/flatbuffers/v23/go"
    24  
    25  	"github.com/dolthub/dolt/go/gen/fb/serial"
    26  )
    27  
    28  // Permissions are a set of flags that denote a user's allowed functionality on a branch.
    29  type Permissions uint64
    30  
    31  const (
    32  	Permissions_Admin Permissions = 1 << iota // Permissions_Admin grants unrestricted control over a branch, including modification of table entries
    33  	Permissions_Write                         // Permissions_Write allows for all modifying operations on a branch, but does not allow modification of table entries
    34  	Permissions_Read                          // Permissions_Read allows for reading from a branch, which is equivalent to having no permissions
    35  
    36  	Permissions_None Permissions = 0 // Permissions_None represents a lack of permissions, which defaults to allowing reading
    37  )
    38  
    39  // Access contains all of the expressions that comprise the "dolt_branch_control" table, which handles write Access to
    40  // branches, along with write access to the branch control system tables.
    41  type Access struct {
    42  	Root     *MatchNode
    43  	RWMutex  *sync.RWMutex
    44  	binlog   *Binlog
    45  	rows     []AccessRow
    46  	freeRows []uint32
    47  }
    48  
    49  // AccessRow contains the user-facing values of a particular row, along with the permissions for a row.
    50  type AccessRow struct {
    51  	Database    string
    52  	Branch      string
    53  	User        string
    54  	Host        string
    55  	Permissions Permissions
    56  }
    57  
    58  // AccessRowIter is an iterator over all valid rows.
    59  type AccessRowIter struct {
    60  	access *Access
    61  	idx    uint32
    62  }
    63  
    64  // newAccess returns a new Access.
    65  func newAccess() *Access {
    66  	return &Access{
    67  		RWMutex: &sync.RWMutex{},
    68  	}
    69  }
    70  
    71  // Match returns whether any entries match the given database, branch, user, and host, along with their permissions.
    72  // Requires external synchronization handling, therefore manually manage the RWMutex.
    73  func (tbl *Access) Match(database string, branch string, user string, host string) (bool, Permissions) {
    74  	results := tbl.Root.Match(database, branch, user, host)
    75  	// We use the result(s) with the longest length
    76  	length := uint32(0)
    77  	perms := Permissions_None
    78  	for _, result := range results {
    79  		if result.Length > length {
    80  			perms = result.Permissions
    81  		} else if result.Length == length {
    82  			perms |= result.Permissions
    83  		}
    84  	}
    85  	return len(results) > 0, perms
    86  }
    87  
    88  // GetBinlog returns the table's binlog.
    89  func (tbl *Access) GetBinlog() *Binlog {
    90  	return tbl.binlog
    91  }
    92  
    93  // Serialize returns the offset for the Access table written to the given builder.
    94  func (tbl *Access) Serialize(b *flatbuffers.Builder) flatbuffers.UOffsetT {
    95  	// Serialize the binlog
    96  	binlog := tbl.binlog.Serialize(b)
    97  	serial.BranchControlAccessStart(b)
    98  	serial.BranchControlAccessAddBinlog(b, binlog)
    99  	return serial.BranchControlAccessEnd(b)
   100  }
   101  
   102  func (tbl *Access) reinit() {
   103  	tbl.Root = &MatchNode{
   104  		SortOrders: []int32{columnMarker},
   105  		Children:   make(map[int32]*MatchNode),
   106  		Data:       nil,
   107  	}
   108  	tbl.binlog = NewAccessBinlog(nil)
   109  	tbl.rows = nil
   110  	tbl.freeRows = nil
   111  }
   112  
   113  // Deserialize populates the table with the data from the flatbuffers representation.
   114  func (tbl *Access) Deserialize(fb *serial.BranchControlAccess) error {
   115  	// Read the binlog
   116  	fbBinlog, err := fb.TryBinlog(nil)
   117  	if err != nil {
   118  		return err
   119  	}
   120  	binlog := NewAccessBinlog(nil)
   121  	if err = binlog.Deserialize(fbBinlog); err != nil {
   122  		return err
   123  	}
   124  
   125  	tbl.reinit()
   126  
   127  	// Recreate the table from the binlog
   128  	for _, binlogRow := range binlog.rows {
   129  		if binlogRow.IsInsert {
   130  			tbl.Insert(binlogRow.Database, binlogRow.Branch, binlogRow.User, binlogRow.Host, Permissions(binlogRow.Permissions))
   131  		} else {
   132  			tbl.Delete(binlogRow.Database, binlogRow.Branch, binlogRow.User, binlogRow.Host)
   133  		}
   134  	}
   135  	return nil
   136  }
   137  
   138  // insertDefaultRow adds a row that allows all users to access and modify all branches, but does not allow them to
   139  // modify any branch control tables. This was the default behavior of Dolt before the introduction of branch permissions.
   140  func (tbl *Access) insertDefaultRow() {
   141  	tbl.reinit()
   142  	tbl.Insert("%", "%", "%", "%", Permissions_Write)
   143  }
   144  
   145  // Insert adds the given expressions to the table. This does not perform any sort of validation whatsoever, so it is
   146  // important to ensure that the expressions are valid before insertion. Folds all strings that are given. Overwrites any
   147  // existing entries with the new permissions. Requires external synchronization handling, therefore manually manage the
   148  // RWMutex.
   149  func (tbl *Access) Insert(database string, branch string, user string, host string, perms Permissions) {
   150  	// Database, Branch, and Host are case-insensitive, while User is case-sensitive
   151  	database = strings.ToLower(FoldExpression(database))
   152  	branch = strings.ToLower(FoldExpression(branch))
   153  	user = FoldExpression(user)
   154  	host = strings.ToLower(FoldExpression(host))
   155  	// Each expression is capped at 2¹⁶-1 values, so we truncate to 2¹⁶-2 and add the any-match character at the end if it's over
   156  	if len(database) > math.MaxUint16 {
   157  		database = string(append([]byte(database[:math.MaxUint16-1]), byte('%')))
   158  	}
   159  	if len(branch) > math.MaxUint16 {
   160  		branch = string(append([]byte(branch[:math.MaxUint16-1]), byte('%')))
   161  	}
   162  	if len(user) > math.MaxUint16 {
   163  		user = string(append([]byte(user[:math.MaxUint16-1]), byte('%')))
   164  	}
   165  	if len(host) > math.MaxUint16 {
   166  		host = string(append([]byte(host[:math.MaxUint16-1]), byte('%')))
   167  	}
   168  	// Add the insertion entry to the binlog
   169  	tbl.binlog.Insert(database, branch, user, host, uint64(perms))
   170  	// Add to the rows and grab the insertion index
   171  	var index uint32
   172  	if len(tbl.freeRows) > 0 {
   173  		index = tbl.freeRows[len(tbl.freeRows)-1]
   174  		tbl.freeRows = tbl.freeRows[:len(tbl.freeRows)-1]
   175  		tbl.rows[index] = AccessRow{
   176  			Database:    database,
   177  			Branch:      branch,
   178  			User:        user,
   179  			Host:        host,
   180  			Permissions: perms,
   181  		}
   182  	} else {
   183  		if len(tbl.rows) >= math.MaxUint32 {
   184  			// If someone has this many branches in Dolt then they're doing something very interesting, we'll probably
   185  			// fail elsewhere way before this point
   186  			panic(fmt.Errorf("branch control has a maximum limit of %d branches", math.MaxUint32-1))
   187  		}
   188  		index = uint32(len(tbl.rows))
   189  		tbl.rows = append(tbl.rows, AccessRow{
   190  			Database:    database,
   191  			Branch:      branch,
   192  			User:        user,
   193  			Host:        host,
   194  			Permissions: perms,
   195  		})
   196  	}
   197  	// Add the entry to the root node
   198  	tbl.Root.Add(database, branch, user, host, MatchNodeData{
   199  		Permissions: perms,
   200  		RowIndex:    index,
   201  	})
   202  }
   203  
   204  // Delete removes the given expressions from the table. This does not perform any sort of validation whatsoever, so it
   205  // is important to ensure that the expressions are valid before deletion. Folds all strings that are given. Requires
   206  // external synchronization handling, therefore manually manage the RWMutex.
   207  func (tbl *Access) Delete(database string, branch string, user string, host string) {
   208  	// Database, Branch, and Host are case-insensitive, while User is case-sensitive
   209  	database = strings.ToLower(FoldExpression(database))
   210  	branch = strings.ToLower(FoldExpression(branch))
   211  	user = FoldExpression(user)
   212  	host = strings.ToLower(FoldExpression(host))
   213  	// Each expression is capped at 2¹⁶-1 values, so we truncate to 2¹⁶-2 and add the any-match character at the end if it's over
   214  	if len(database) > math.MaxUint16 {
   215  		database = string(append([]byte(database[:math.MaxUint16-1]), byte('%')))
   216  	}
   217  	if len(branch) > math.MaxUint16 {
   218  		branch = string(append([]byte(branch[:math.MaxUint16-1]), byte('%')))
   219  	}
   220  	if len(user) > math.MaxUint16 {
   221  		user = string(append([]byte(user[:math.MaxUint16-1]), byte('%')))
   222  	}
   223  	if len(host) > math.MaxUint16 {
   224  		host = string(append([]byte(host[:math.MaxUint16-1]), byte('%')))
   225  	}
   226  	// Add the deletion entry to the binlog
   227  	tbl.binlog.Delete(database, branch, user, host, uint64(Permissions_None))
   228  	// Remove the entry from the root node
   229  	removedIndex := tbl.Root.Remove(database, branch, user, host)
   230  	// Remove from the rows
   231  	if removedIndex != math.MaxUint32 {
   232  		tbl.freeRows = append(tbl.freeRows, removedIndex)
   233  	}
   234  }
   235  
   236  // Iter returns an iterator that goes over all valid rows. The iterator does not acquire a read lock, therefore this
   237  // requires external synchronization handling via RWMutex.
   238  func (tbl *Access) Iter() *AccessRowIter {
   239  	return &AccessRowIter{
   240  		access: tbl,
   241  		idx:    0,
   242  	}
   243  }
   244  
   245  // Next returns the next valid row. Returns false if there are no more rows.
   246  func (iter *AccessRowIter) Next() (AccessRow, bool) {
   247  OuterLoop:
   248  	for iter.idx < uint32(len(iter.access.rows)) {
   249  		idx := iter.idx
   250  		iter.idx++
   251  		// Not the most efficient, but I expect this to be empty 99% of the time so it should be fine
   252  		for _, freeRow := range iter.access.freeRows {
   253  			if idx == freeRow {
   254  				continue OuterLoop
   255  			}
   256  		}
   257  		return iter.access.rows[idx], true
   258  	}
   259  	return AccessRow{}, false
   260  }