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 }