// Copyright 2024 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 auth import ( "os" "path/filepath" "sync" "sync/atomic" "github.com/dolthub/dolt/go/libraries/doltcore/env" "github.com/dolthub/dolt/go/libraries/utils/filesys" ) // authFileName is the name of the file that contains all authorization-related data. var authFileName = "auth.db" var ( globalDatabase Database globalLock *sync.RWMutex userIDCounter atomic.Uint64 fileSystem filesys.Filesys ) // Database contains all information pertaining to authorization and privileges. This is a global structure that is // shared between all branches. type Database struct { rolesByName map[string]RoleID rolesByID map[RoleID]Role databasePrivileges *DatabasePrivileges schemaPrivileges *SchemaPrivileges tablePrivileges *TablePrivileges sequencePrivileges *SequencePrivileges routinePrivileges *RoutinePrivileges roleMembership *RoleMembership } // ClearDatabase clears the internal database, leaving only the default users. This is primarily for use by tests. func ClearDatabase() { clear(globalDatabase.rolesByName) clear(globalDatabase.rolesByID) clear(globalDatabase.databasePrivileges.Data) clear(globalDatabase.schemaPrivileges.Data) clear(globalDatabase.tablePrivileges.Data) clear(globalDatabase.sequencePrivileges.Data) clear(globalDatabase.routinePrivileges.Data) clear(globalDatabase.roleMembership.Data) dbInitDefault() } // DropRole removes the given role from the database. If the role does not exist, then this is a no-op. func DropRole(name string) { if roleID, ok := globalDatabase.rolesByName[name]; ok { delete(globalDatabase.rolesByName, name) delete(globalDatabase.rolesByID, roleID) // TODO: remove from ownership, schema privileges, table privileges, and role membership } } // GetRole returns the role with the given name. Use RoleExists to determine if the role exists, as this will return a // role with the default values set if it does not exist. func GetRole(name string) Role { roleID, ok := globalDatabase.rolesByName[name] if !ok { return createDefaultRoleWithoutID(name) } return globalDatabase.rolesByID[roleID] } // RenameRole renames the role with the old name to the new name. If the role does not exist, then this is a no-op. func RenameRole(oldName string, newName string) { if roleID, ok := globalDatabase.rolesByName[oldName]; ok { delete(globalDatabase.rolesByName, oldName) globalDatabase.rolesByName[newName] = roleID role := globalDatabase.rolesByID[roleID] role.Name = newName globalDatabase.rolesByID[roleID] = role } } // RoleExists returns whether the given role exists. func RoleExists(name string) bool { _, ok := globalDatabase.rolesByName[name] return ok } // SetRole sets the role matching the given name. This will add a role that does not yet exist, and overwrite an // existing role. func SetRole(role Role) { // We want to ignore invalid roles, which should not exist outside specific circumstances (like during login) if role.id == 0 { return } if existingRole, ok := globalDatabase.rolesByID[role.id]; ok { delete(globalDatabase.rolesByName, existingRole.Name) } globalDatabase.rolesByName[role.Name] = role.id globalDatabase.rolesByID[role.ID()] = role } // IsSuperUser returns whether the given role is a SUPERUSER. func IsSuperUser(role RoleID) bool { return globalDatabase.rolesByID[role].IsSuperUser } // LockRead takes an anonymous function and runs it while using a read lock. This ensures that the lock is automatically // released once the function finishes. func LockRead(f func()) { globalLock.RLock() defer globalLock.RUnlock() f() } // LockWrite takes an anonymous function and runs it while using a write lock. This ensures that the lock is // automatically released once the function finishes. func LockWrite(f func()) { globalLock.Lock() defer globalLock.Unlock() f() } // dbInit handle the global database initialization. Panics if an error occurs, since it points to something going // terribly wrong. func dbInit(dEnv *env.DoltEnv, cfg Config) { globalDatabase = Database{ rolesByName: make(map[string]RoleID), rolesByID: make(map[RoleID]Role), databasePrivileges: NewDatabasePrivileges(), schemaPrivileges: NewSchemaPrivileges(), tablePrivileges: NewTablePrivileges(), sequencePrivileges: NewSequencePrivileges(), routinePrivileges: NewRoutinePrivileges(), roleMembership: NewRoleMembership(), } globalLock = &sync.RWMutex{} if dEnv != nil { if _, ok := dEnv.FS.(*filesys.InMemFS); !ok { if cfg != nil && len(cfg.AuthFilePath()) > 0 { authFileName = cfg.AuthFilePath() } fileSystem = dEnv.FS authData, err := fileSystem.ReadFile(authFileName) if os.IsNotExist(err) { dbInitDefault() if err = dbInitCreateAuthDirectory(authFileName); err != nil { panic(err) } if err = fileSystem.WriteFile(authFileName, globalDatabase.serialize(), 0644); err != nil { panic(err) } } else if err != nil { panic(err) } else if err = globalDatabase.deserialize(authData); err != nil { panic(err) } } else { dbInitDefault() } } else { dbInitDefault() } } // dbInitDefault initializes the database and fills it with default users for testing. func dbInitDefault() { user, password := GetSuperUserAndPassword() public := CreateDefaultRole("public") SetRole(public) superUser := CreateDefaultRole(user) superUser.IsSuperUser = true superUser.CanCreateRoles = true superUser.CanCreateDB = true superUser.CanLogin = true var err error superUser.Password, err = NewScramSha256Password(password) if err != nil { panic(err) } SetRole(superUser) } // dbInitCreateAuthDirectory creates the directory structure pointed to by the auth file if it does not already exist. func dbInitCreateAuthDirectory(authFileName string) error { dir, _ := filepath.Split(authFileName) // If there's no directory, then we have nothing to create if len(dir) == 0 { return nil } fullDir, err := fileSystem.Abs(dir) if err != nil { return err } if exists, _ := fileSystem.Exists(fullDir); !exists { return fileSystem.MkDirs(fullDir) } return nil }