Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

305 lines
11 KiB
Go

// Copyright 2022 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 branch_control
import (
"fmt"
"math"
"strings"
"sync"
flatbuffers "github.com/dolthub/flatbuffers/v23/go"
"github.com/dolthub/dolt/go/gen/fb/serial"
)
// Permissions are a set of flags that denote a user's allowed functionality on a branch.
type Permissions uint64
const (
Permissions_Admin Permissions = 1 << iota // Permissions_Admin grants unrestricted control over a branch, including modification of table entries
Permissions_Write // Permissions_Write allows for all modifying operations on a branch, but does not allow modification of table entries
Permissions_Merge // Permissions_Merge allows for merging into this branch with dolt_merge, but does not allow for arbitrary writes.
Permissions_Read // Permissions_Read allows for reading from a branch, which is equivalent to having no permissions
Permissions_None Permissions = 0 // Permissions_None represents a lack of permissions, which defaults to allowing reading
)
// Access contains all of the expressions that comprise the "dolt_branch_control" table, which handles write Access to
// branches, along with write access to the branch control system tables.
type Access struct {
Root *MatchNode
RWMutex *sync.RWMutex
binlog *Binlog
rows []AccessRow
freeRows []uint32
}
// AccessRow contains the user-facing values of a particular row, along with the permissions for a row.
type AccessRow struct {
Database string
Branch string
User string
Host string
Permissions Permissions
}
// AccessRowIter is an iterator over all valid rows.
type AccessRowIter struct {
access *Access
idx uint32
}
// newAccess returns a new Access.
func newAccess() *Access {
return &Access{
RWMutex: &sync.RWMutex{},
}
}
// Match returns whether any entries match the given database, branch, user, and host, along with their permissions.
// This will match subsets against their superset as well. Requires external synchronization handling, therefore
// manually manage the RWMutex.
func (tbl *Access) Match(database string, branch string, user string, host string) (bool, Permissions) {
return tbl.MatchIgnoringRow(database, branch, user, host, -1)
}
// MatchIgnoringRow returns whether any entries match the given database, branch, user, and host, along with their
// permissions. This will match subsets against their superset as well. If `rowToIgnore` is >= 0, then that row is
// ignored from the match results. Requires external synchronization handling, therefore manually manage the RWMutex.
func (tbl *Access) MatchIgnoringRow(database string, branch string, user string, host string, rowToIgnore int64) (bool, Permissions) {
results := tbl.Root.Match(database, branch, user, host)
// We use the result(s) with the longest length
length := uint32(0)
perms := Permissions_None
for _, result := range results {
if int64(result.RowIndex) == rowToIgnore {
continue
}
if result.Length > length {
perms = result.Permissions
length = result.Length
} else if result.Length == length {
perms |= result.Permissions
}
}
// Higher permissions imply lower ones: Admin > Write > Merge > Read.
if perms&Permissions_Admin == Permissions_Admin {
perms |= Permissions_Write | Permissions_Merge | Permissions_Read
} else if perms&Permissions_Write == Permissions_Write {
perms |= Permissions_Merge | Permissions_Read
} else if perms&Permissions_Merge == Permissions_Merge {
perms |= Permissions_Read
}
return len(results) > 0, perms
}
// ExactMatch returns whether any entries exactly match the given database, branch, user, and host. Returns nil if an
// exact match is not found. Requires external synchronization handling, therefore manually manage the RWMutex.
func (tbl *Access) ExactMatch(database string, branch string, user string, host string) *MatchNode {
return tbl.Root.ExactMatch(database, branch, user, host)
}
// GetBinlog returns the table's binlog.
func (tbl *Access) GetBinlog() *Binlog {
return tbl.binlog
}
// Serialize returns the offset for the Access table written to the given builder.
func (tbl *Access) Serialize(b *flatbuffers.Builder) flatbuffers.UOffsetT {
// Serialize the binlog
binlog := tbl.binlog.Serialize(b)
serial.BranchControlAccessStart(b)
serial.BranchControlAccessAddBinlog(b, binlog)
return serial.BranchControlAccessEnd(b)
}
func (tbl *Access) reinit() {
tbl.Root = &MatchNode{
SortOrders: []int32{columnMarker},
Children: make(map[int32]*MatchNode),
Data: nil,
}
tbl.binlog = NewAccessBinlog(nil)
tbl.rows = nil
tbl.freeRows = nil
}
// Deserialize populates the table with the data from the flatbuffers representation.
func (tbl *Access) Deserialize(fb *serial.BranchControlAccess) error {
// Read the binlog
fbBinlog, err := fb.TryBinlog(nil)
if err != nil {
return err
}
binlog := NewAccessBinlog(nil)
if err = binlog.Deserialize(fbBinlog); err != nil {
return err
}
tbl.reinit()
// Recreate the table from the binlog
for _, binlogRow := range binlog.rows {
if binlogRow.IsInsert {
tbl.Insert(binlogRow.Database, binlogRow.Branch, binlogRow.User, binlogRow.Host, Permissions(binlogRow.Permissions))
} else {
tbl.Delete(binlogRow.Database, binlogRow.Branch, binlogRow.User, binlogRow.Host)
}
}
return nil
}
// insertDefaultRow adds a row that allows all users to access and modify all branches, but does not allow them to
// modify any branch control tables. This was the default behavior of Dolt before the introduction of branch permissions.
func (tbl *Access) insertDefaultRow() {
tbl.reinit()
tbl.Insert("%", "%", "%", "%", Permissions_Write)
}
// Insert adds the given expressions to the table. This does not perform any sort of validation whatsoever, so it is
// important to ensure that the expressions are valid before insertion. Folds all strings that are given. Overwrites any
// existing entries with the new permissions. Requires external synchronization handling, therefore manually manage the
// RWMutex.
func (tbl *Access) Insert(database string, branch string, user string, host string, perms Permissions) {
// Database, Branch, and Host are case-insensitive, while User is case-sensitive
database = strings.ToLower(FoldExpression(database))
branch = strings.ToLower(FoldExpression(branch))
user = FoldExpression(user)
host = strings.ToLower(FoldExpression(host))
// 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
if len(database) > math.MaxUint16 {
database = string(append([]byte(database[:math.MaxUint16-1]), byte('%')))
}
if len(branch) > math.MaxUint16 {
branch = string(append([]byte(branch[:math.MaxUint16-1]), byte('%')))
}
if len(user) > math.MaxUint16 {
user = string(append([]byte(user[:math.MaxUint16-1]), byte('%')))
}
if len(host) > math.MaxUint16 {
host = string(append([]byte(host[:math.MaxUint16-1]), byte('%')))
}
// Add the insertion entry to the binlog
tbl.binlog.Insert(database, branch, user, host, uint64(perms))
// Add to the rows and grab the insertion index
var index uint32
if len(tbl.freeRows) > 0 {
index = tbl.freeRows[len(tbl.freeRows)-1]
tbl.freeRows = tbl.freeRows[:len(tbl.freeRows)-1]
tbl.rows[index] = AccessRow{
Database: database,
Branch: branch,
User: user,
Host: host,
Permissions: perms,
}
} else {
if len(tbl.rows) >= math.MaxUint32 {
// If someone has this many branches in Dolt then they're doing something very interesting, we'll probably
// fail elsewhere way before this point
panic(fmt.Errorf("branch control has a maximum limit of %d branches", math.MaxUint32-1))
}
index = uint32(len(tbl.rows))
tbl.rows = append(tbl.rows, AccessRow{
Database: database,
Branch: branch,
User: user,
Host: host,
Permissions: perms,
})
}
// Add the entry to the root node
tbl.Root.Add(database, branch, user, host, MatchNodeData{
Permissions: perms,
RowIndex: index,
})
}
// Delete removes the given expressions from the table. This does not perform any sort of validation whatsoever, so it
// is important to ensure that the expressions are valid before deletion. Folds all strings that are given. Requires
// external synchronization handling, therefore manually manage the RWMutex.
func (tbl *Access) Delete(database string, branch string, user string, host string) {
// Database, Branch, and Host are case-insensitive, while User is case-sensitive
database = strings.ToLower(FoldExpression(database))
branch = strings.ToLower(FoldExpression(branch))
user = FoldExpression(user)
host = strings.ToLower(FoldExpression(host))
// 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
if len(database) > math.MaxUint16 {
database = string(append([]byte(database[:math.MaxUint16-1]), byte('%')))
}
if len(branch) > math.MaxUint16 {
branch = string(append([]byte(branch[:math.MaxUint16-1]), byte('%')))
}
if len(user) > math.MaxUint16 {
user = string(append([]byte(user[:math.MaxUint16-1]), byte('%')))
}
if len(host) > math.MaxUint16 {
host = string(append([]byte(host[:math.MaxUint16-1]), byte('%')))
}
// Remove the entry from the root node
removedIndex, success := tbl.Root.Remove(database, branch, user, host)
// Add the deletion entry to the binlog only if we had something to delete
if success {
tbl.binlog.Delete(database, branch, user, host, uint64(Permissions_None))
}
// Remove from the rows
if removedIndex != math.MaxUint32 {
tbl.freeRows = append(tbl.freeRows, removedIndex)
}
}
// Iter returns an iterator that goes over all valid rows. The iterator does not acquire a read lock, therefore this
// requires external synchronization handling via RWMutex.
func (tbl *Access) Iter() *AccessRowIter {
return &AccessRowIter{
access: tbl,
idx: 0,
}
}
// Next returns the next valid row. Returns false if there are no more rows.
func (iter *AccessRowIter) Next() (AccessRow, bool) {
OuterLoop:
for iter.idx < uint32(len(iter.access.rows)) {
idx := iter.idx
iter.idx++
// Not the most efficient, but I expect this to be empty 99% of the time so it should be fine
for _, freeRow := range iter.access.freeRows {
if idx == freeRow {
continue OuterLoop
}
}
return iter.access.rows[idx], true
}
return AccessRow{}, false
}
// Consolidate reduces the permission set down to the most representative permission. For example, having both admin and
// write permissions are equivalent to only having the admin permission. Additionally, having no permissions is
// equivalent to only having the read permission.
func (perm Permissions) Consolidate() Permissions {
if perm&Permissions_Admin == Permissions_Admin {
return Permissions_Admin
} else if perm&Permissions_Write == Permissions_Write {
return Permissions_Write
} else if perm&Permissions_Merge == Permissions_Merge {
return Permissions_Merge
} else {
return Permissions_Read
}
}