chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:25 +08:00
commit e014feafe1
2285 changed files with 1131979 additions and 0 deletions
+387
View File
@@ -0,0 +1,387 @@
// 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 (
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/go-mysql-server/sql"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/server/functions/framework"
)
// AuthorizationQueryState contains any cached state for a query.
type AuthorizationQueryState struct {
role Role
public Role
err error
}
var _ sql.AuthorizationQueryState = AuthorizationQueryState{}
// Error implements the sql.AuthorizationQueryState interface.
func (state AuthorizationQueryState) Error() error {
return state.err
}
// AuthorizationQueryStateImpl implements the sql.AuthorizationQueryState interface.
func (state AuthorizationQueryState) AuthorizationQueryStateImpl() {}
// AuthorizationHandlerFactory is the factory for Doltgres.
type AuthorizationHandlerFactory struct{}
var _ sql.AuthorizationHandlerFactory = AuthorizationHandlerFactory{}
// CreateHandler implements the sql.AuthorizationHandlerFactory interface.
func (h AuthorizationHandlerFactory) CreateHandler(cat sql.Catalog) sql.AuthorizationHandler {
return &AuthorizationHandler{
cat: cat,
}
}
// AuthorizationHandler handles vitess.AuthInformation for Doltgres.
type AuthorizationHandler struct {
cat sql.Catalog
}
var _ sql.AuthorizationHandler = (*AuthorizationHandler)(nil)
// NewQueryState implements the sql.AuthorizationHandler interface.
func (h *AuthorizationHandler) NewQueryState(ctx *sql.Context) sql.AuthorizationQueryState {
state := AuthorizationQueryState{}
LockRead(func() {
state.role = GetRole(ctx.Client().User)
if !state.role.IsValid() {
state.err = errors.Errorf(`role "%s" does not exist`, state.role.Name)
return
}
state.public = GetRole("public")
if !state.public.IsValid() {
state.err = errors.Errorf(`role "%s" does not exist`, state.public.Name)
return
}
})
return state
}
// HandleAuth implements the sql.AuthorizationHandler interface.
func (h *AuthorizationHandler) HandleAuth(ctx *sql.Context, aqs sql.AuthorizationQueryState, auth vitess.AuthInformation) error {
// TODO: eventually we'll want all conversion paths to provide both the AuthType and TargetType, but this lets us iterate faster for now
if len(auth.AuthType) == 0 && len(auth.TargetType) == 0 {
return nil
}
if aqs == nil {
aqs = h.NewQueryState(ctx)
}
state := aqs.(AuthorizationQueryState)
if state.err != nil {
return state.err
}
globalLock.RLock()
defer globalLock.RUnlock()
checkSchemaForUsage := false
var privileges []Privilege
switch auth.AuthType {
case AuthType_IGNORE:
// This means that authorization is being handled elsewhere (such as a child or parent), and should be ignored here
return nil
case AuthType_CREATE:
privileges = []Privilege{Privilege_CREATE}
case AuthType_DELETE:
privileges = []Privilege{Privilege_DELETE}
case AuthType_DROPTABLE:
privileges = []Privilege{Privilege_DROP}
case AuthType_EXECUTE:
privileges = []Privilege{Privilege_EXECUTE}
case AuthType_INSERT:
privileges = []Privilege{Privilege_INSERT}
case AuthType_SELECT:
privileges = []Privilege{Privilege_SELECT}
case AuthType_TRUNCATE:
privileges = []Privilege{Privilege_TRUNCATE}
case AuthType_USAGE:
checkSchemaForUsage = true
privileges = []Privilege{Privilege_USAGE}
case AuthType_UPDATE:
privileges = []Privilege{Privilege_UPDATE}
default:
if len(auth.AuthType) == 0 {
return errors.New("AuthType is empty")
} else {
return errors.Errorf("AuthType not handled: `%s`", auth.AuthType)
}
}
// TODO: implement the rest of these
switch auth.TargetType {
case AuthTargetType_Ignore:
// This means that the AuthType did not need a TargetType, so we can safely ignore it
case AuthTargetType_DatabaseIdentifiers:
for _, database := range auth.TargetNames {
database = h.dbName(ctx, database)
roleDatabaseKey := DatabasePrivilegeKey{
Role: state.role.ID(),
Name: database,
}
publicDatabaseKey := DatabasePrivilegeKey{
Role: state.public.ID(),
Name: database,
}
for _, privilege := range privileges {
if !HasDatabasePrivilege(roleDatabaseKey, privilege) && !HasDatabasePrivilege(publicDatabaseKey, privilege) {
return errors.Errorf("permission denied for database %s", database)
}
}
}
case AuthTargetType_SchemaIdentifiers:
if len(auth.TargetNames)%2 != 0 {
return errors.Errorf("schema identifiers has an unsupported count: %d", len(auth.TargetNames))
}
for i := 0; i < len(auth.TargetNames); i += 2 {
// TODO: handle database
schemaName, err := core.GetSchemaName(ctx, nil, auth.TargetNames[i+1])
if err != nil {
// If this fails, then there's an issue with the search path.
// This will error later in the process, so we'll pass auth for now.
return nil
}
err = checkPrivilegeOnSchema(state, schemaName, privileges)
if err != nil {
return err
}
}
case AuthTargetType_TableIdentifiers:
if len(auth.TargetNames)%3 != 0 {
return errors.Errorf("table identifiers has an unsupported count: %d", len(auth.TargetNames))
}
for i := 0; i < len(auth.TargetNames); i += 3 {
// TODO: handle database
schemaName, err := core.GetSchemaName(ctx, nil, auth.TargetNames[i+1])
if err != nil {
// If this fails, then there's an issue with the search path.
// This will error later in the process, so we'll pass auth for now.
return nil
}
err = checkPrivilegeOnTable(state, schemaName, auth.TargetNames[i+2], privileges)
if err != nil {
return err
}
}
case AuthTargetType_FunctionIdentifiers:
if len(auth.TargetNames)%2 != 0 {
return errors.Errorf("function identifiers has an unsupported count: %d", len(auth.TargetNames))
}
for i := 0; i < len(auth.TargetNames); i += 2 {
err := checkPrivilegeOnRoutine(ctx, state, auth.TargetNames[i], auth.TargetNames[i+1], privileges)
if err != nil {
return err
}
}
case AuthTargetType_SequenceIdentifiers:
if len(auth.TargetNames)%2 != 0 {
return errors.Errorf("function identifiers has an unsupported count: %d", len(auth.TargetNames))
}
for i := 0; i < len(auth.TargetNames); i += 2 {
// TODO: handle database
schemaName, err := core.GetSchemaName(ctx, nil, auth.TargetNames[i])
if err != nil {
// If this fails, then there's an issue with the search path.
// This will error later in the process, so we'll pass auth for now.
return nil
}
err = checkPrivilegeOnSequence(state, schemaName, auth.TargetNames[i+1], privileges)
if err != nil {
if checkSchemaForUsage {
// there can be schema USAGE privilege for the user/role.
err = checkPrivilegeOnSchema(state, schemaName, privileges)
if err != nil {
return err
}
} else {
return err
}
}
}
case AuthTargetType_TODO:
// This is similar to IGNORE, except we're meant to replace this at some point
default:
if len(auth.TargetType) == 0 {
return errors.New("TargetType is unexpectedly empty")
} else {
return errors.Errorf("TargetType not handled: `%s`", auth.TargetType)
}
}
return nil
}
// HandleAuthNode implements the sql.AuthorizationHandler interface.
func (h *AuthorizationHandler) HandleAuthNode(ctx *sql.Context, aqs sql.AuthorizationQueryState, node sql.AuthorizationCheckerNode) error {
if aqs == nil {
aqs = h.NewQueryState(ctx)
}
state := aqs.(AuthorizationQueryState)
if state.err != nil {
return state.err
}
// TODO: implement this
return nil
}
// CheckDatabase implements the sql.AuthorizationHandler interface.
func (h *AuthorizationHandler) CheckDatabase(ctx *sql.Context, aqs sql.AuthorizationQueryState, dbName string) error {
if aqs == nil {
aqs = h.NewQueryState(ctx)
}
state := aqs.(AuthorizationQueryState)
if state.err != nil {
return state.err
}
// TODO: implement this
return nil
}
// CheckSchema implements the sql.AuthorizationHandler interface.
func (h *AuthorizationHandler) CheckSchema(ctx *sql.Context, aqs sql.AuthorizationQueryState, dbName string, schemaName string) error {
if aqs == nil {
aqs = h.NewQueryState(ctx)
}
state := aqs.(AuthorizationQueryState)
if state.err != nil {
return state.err
}
// TODO: implement this
return nil
}
// CheckTable implements the sql.AuthorizationHandler interface.
func (h *AuthorizationHandler) CheckTable(ctx *sql.Context, aqs sql.AuthorizationQueryState, dbName string, schemaName string, tableName string) error {
if aqs == nil {
aqs = h.NewQueryState(ctx)
}
state := aqs.(AuthorizationQueryState)
if state.err != nil {
return state.err
}
// TODO: implement this
return nil
}
// dbName uses the current database from the context if a database is not specified, otherwise it returns the given
// database name.
func (h *AuthorizationHandler) dbName(ctx *sql.Context, dbName string) string {
if len(dbName) == 0 {
dbName = ctx.GetCurrentDatabase()
}
// Revision databases take the form "dbname/revision", so we must split the revision from the database name
splitDbName := strings.SplitN(dbName, "/", 2)
return splitDbName[0]
}
// checkPrivilegeOnSchema checks privileges for given schema.
func checkPrivilegeOnSchema(state AuthorizationQueryState, schemaName string, privileges []Privilege) error {
roleSchemaKey := SchemaPrivilegeKey{
Role: state.role.ID(),
Schema: schemaName,
}
publicSchemaKey := SchemaPrivilegeKey{
Role: state.public.ID(),
Schema: schemaName,
}
for _, privilege := range privileges {
if !HasSchemaPrivilege(roleSchemaKey, privilege) && !HasSchemaPrivilege(publicSchemaKey, privilege) {
return errors.Errorf("permission denied for schema %s", schemaName)
}
}
return nil
}
// checkPrivilegeOnTable checks privileges for given table provided with schema name.
func checkPrivilegeOnTable(state AuthorizationQueryState, schemaName, tableName string, privileges []Privilege) error {
roleTableKey := TablePrivilegeKey{
Role: state.role.ID(),
Table: doltdb.TableName{Name: tableName, Schema: schemaName},
}
publicTableKey := TablePrivilegeKey{
Role: state.public.ID(),
Table: doltdb.TableName{Name: tableName, Schema: schemaName},
}
for _, privilege := range privileges {
if !HasTablePrivilege(roleTableKey, privilege) && !HasTablePrivilege(publicTableKey, privilege) {
return errors.Errorf("permission denied for table %s", tableName)
}
}
return nil
}
// checkPrivilegeOnSequence checks privileges for given sequence provided with schema name.
func checkPrivilegeOnSequence(state AuthorizationQueryState, schemaName, seqName string, privileges []Privilege) error {
roleSequenceKey := SequencePrivilegeKey{
Role: state.role.ID(),
Schema: schemaName,
Name: seqName,
}
publicSequenceKey := SequencePrivilegeKey{
Role: state.public.ID(),
Schema: schemaName,
Name: seqName,
}
for _, privilege := range privileges {
if !HasSequencePrivilege(roleSequenceKey, privilege) && !HasSequencePrivilege(publicSequenceKey, privilege) {
return errors.Errorf("permission denied for sequence %s", seqName)
}
}
return nil
}
// checkPrivilegeOnRoutine checks privileges for given function or procedure provided with schema name.
func checkPrivilegeOnRoutine(ctx *sql.Context, state AuthorizationQueryState, schemaName, routineName string, privileges []Privilege) error {
// TODO: handle database
schName, err := core.GetSchemaName(ctx, nil, schemaName)
if err != nil {
// If this fails, then there's an issue with the search path.
// This will error later in the process, so we'll pass auth for now.
return nil
}
roleRoutineKey := RoutinePrivilegeKey{
Role: state.role.ID(),
Schema: schName,
Name: routineName,
//ArgTypes: auth.Extra.(string),
}
publicRoutineKey := RoutinePrivilegeKey{
Role: state.public.ID(),
Schema: schName,
Name: routineName,
//ArgTypes: auth.Extra.(string),
}
for _, privilege := range privileges {
if !HasRoutinePrivilege(roleRoutineKey, privilege) && !HasRoutinePrivilege(publicRoutineKey, privilege) {
// check if it's system function
_, ok := framework.Catalog[strings.ToLower(routineName)]
if ok && schemaName == "" {
// TODO: for now we don't check privilege for pg_catalog tables as it's granted for PUBLIC by default
// need to fix it when we support 'REVOKE privileges FROM PUBLIC'
return nil
}
return errors.Errorf("permission denied for routine %s", routineName)
}
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
// 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
// These AuthType_ enums are used as the AuthType in vitess.AuthInformation.
const (
AuthType_IGNORE = "IGNORE"
AuthType_ALTER_SYSTEM = "ALTER_SYSTEM"
AuthType_CONNECT = "CONNECT"
AuthType_CREATE = "CREATE"
AuthType_DELETE = "DELETE"
AuthType_DROPTABLE = "DROPTABLE"
AuthType_EXECUTE = "EXECUTE"
AuthType_INSERT = "INSERT"
AuthType_REFERENCES = "REFERENCES"
AuthType_SELECT = "SELECT"
AuthType_SET = "SET"
AuthType_TEMPORARY = "TEMPORARY"
AuthType_TRIGGER = "TRIGGER"
AuthType_TRUNCATE = "TRUNCATE"
AuthType_UPDATE = "UPDATE"
AuthType_USAGE = "USAGE"
)
// These AuthTargetType_ enums are used as the TargetType in vitess.AuthInformation.
const (
AuthTargetType_Ignore = "IGNORE"
AuthTargetType_DatabaseIdentifiers = "DB_IDENTS"
AuthTargetType_SchemaIdentifiers = "DB_SCH_IDENTS"
AuthTargetType_TableIdentifiers = "DB_SCH_TABLE_IDENTS"
AuthTargetType_FunctionIdentifiers = "DB_SCH_FUNCTION_IDENTS"
AuthTargetType_SequenceIdentifiers = "DB_SCH_SEQUENCE_IDENTS"
AuthTargetType_TODO = "TODO"
)
+50
View File
@@ -0,0 +1,50 @@
// 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 "github.com/dolthub/doltgresql/utils"
// AuthContext contains the auth portion of the context when converting from the Postgres AST to the Vitess AST.
type AuthContext struct {
authType utils.Stack[string]
}
// NewAuthContext returns a new *AuthContext.
func NewAuthContext() *AuthContext {
return &AuthContext{}
}
// PushAuthType pushes the given AuthType into the context's stack.
func (ctx *AuthContext) PushAuthType(authType string) {
ctx.authType.Push(authType)
}
// PeekAuthType returns the AuthType that is on the top of the stack. This does not remove it from the stack. Returns
// AuthType_IGNORE if the stack is empty.
func (ctx *AuthContext) PeekAuthType() string {
if ctx.authType.Empty() {
return AuthType_IGNORE
}
return ctx.authType.Peek()
}
// PopAuthType returns the AuthType that is on the top of the stack. This also removes it from the stack. Returns
// AuthType_IGNORE if the stack is empty.
func (ctx *AuthContext) PopAuthType() string {
if ctx.authType.Empty() {
return AuthType_IGNORE
}
return ctx.authType.Pop()
}
+211
View File
@@ -0,0 +1,211 @@
// 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
}
+202
View File
@@ -0,0 +1,202 @@
// 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 (
"github.com/dolthub/doltgresql/utils"
)
// DatabasePrivileges contains the privileges given to a role on a database.
type DatabasePrivileges struct {
Data map[DatabasePrivilegeKey]DatabasePrivilegeValue
}
// DatabasePrivilegeKey points to a specific database object.
type DatabasePrivilegeKey struct {
Role RoleID
Name string
}
// DatabasePrivilegeValue is the value associated with the DatabasePrivilegeKey.
type DatabasePrivilegeValue struct {
Key DatabasePrivilegeKey
Privileges map[Privilege]map[GrantedPrivilege]bool
}
// NewDatabasePrivileges returns a new *DatabasePrivileges.
func NewDatabasePrivileges() *DatabasePrivileges {
return &DatabasePrivileges{make(map[DatabasePrivilegeKey]DatabasePrivilegeValue)}
}
// AddDatabasePrivilege adds the given database privilege to the global database.
func AddDatabasePrivilege(key DatabasePrivilegeKey, privilege GrantedPrivilege, withGrantOption bool) {
databasePrivilegeValue, ok := globalDatabase.databasePrivileges.Data[key]
if !ok {
databasePrivilegeValue = DatabasePrivilegeValue{
Key: key,
Privileges: make(map[Privilege]map[GrantedPrivilege]bool),
}
globalDatabase.databasePrivileges.Data[key] = databasePrivilegeValue
}
privilegeMap, ok := databasePrivilegeValue.Privileges[privilege.Privilege]
if !ok {
privilegeMap = make(map[GrantedPrivilege]bool)
databasePrivilegeValue.Privileges[privilege.Privilege] = privilegeMap
}
privilegeMap[privilege] = withGrantOption
}
// HasDatabasePrivilege checks whether the user has the given privilege on the associated database.
func HasDatabasePrivilege(key DatabasePrivilegeKey, privilege Privilege) bool {
if IsSuperUser(key.Role) {
return true
}
if databasePrivilegeValue, ok := globalDatabase.databasePrivileges.Data[key]; ok {
if privilegeMap, ok := databasePrivilegeValue.Privileges[privilege]; ok && len(privilegeMap) > 0 {
return true
}
}
for _, group := range GetAllGroupsWithMember(key.Role, true) {
if HasDatabasePrivilege(DatabasePrivilegeKey{
Role: group,
Name: key.Name,
}, privilege) {
return true
}
}
return false
}
// HasDatabasePrivilegeGrantOption checks whether the user has WITH GRANT OPTION for the given privilege on the associated
// database. Returns the role that has WITH GRANT OPTION, or an invalid role if WITH GRANT OPTION is not available.
func HasDatabasePrivilegeGrantOption(key DatabasePrivilegeKey, privilege Privilege) RoleID {
if IsSuperUser(key.Role) {
return key.Role
}
if databasePrivilegeValue, ok := globalDatabase.databasePrivileges.Data[key]; ok {
if privilegeMap, ok := databasePrivilegeValue.Privileges[privilege]; ok {
for _, withGrantOption := range privilegeMap {
if withGrantOption {
return key.Role
}
}
}
}
for _, group := range GetAllGroupsWithMember(key.Role, true) {
if returnedID := HasDatabasePrivilegeGrantOption(DatabasePrivilegeKey{
Role: group,
Name: key.Name,
}, privilege); returnedID.IsValid() {
return returnedID
}
}
return 0
}
// RemoveDatabasePrivilege removes the privilege from the global database. If `grantOptionOnly` is true, then only the WITH
// GRANT OPTION portion is revoked. If `grantOptionOnly` is false, then the full privilege is removed. If the GrantedBy
// field contains a valid RoleID, then only the privilege associated with that granter is removed. Otherwise, the
// privilege is completely removed for the grantee.
func RemoveDatabasePrivilege(key DatabasePrivilegeKey, privilege GrantedPrivilege, grantOptionOnly bool) {
if databasePrivilegeValue, ok := globalDatabase.databasePrivileges.Data[key]; ok {
if privilegeMap, ok := databasePrivilegeValue.Privileges[privilege.Privilege]; ok {
if grantOptionOnly {
// This is provided when we only want to revoke the WITH GRANT OPTION, and not the privilege itself.
// If a role is provided in GRANTED BY, then we specifically delete the option associated with that role.
// If no role was given, then we'll remove WITH GRANT OPTION from all of the associated roles.
if privilege.GrantedBy.IsValid() {
if _, ok = privilegeMap[privilege]; ok {
privilegeMap[privilege] = false
}
} else {
for privilegeMapKey := range privilegeMap {
privilegeMap[privilegeMapKey] = false
}
}
} else {
// If a role is provided in GRANTED BY, then we specifically delete the privilege associated with that role.
// If no role was given, then we'll delete the privileges granted by all roles.
if privilege.GrantedBy.IsValid() {
delete(privilegeMap, privilege)
} else {
privilegeMap = nil
}
if len(privilegeMap) == 0 {
delete(databasePrivilegeValue.Privileges, privilege.Privilege)
}
}
}
if len(databasePrivilegeValue.Privileges) == 0 {
delete(globalDatabase.databasePrivileges.Data, key)
}
}
}
// serialize writes the DatabasePrivileges to the given writer.
func (sp *DatabasePrivileges) serialize(writer *utils.Writer) {
// Version 0
// Write the total number of values
writer.Uint64(uint64(len(sp.Data)))
for _, value := range sp.Data {
// Write the key
writer.Uint64(uint64(value.Key.Role))
writer.String(value.Key.Name)
// Write the total number of privileges
writer.Uint64(uint64(len(value.Privileges)))
for privilege, privilegeMap := range value.Privileges {
writer.String(string(privilege))
// Write the number of granted privileges
writer.Uint32(uint32(len(privilegeMap)))
for grantedPrivilege, withGrantOption := range privilegeMap {
writer.Uint64(uint64(grantedPrivilege.GrantedBy))
writer.Bool(withGrantOption)
}
}
}
}
// deserialize reads the DatabasePrivileges from the given reader.
func (sp *DatabasePrivileges) deserialize(version uint32, reader *utils.Reader) {
sp.Data = make(map[DatabasePrivilegeKey]DatabasePrivilegeValue)
switch version {
case 0, 1:
// Read the total number of values
dataCount := reader.Uint64()
for dataIdx := uint64(0); dataIdx < dataCount; dataIdx++ {
// Read the key
spv := DatabasePrivilegeValue{Privileges: make(map[Privilege]map[GrantedPrivilege]bool)}
spv.Key.Role = RoleID(reader.Uint64())
spv.Key.Name = reader.String()
// Read the total number of privileges
privilegeCount := reader.Uint64()
for privilegeIdx := uint64(0); privilegeIdx < privilegeCount; privilegeIdx++ {
privilege := Privilege(reader.String())
// Read the number of granted privileges
grantedCount := reader.Uint32()
grantedMap := make(map[GrantedPrivilege]bool)
for grantedIdx := uint32(0); grantedIdx < grantedCount; grantedIdx++ {
grantedPrivilege := GrantedPrivilege{}
grantedPrivilege.Privilege = privilege
grantedPrivilege.GrantedBy = RoleID(reader.Uint64())
grantedMap[grantedPrivilege] = reader.Bool()
}
spv.Privileges[privilege] = grantedMap
}
sp.Data[spv.Key] = spv
}
default:
panic("unexpected version in DatabasePrivileges")
}
}
+206
View File
@@ -0,0 +1,206 @@
// Copyright 2026 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 "github.com/dolthub/go-mysql-server/sql"
// PrivilegeSetLayer is used to allow some functions that inspect the GMS privilege set (such as branch control) to
// interface with Doltgres' auth system.
type PrivilegeSetLayer struct {
Role RoleID
}
var _ sql.PrivilegeSet = (*PrivilegeSetLayer)(nil)
// NewPrivilegeSetLayer creates a new PrivilegeSetLayer for the user in the given context's session.
func NewPrivilegeSetLayer(ctx *sql.Context) *PrivilegeSetLayer {
return &PrivilegeSetLayer{
Role: GetRole(ctx.Client().User).id,
}
}
// Has implements the interface sql.PrivilegeSet.
func (privSet *PrivilegeSetLayer) Has(privileges ...sql.PrivilegeType) bool {
return IsSuperUser(privSet.Role)
}
// HasPrivileges implements the interface sql.PrivilegeSet.
func (privSet *PrivilegeSetLayer) HasPrivileges() bool {
return IsSuperUser(privSet.Role)
}
// Count implements the interface sql.PrivilegeSet.
func (privSet *PrivilegeSetLayer) Count() int {
if IsSuperUser(privSet.Role) {
return 31 // The current number in GMS
}
return 0
}
// Database implements the interface sql.PrivilegeSet.
func (privSet *PrivilegeSetLayer) Database(dbName string) sql.PrivilegeSetDatabase {
return &PrivilegeSetLayerDatabase{
Db: dbName,
Role: privSet.Role,
}
}
// GetDatabases implements the interface sql.PrivilegeSet.
func (privSet *PrivilegeSetLayer) GetDatabases() []sql.PrivilegeSetDatabase {
return nil
}
// Equals implements the interface sql.PrivilegeSet.
func (privSet *PrivilegeSetLayer) Equals(otherPs sql.PrivilegeSet) bool {
if other, ok := otherPs.(*PrivilegeSetLayer); ok {
return privSet.Role == other.Role
}
return false
}
// ToSlice implements the interface sql.PrivilegeSet.
func (privSet *PrivilegeSetLayer) ToSlice() []sql.PrivilegeType {
if IsSuperUser(privSet.Role) {
return []sql.PrivilegeType{sql.PrivilegeType_Select,
sql.PrivilegeType_Insert,
sql.PrivilegeType_Update,
sql.PrivilegeType_Delete,
sql.PrivilegeType_Create,
sql.PrivilegeType_Drop,
sql.PrivilegeType_Reload,
sql.PrivilegeType_Shutdown,
sql.PrivilegeType_Process,
sql.PrivilegeType_File,
sql.PrivilegeType_GrantOption,
sql.PrivilegeType_References,
sql.PrivilegeType_Index,
sql.PrivilegeType_Alter,
sql.PrivilegeType_ShowDB,
sql.PrivilegeType_Super,
sql.PrivilegeType_CreateTempTable,
sql.PrivilegeType_LockTables,
sql.PrivilegeType_Execute,
sql.PrivilegeType_ReplicationSlave,
sql.PrivilegeType_ReplicationClient,
sql.PrivilegeType_CreateView,
sql.PrivilegeType_ShowView,
sql.PrivilegeType_CreateRoutine,
sql.PrivilegeType_AlterRoutine,
sql.PrivilegeType_CreateUser,
sql.PrivilegeType_Event,
sql.PrivilegeType_Trigger,
sql.PrivilegeType_CreateTablespace,
sql.PrivilegeType_CreateRole,
sql.PrivilegeType_DropRole}
}
return nil
}
// PrivilegeSetLayerDatabase is the database portion of PrivilegeSetLayer.
type PrivilegeSetLayerDatabase struct {
Db string
Role RoleID
}
var _ sql.PrivilegeSetDatabase = (*PrivilegeSetLayerDatabase)(nil)
// Name implements the interface sql.PrivilegeSetDatabase.
func (privSet *PrivilegeSetLayerDatabase) Name() string {
return privSet.Db
}
// Has implements the interface sql.PrivilegeSetDatabase.
func (privSet *PrivilegeSetLayerDatabase) Has(privileges ...sql.PrivilegeType) bool {
return IsSuperUser(privSet.Role)
}
// HasPrivileges implements the interface sql.PrivilegeSetDatabase.
func (privSet *PrivilegeSetLayerDatabase) HasPrivileges() bool {
return IsSuperUser(privSet.Role)
}
// Count implements the interface sql.PrivilegeSetDatabase.
func (privSet *PrivilegeSetLayerDatabase) Count() int {
if IsSuperUser(privSet.Role) {
return 31 // The current number in GMS
}
return 0
}
// Table implements the interface sql.PrivilegeSetDatabase.
func (privSet *PrivilegeSetLayerDatabase) Table(tblName string) sql.PrivilegeSetTable {
panic("Table is not yet implemented for the Doltgres privilege layer")
}
// GetTables implements the interface sql.PrivilegeSetDatabase.
func (privSet *PrivilegeSetLayerDatabase) GetTables() []sql.PrivilegeSetTable {
return nil
}
// Routine implements the interface sql.PrivilegeSetDatabase.
func (privSet *PrivilegeSetLayerDatabase) Routine(routineName string, isProcedure bool) sql.PrivilegeSetRoutine {
panic("Routine is not yet implemented for the Doltgres privilege layer")
}
// GetRoutines implements the interface sql.PrivilegeSetDatabase.
func (privSet *PrivilegeSetLayerDatabase) GetRoutines() []sql.PrivilegeSetRoutine {
return nil
}
// Equals implements the interface sql.PrivilegeSetDatabase.
func (privSet *PrivilegeSetLayerDatabase) Equals(otherPs sql.PrivilegeSetDatabase) bool {
if other, ok := otherPs.(*PrivilegeSetLayerDatabase); ok {
return privSet.Role == other.Role && privSet.Db == other.Db
}
return false
}
// ToSlice implements the interface sql.PrivilegeSetDatabase.
func (privSet *PrivilegeSetLayerDatabase) ToSlice() []sql.PrivilegeType {
if IsSuperUser(privSet.Role) {
return []sql.PrivilegeType{sql.PrivilegeType_Select,
sql.PrivilegeType_Insert,
sql.PrivilegeType_Update,
sql.PrivilegeType_Delete,
sql.PrivilegeType_Create,
sql.PrivilegeType_Drop,
sql.PrivilegeType_Reload,
sql.PrivilegeType_Shutdown,
sql.PrivilegeType_Process,
sql.PrivilegeType_File,
sql.PrivilegeType_GrantOption,
sql.PrivilegeType_References,
sql.PrivilegeType_Index,
sql.PrivilegeType_Alter,
sql.PrivilegeType_ShowDB,
sql.PrivilegeType_Super,
sql.PrivilegeType_CreateTempTable,
sql.PrivilegeType_LockTables,
sql.PrivilegeType_Execute,
sql.PrivilegeType_ReplicationSlave,
sql.PrivilegeType_ReplicationClient,
sql.PrivilegeType_CreateView,
sql.PrivilegeType_ShowView,
sql.PrivilegeType_CreateRoutine,
sql.PrivilegeType_AlterRoutine,
sql.PrivilegeType_CreateUser,
sql.PrivilegeType_Event,
sql.PrivilegeType_Trigger,
sql.PrivilegeType_CreateTablespace,
sql.PrivilegeType_CreateRole,
sql.PrivilegeType_DropRole}
}
return nil
}
+56
View File
@@ -0,0 +1,56 @@
// 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"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/go-mysql-server/sql"
)
// doltgresPasswordEnvVar is the name of the environment variable that can be used to set the password for the
// default user.
const doltgresPasswordEnvVar = "DOLTGRES_PASSWORD"
// doltgresUserEnvVar is the name of the environment variable that can be used to set the password for the
// default user.
const doltgresUserEnvVar = "DOLTGRES_USER"
// Config is an interface that exists as pulling the actual config package would cause a cyclical dependency.
type Config interface {
AuthFilePath() string
}
// Init handles all initialization needs in this package.
func Init(dEnv *env.DoltEnv, cfg Config) {
dbInit(dEnv, cfg)
sql.SetAuthorizationHandlerFactory(AuthorizationHandlerFactory{})
}
// GetSuperUserAndPassword returns the superuser and password for the server to use, as defined in the environment
func GetSuperUserAndPassword() (string, string) {
user := "postgres"
if envUser := os.Getenv(doltgresUserEnvVar); envUser != "" {
user = envUser
}
password := "password"
if envPassword := os.Getenv(doltgresPasswordEnvVar); envPassword != "" {
password = envPassword
}
return user, password
}
+54
View File
@@ -0,0 +1,54 @@
// 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 (
"fmt"
"github.com/dolthub/doltgresql/server/auth/rfc5802"
)
// ScramSha256Password is the struct form of an encrypted password.
type ScramSha256Password struct {
Iterations uint32
Salt rfc5802.OctetString
StoredKey rfc5802.OctetString
ServerKey rfc5802.OctetString
}
// NewScramSha256Password creates a ScramSha256Password with a randomly-generated salt.
func NewScramSha256Password(rawPassword string) (*ScramSha256Password, error) {
// This is unlikely to change, but it's defined here since we don't want to reference it elsewhere accidentally
const iterations = 4096
salt := GenerateRandomOctetString(16)
saltedPassword, err := rfc5802.SaltedPassword(rawPassword, salt, iterations)
if err != nil {
return nil, err
}
storedKey := rfc5802.StoredKey(rfc5802.ClientKey(saltedPassword))
serverKey := rfc5802.ServerKey(saltedPassword)
return &ScramSha256Password{
Iterations: iterations,
Salt: salt,
StoredKey: storedKey,
ServerKey: serverKey,
}, nil
}
// AsPasswordString returns the password as defined in https://www.postgresql.org/docs/15/catalog-pg-authid.html
func (password ScramSha256Password) AsPasswordString() string {
return fmt.Sprintf(`SCRAM-SHA-256$%d:%s$%s:%s`,
password.Iterations, password.Salt.ToBase64(), password.StoredKey.ToBase64(), password.ServerKey.ToBase64())
}
+267
View File
@@ -0,0 +1,267 @@
// 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
// Privilege represents some permission for a database object.
// https://www.postgresql.org/docs/15/ddl-priv.html
type Privilege string
const (
Privilege_SELECT = "r"
Privilege_INSERT = "a"
Privilege_UPDATE = "w"
Privilege_DELETE = "d"
Privilege_TRUNCATE = "D"
Privilege_REFERENCES = "x"
Privilege_TRIGGER = "t"
Privilege_CREATE = "C"
Privilege_CONNECT = "c"
Privilege_TEMPORARY = "T"
Privilege_EXECUTE = "X"
Privilege_USAGE = "U"
Privilege_SET = "s"
Privilege_ALTER_SYSTEM = "A"
Privilege_DROP = "Y"
)
// PrivilegeObject is the database object that privileges are applied to.
// https://www.postgresql.org/docs/15/ddl-priv.html
type PrivilegeObject byte
const (
PrivilegeObject_DATABASE PrivilegeObject = iota
PrivilegeObject_DOMAIN
PrivilegeObject_FUNCTION // Also applies to procedures and routines
PrivilegeObject_FOREIGN_DATA_WRAPPER
PrivilegeObject_FOREIGN_SERVER
PrivilegeObject_LANGUAGE
PrivilegeObject_LARGE_OBJECT
PrivilegeObject_PARAMETER
PrivilegeObject_SCHEMA
PrivilegeObject_SEQUENCE
PrivilegeObject_TABLE
PrivilegeObject_TABLE_COLUMN
PrivilegeObject_TABLESPACE
PrivilegeObject_TYPE
)
// GrantedPrivilege specifies details.
type GrantedPrivilege struct {
Privilege
GrantedBy RoleID
}
// GetAllPrivileges returns every Privilege.
func GetAllPrivileges() []Privilege {
return []Privilege{
Privilege_SELECT,
Privilege_INSERT,
Privilege_UPDATE,
Privilege_DELETE,
Privilege_TRUNCATE,
Privilege_REFERENCES,
Privilege_TRIGGER,
Privilege_CREATE,
Privilege_CONNECT,
Privilege_TEMPORARY,
Privilege_EXECUTE,
Privilege_USAGE,
Privilege_SET,
Privilege_ALTER_SYSTEM,
Privilege_DROP,
}
}
// GetAllPrivilegeObjects returns every PrivilegeObject.
func GetAllPrivilegeObjects() []PrivilegeObject {
return []PrivilegeObject{
PrivilegeObject_DATABASE,
PrivilegeObject_DOMAIN,
PrivilegeObject_FUNCTION,
PrivilegeObject_FOREIGN_DATA_WRAPPER,
PrivilegeObject_FOREIGN_SERVER,
PrivilegeObject_LANGUAGE,
PrivilegeObject_LARGE_OBJECT,
PrivilegeObject_PARAMETER,
PrivilegeObject_SCHEMA,
PrivilegeObject_SEQUENCE,
PrivilegeObject_TABLE,
PrivilegeObject_TABLE_COLUMN,
PrivilegeObject_TABLESPACE,
PrivilegeObject_TYPE,
}
}
// ACLAbbreviation returns the name of the privilege using the Access Control List abbreviation.
func (p Privilege) ACLAbbreviation() string {
return string(p)
}
// String returns the name of the privilege (uppercased).
func (p Privilege) String() string {
switch p {
case Privilege_SELECT:
return "SELECT"
case Privilege_INSERT:
return "INSERT"
case Privilege_UPDATE:
return "UPDATE"
case Privilege_DELETE:
return "DELETE"
case Privilege_TRUNCATE:
return "TRUNCATE"
case Privilege_REFERENCES:
return "REFERENCES"
case Privilege_TRIGGER:
return "TRIGGER"
case Privilege_CREATE:
return "CREATE"
case Privilege_CONNECT:
return "CONNECT"
case Privilege_TEMPORARY:
return "TEMPORARY"
case Privilege_EXECUTE:
return "EXECUTE"
case Privilege_USAGE:
return "USAGE"
case Privilege_SET:
return "SET"
case Privilege_ALTER_SYSTEM:
return "ALTER SYSTEM"
case Privilege_DROP:
return "DROP"
default:
return "UNKNOWN"
}
}
// AllPrivileges returns all valid privileges that may be applied to this object.
func (po PrivilegeObject) AllPrivileges() []Privilege {
switch po {
case PrivilegeObject_DATABASE:
return []Privilege{Privilege_CREATE, Privilege_TEMPORARY, Privilege_CONNECT, Privilege_DROP}
case PrivilegeObject_DOMAIN:
return []Privilege{Privilege_USAGE, Privilege_DROP}
case PrivilegeObject_FUNCTION:
return []Privilege{Privilege_EXECUTE, Privilege_DROP}
case PrivilegeObject_FOREIGN_DATA_WRAPPER:
return []Privilege{Privilege_USAGE, Privilege_DROP}
case PrivilegeObject_FOREIGN_SERVER:
return []Privilege{Privilege_USAGE, Privilege_DROP}
case PrivilegeObject_LANGUAGE:
return []Privilege{Privilege_USAGE, Privilege_DROP}
case PrivilegeObject_LARGE_OBJECT:
return []Privilege{Privilege_SELECT, Privilege_UPDATE, Privilege_DROP}
case PrivilegeObject_PARAMETER:
return []Privilege{Privilege_SET, Privilege_ALTER_SYSTEM, Privilege_DROP}
case PrivilegeObject_SCHEMA:
return []Privilege{Privilege_USAGE, Privilege_CREATE, Privilege_DROP}
case PrivilegeObject_SEQUENCE:
return []Privilege{Privilege_SELECT, Privilege_UPDATE, Privilege_USAGE, Privilege_DROP}
case PrivilegeObject_TABLE:
return []Privilege{Privilege_INSERT, Privilege_SELECT, Privilege_UPDATE, Privilege_DELETE, Privilege_TRUNCATE, Privilege_REFERENCES, Privilege_TRIGGER, Privilege_DROP}
case PrivilegeObject_TABLE_COLUMN:
return []Privilege{Privilege_INSERT, Privilege_SELECT, Privilege_UPDATE, Privilege_REFERENCES, Privilege_DROP}
case PrivilegeObject_TABLESPACE:
return []Privilege{Privilege_CREATE, Privilege_DROP}
case PrivilegeObject_TYPE:
return []Privilege{Privilege_USAGE, Privilege_DROP}
default:
panic("unknown privilege object")
}
}
// DefaultPublicPrivileges return the default PUBLIC privileges for this object.
func (po PrivilegeObject) DefaultPublicPrivileges() []Privilege {
switch po {
case PrivilegeObject_DATABASE:
return []Privilege{Privilege_TEMPORARY, Privilege_CONNECT}
case PrivilegeObject_DOMAIN:
return []Privilege{Privilege_USAGE}
case PrivilegeObject_FUNCTION:
return []Privilege{Privilege_EXECUTE}
case PrivilegeObject_FOREIGN_DATA_WRAPPER:
return nil
case PrivilegeObject_FOREIGN_SERVER:
return nil
case PrivilegeObject_LANGUAGE:
return []Privilege{Privilege_USAGE}
case PrivilegeObject_LARGE_OBJECT:
return nil
case PrivilegeObject_PARAMETER:
return nil
case PrivilegeObject_SCHEMA:
return nil
case PrivilegeObject_SEQUENCE:
return nil
case PrivilegeObject_TABLE:
return nil
case PrivilegeObject_TABLE_COLUMN:
return nil
case PrivilegeObject_TABLESPACE:
return nil
case PrivilegeObject_TYPE:
return []Privilege{Privilege_USAGE}
default:
panic("unknown privilege object")
}
}
// IsValid returns whether the given Privilege is valid for the PrivilegeObject, as not all privileges are valid for all
// objects.
func (po PrivilegeObject) IsValid(privilege Privilege) bool {
for _, poPriv := range po.AllPrivileges() {
if privilege == poPriv {
return true
}
}
return false
}
// String returns the name of the privilege (uppercased).
func (po PrivilegeObject) String() string {
switch po {
case PrivilegeObject_DATABASE:
return "DATABASE"
case PrivilegeObject_DOMAIN:
return "DOMAIN"
case PrivilegeObject_FUNCTION:
return "FUNCTION"
case PrivilegeObject_FOREIGN_DATA_WRAPPER:
return "FOREIGN DATA WRAPPER"
case PrivilegeObject_FOREIGN_SERVER:
return "FOREIGN SERVER"
case PrivilegeObject_LANGUAGE:
return "LANGUAGE"
case PrivilegeObject_LARGE_OBJECT:
return "LARGE_OBJECT"
case PrivilegeObject_PARAMETER:
return "PARAMETER"
case PrivilegeObject_SCHEMA:
return "SCHEMA"
case PrivilegeObject_SEQUENCE:
return "SEQUENCE"
case PrivilegeObject_TABLE:
return "TABLE"
case PrivilegeObject_TABLE_COLUMN:
return "TABLE COLUMN"
case PrivilegeObject_TABLESPACE:
return "TABLESPACE"
case PrivilegeObject_TYPE:
return "TYPE"
default:
return "UNKNOWN"
}
}
+38
View File
@@ -0,0 +1,38 @@
// 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 (
crptorand "crypto/rand"
mathrand "math/rand"
"github.com/dolthub/doltgresql/server/auth/rfc5802"
)
// GenerateRandomOctetString generates an OctetString filled with random bytes.
func GenerateRandomOctetString(length int) rfc5802.OctetString {
if length <= 0 {
return nil
}
str := make(rfc5802.OctetString, length)
_, err := crptorand.Read(str)
if err != nil {
// If crypto/rand errors for some reason, we'll use the non-cryptographic version since it will never error.
// Considering this is used for salts, etc. it is fine to use the non-cryptographic version. We still prefer
// the other though, this is just backup.
_, _ = mathrand.Read(str) //lint:ignore SA1019 Intentional usage
}
return str
}
+19
View File
@@ -0,0 +1,19 @@
// 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 rfc5802
// This package is meant to implement the functions defined in RFC 5802. As such, function names will be non-standard
// compared to most other names in the project.
// https://datatracker.ietf.org/doc/html/rfc5802
+147
View File
@@ -0,0 +1,147 @@
// 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 rfc5802
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"github.com/xdg-go/stringprep"
"golang.org/x/crypto/pbkdf2"
)
var (
clientKeyConstant = OctetString("Client Key")
serverKeyConstant = OctetString("Server Key")
)
// OctetString is equivalent to a byte slice. An octet, as defined in the RFC, is an 8-bit byte. Go only supports 8-bit
// bytes. Additionally, an octet string is defined as a sequence of octets, which we can represent as a slice.
type OctetString []byte
// Base64ToOctetString returns the original octet string from its base64 encoded form.
func Base64ToOctetString(base64String string) OctetString {
decoded, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
// If we've encountered an error, then we'll return the equivalent of an empty hash. This will fail in a later step.
return make(OctetString, 32)
}
return decoded
}
// ClientKey returns the client key created using the salted password and a specific constant.
func ClientKey(saltedPassword OctetString) OctetString {
return HMAC(saltedPassword, clientKeyConstant)
}
// ClientProof returns the client proof by xor'ing the client key and client signature.
func ClientProof(clientKey OctetString, clientSignature OctetString) OctetString {
if len(clientKey) != len(clientSignature) {
return make(OctetString, 32)
}
return clientKey.Xor(clientSignature)
}
// ClientSignature returns the client signature using the given stored key and auth message.
func ClientSignature(storedKey OctetString, authMessage string) OctetString {
return HMAC(storedKey, OctetString(authMessage))
}
// H performs the SHA256 hash function, which is the hash function used by Postgres. The returned OctetString will
// always have a length of 32.
func H(str OctetString) OctetString {
ret := sha256.Sum256(str)
return ret[:]
}
// Hi is, essentially, PBKDF2 with HMAC as the pseudorandom function.
func Hi(str OctetString, salt OctetString, i uint32) OctetString {
return pbkdf2.Key(str, salt, int(i), 32, sha256.New)
}
// HMAC applies the HMAC keyed hash algorithm on the given octet strings.
func HMAC(key OctetString, str OctetString) OctetString {
mac := hmac.New(sha256.New, key)
mac.Write(str)
return mac.Sum(nil)
}
// Normalize runs the SASLprep profile (https://datatracker.ietf.org/doc/html/rfc4013) of the stringprep algorithm
// (https://datatracker.ietf.org/doc/html/rfc3454). This accepts a standard UTF8 encoded string, unlike other functions
// which may take an OctetString.
func Normalize(str string) (string, error) {
return stringprep.SASLprep.Prepare(str)
}
// SaltedPassword returns the salted password. The password should not have been normalized, as it is normalized within
// the function.
func SaltedPassword(password string, salt OctetString, i uint32) (OctetString, error) {
normalizedPassword, err := Normalize(password)
if err != nil {
return nil, err
}
return Hi(OctetString(normalizedPassword), salt, i), nil
}
// ServerKey returns the server key created using the salted password and a specific constant.
func ServerKey(saltedPassword OctetString) OctetString {
return HMAC(saltedPassword, serverKeyConstant)
}
// ServerSignature returns the server signature using the given server key and auth message.
func ServerSignature(serverKey OctetString, authMessage string) OctetString {
return HMAC(serverKey, OctetString(authMessage))
}
// StoredKey returns the stored key created using the client key.
func StoredKey(clientKey OctetString) OctetString {
return H(clientKey)
}
// AppendInteger appends the given integer.
func (os OctetString) AppendInteger(val uint32) OctetString {
result := make(OctetString, len(os)+4)
binary.BigEndian.PutUint32(result[len(os):], val)
return result
}
// Equals returns whether the calling octet string is equal to the given octet string.
func (os OctetString) Equals(other OctetString) bool {
return bytes.Equal(os, other)
}
// ToBase64 returns the OctetString as a base64 encoded UTF8 string.
func (os OctetString) ToBase64() string {
return base64.StdEncoding.EncodeToString(os)
}
// ToHex returns the OctetString as a hex encoded UTF8 string (lowercase).
func (os OctetString) ToHex() string {
return hex.EncodeToString(os)
}
// Xor applies "exclusive or" for every octet between both strings. Assumes that both strings have the same length, so
// perform any checks before calling this function.
func (os OctetString) Xor(other OctetString) OctetString {
result := make(OctetString, len(os))
for i := range os {
result[i] = os[i] ^ other[i]
}
return result
}
+145
View File
@@ -0,0 +1,145 @@
// 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 (
"time"
"github.com/dolthub/doltgresql/utils"
)
// Role represents a role/user.
type Role struct {
Name string // rolname
IsSuperUser bool // rolsuper
InheritPrivileges bool // rolinherit
CanCreateRoles bool // rolcreaterole
CanCreateDB bool // rolcreatedb
CanLogin bool // rolcanlogin
IsReplicationRole bool // rolreplication
CanBypassRowLevelSecurity bool // rolbypassrls
ConnectionLimit int32 // rolconnlimit
Password *ScramSha256Password // rolpassword
ValidUntil *time.Time // rolvaliduntil
id RoleID
}
// RoleID represents a Role's ID. IDs are assigned during load and will be stable throughout the server's current
// process. IDs are useful for referencing a specific role without using their name, since names can change. This is
// basically a special OID specific to roles. Eventually, we'll have a proper OID system, but this is a placeholder for
// now.
// TODO: need to replace with id.InternalUser
type RoleID uint64
// CreateDefaultRole creates the given role object with all default values set.
func CreateDefaultRole(name string) Role {
r := createDefaultRoleWithoutID(name)
r.id = RoleID(userIDCounter.Add(1))
return r
}
// createDefaultRoleWithoutID creates a default role, but does not assign an ID.
func createDefaultRoleWithoutID(name string) Role {
return Role{
Name: name,
IsSuperUser: false,
InheritPrivileges: true,
CanCreateRoles: false,
CanCreateDB: false,
CanLogin: false,
IsReplicationRole: false,
CanBypassRowLevelSecurity: false,
ConnectionLimit: -1,
Password: nil,
ValidUntil: nil,
id: RoleID(0),
}
}
// ID returns this Role's ID value.
func (r *Role) ID() RoleID {
return r.id
}
// IsValid returns true when the role has a valid ID.
func (r *Role) IsValid() bool {
return r.id.IsValid()
}
// IsValid returns true when the RoleID has a valid value. It does not indicate that the RoleID is attached to a role
// that actually exists.
func (id RoleID) IsValid() bool {
return id != RoleID(0)
}
// serialize writes the Role to the given writer.
func (r *Role) serialize(writer *utils.Writer) {
// Version 0
writer.String(r.Name)
writer.Bool(r.IsSuperUser)
writer.Bool(r.InheritPrivileges)
writer.Bool(r.CanCreateRoles)
writer.Bool(r.CanCreateDB)
writer.Bool(r.CanLogin)
writer.Bool(r.IsReplicationRole)
writer.Bool(r.CanBypassRowLevelSecurity)
writer.Int32(r.ConnectionLimit)
if r.Password != nil {
writer.Bool(true)
writer.Uint32(r.Password.Iterations)
writer.ByteSlice(r.Password.Salt)
writer.ByteSlice(r.Password.StoredKey)
writer.ByteSlice(r.Password.ServerKey)
} else {
writer.Bool(false)
}
if r.ValidUntil != nil {
writer.Bool(true)
writer.Int64(r.ValidUntil.UnixMicro())
} else {
writer.Bool(false)
}
writer.Uint64(uint64(r.id))
}
// deserialize reads the Role from the given reader.
func (r *Role) deserialize(version uint32, reader *utils.Reader) {
switch version {
case 0, 1:
r.Name = reader.String()
r.IsSuperUser = reader.Bool()
r.InheritPrivileges = reader.Bool()
r.CanCreateRoles = reader.Bool()
r.CanCreateDB = reader.Bool()
r.CanLogin = reader.Bool()
r.IsReplicationRole = reader.Bool()
r.CanBypassRowLevelSecurity = reader.Bool()
r.ConnectionLimit = reader.Int32()
if reader.Bool() {
r.Password = &ScramSha256Password{}
r.Password.Iterations = reader.Uint32()
r.Password.Salt = reader.ByteSlice()
r.Password.StoredKey = reader.ByteSlice()
r.Password.ServerKey = reader.ByteSlice()
}
if reader.Bool() {
t := time.UnixMicro(reader.Int64())
r.ValidUntil = &t
}
r.id = RoleID(reader.Uint64())
default:
panic("unexpected version in Role")
}
}
+167
View File
@@ -0,0 +1,167 @@
// 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 "github.com/dolthub/doltgresql/utils"
// RoleMembership contains all roles that have been granted to other roles.
type RoleMembership struct {
Data map[RoleID]map[RoleID]RoleMembershipValue
}
// RoleMembershipValue contains specific membership information between two roles.
type RoleMembershipValue struct {
Member RoleID
Group RoleID
WithAdminOption bool
GrantedBy RoleID
}
// NewRoleMembership returns a new *RoleMembership.
func NewRoleMembership() *RoleMembership {
return &RoleMembership{
Data: make(map[RoleID]map[RoleID]RoleMembershipValue),
}
}
// AddMemberToGroup adds the member role to the group role.
func AddMemberToGroup(member RoleID, group RoleID, withAdminOption bool, grantedBy RoleID) {
// We'll perform a sanity check for circular membership. This should be done before this call is made, but since we
// make assumptions that circular relationships are forbidden (which could lead to infinite loops otherwise), we
// enforce it here too.
if groupID, _, _ := IsRoleAMember(group, member); (groupID.IsValid() || member == group) && !globalDatabase.rolesByID[group].IsSuperUser {
panic("missing validation to prevent circular role relationships")
}
groupMap, ok := globalDatabase.roleMembership.Data[member]
if !ok {
groupMap = make(map[RoleID]RoleMembershipValue)
globalDatabase.roleMembership.Data[member] = groupMap
}
groupMap[group] = RoleMembershipValue{
Member: member,
Group: group,
WithAdminOption: withAdminOption,
GrantedBy: grantedBy,
}
}
// IsRoleAMember returns whether the given role is a member of the group by returning the group's ID. Also returns
// whether the member was granted WITH ADMIN OPTION, allowing it to grant membership to the group to other roles. A
// member does not automatically have ADMIN OPTION on itself, therefore this check must be performed.
func IsRoleAMember(member RoleID, group RoleID) (groupID RoleID, inheritsPrivileges bool, hasWithAdminOption bool) {
// If the member and group are the same, then we only check for SUPERUSER status to allow WITH ADMIN OPTION
if member == group {
return group, true, globalDatabase.rolesByID[member].IsSuperUser
}
// Postgres does not allow for circular role membership, so we can recursively check without worry:
// https://www.postgresql.org/docs/15/catalog-pg-auth-members.html
if groupMap, ok := globalDatabase.roleMembership.Data[member]; ok {
for _, value := range groupMap {
if value.Group == group {
return group, globalDatabase.rolesByID[member].InheritPrivileges, value.WithAdminOption
}
// This recursively walks through memberships
if groupID, _, hasWithAdminOption = IsRoleAMember(value.Group, group); groupID.IsValid() {
return groupID, globalDatabase.rolesByID[member].InheritPrivileges, hasWithAdminOption
}
}
}
// A SUPERUSER has access to everything, and therefore functions as though it's a member of every group
if globalDatabase.rolesByID[member].IsSuperUser {
return group, true, true
}
return 0, false, false
}
// GetAllGroupsWithMember returns every group that the role is a direct member of. This can also filter by groups that
// the member has privilege access on.
func GetAllGroupsWithMember(member RoleID, inheritsPrivilegesOnly bool) []RoleID {
memberRole, ok := globalDatabase.rolesByID[member]
if !ok || !memberRole.InheritPrivileges {
return nil
}
groupMap := globalDatabase.roleMembership.Data[member]
groups := make([]RoleID, 0, len(groupMap))
for groupID := range groupMap {
groups = append(groups, groupID)
}
return groups
}
// RemoveMemberFromGroup removes the member from the group. If `adminOptionOnly` is true, then only the WITH ADMIN
// OPTION portion is revoked. If `adminOptionOnly` is false, then the member is fully is removed.
func RemoveMemberFromGroup(member RoleID, group RoleID, adminOptionOnly bool) {
if groupMap, ok := globalDatabase.roleMembership.Data[member]; ok {
if adminOptionOnly {
value := groupMap[group]
value.WithAdminOption = false
groupMap[group] = value
} else {
delete(groupMap, group)
}
if len(groupMap) == 0 {
delete(globalDatabase.roleMembership.Data, member)
}
}
}
// serialize writes the RoleMembership to the given writer.
func (membership *RoleMembership) serialize(writer *utils.Writer) {
// Version 0
// Write the total number of members
writer.Uint64(uint64(len(membership.Data)))
for _, groupMap := range membership.Data {
// Write the number of groups
writer.Uint64(uint64(len(groupMap)))
for _, mapValue := range groupMap {
// Write the membership information
writer.Uint64(uint64(mapValue.Member))
writer.Uint64(uint64(mapValue.Group))
writer.Bool(mapValue.WithAdminOption)
writer.Uint64(uint64(mapValue.GrantedBy))
}
}
}
// deserialize reads the RoleMembership from the given reader.
func (membership *RoleMembership) deserialize(version uint32, reader *utils.Reader) {
membership.Data = make(map[RoleID]map[RoleID]RoleMembershipValue)
switch version {
case 0, 1:
// Read the total number of members
memberCount := reader.Uint64()
for memberIdx := uint64(0); memberIdx < memberCount; memberIdx++ {
// Read the number of groups
groupCount := reader.Uint64()
groupMap := make(map[RoleID]RoleMembershipValue)
var member RoleID
for groupIdx := uint64(0); groupIdx < groupCount; groupIdx++ {
// Read the membership information
value := RoleMembershipValue{}
value.Member = RoleID(reader.Uint64())
value.Group = RoleID(reader.Uint64())
value.WithAdminOption = reader.Bool()
value.GrantedBy = RoleID(reader.Uint64())
// Add the information to the map
groupMap[value.Group] = value
member = value.Member
}
// Add the group map to the data
membership.Data[member] = groupMap
}
default:
panic("unexpected version in RoleMembership")
}
}
+223
View File
@@ -0,0 +1,223 @@
// Copyright 2026 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 (
"github.com/dolthub/doltgresql/utils"
)
// RoutinePrivileges contains the privileges given to a role on a routine (function or procedure).
type RoutinePrivileges struct {
Data map[RoutinePrivilegeKey]RoutinePrivilegeValue
}
// RoutinePrivilegeKey points to a specific routine object. An empty Name represents all routines in the schema.
// ArgTypes is a comma-separated list of argument type SQL strings, e.g. "integer,text".
type RoutinePrivilegeKey struct {
Role RoleID
Schema string
Name string
ArgTypes string
}
// RoutinePrivilegeValue is the value associated with the RoutinePrivilegeKey.
type RoutinePrivilegeValue struct {
Key RoutinePrivilegeKey
Privileges map[Privilege]map[GrantedPrivilege]bool
}
// NewRoutinePrivileges returns a new *RoutinePrivileges.
func NewRoutinePrivileges() *RoutinePrivileges {
return &RoutinePrivileges{make(map[RoutinePrivilegeKey]RoutinePrivilegeValue)}
}
// AddRoutinePrivilege adds the given routine privilege to the global database.
func AddRoutinePrivilege(key RoutinePrivilegeKey, privilege GrantedPrivilege, withGrantOption bool) {
routinePrivilegeValue, ok := globalDatabase.routinePrivileges.Data[key]
if !ok {
routinePrivilegeValue = RoutinePrivilegeValue{
Key: key,
Privileges: make(map[Privilege]map[GrantedPrivilege]bool),
}
globalDatabase.routinePrivileges.Data[key] = routinePrivilegeValue
}
privilegeMap, ok := routinePrivilegeValue.Privileges[privilege.Privilege]
if !ok {
privilegeMap = make(map[GrantedPrivilege]bool)
routinePrivilegeValue.Privileges[privilege.Privilege] = privilegeMap
}
privilegeMap[privilege] = withGrantOption
}
// HasRoutinePrivilege checks whether the user has the given privilege on the associated routine.
func HasRoutinePrivilege(key RoutinePrivilegeKey, privilege Privilege) bool {
if IsSuperUser(key.Role) {
return true
}
// If a routine name was provided, also check for privileges on all routines in the schema.
if len(key.Name) > 0 {
if HasRoutinePrivilege(RoutinePrivilegeKey{
Role: key.Role,
Schema: key.Schema,
Name: "",
}, privilege) {
return true
}
}
if routinePrivilegeValue, ok := globalDatabase.routinePrivileges.Data[key]; ok {
if privilegeMap, ok := routinePrivilegeValue.Privileges[privilege]; ok && len(privilegeMap) > 0 {
return true
}
}
for _, group := range GetAllGroupsWithMember(key.Role, true) {
if HasRoutinePrivilege(RoutinePrivilegeKey{
Role: group,
Schema: key.Schema,
Name: key.Name,
ArgTypes: key.ArgTypes,
}, privilege) {
return true
}
}
return false
}
// HasRoutinePrivilegeGrantOption checks whether the user has WITH GRANT OPTION for the given privilege on the
// associated routine. Returns the role that has WITH GRANT OPTION, or an invalid role if not available.
func HasRoutinePrivilegeGrantOption(key RoutinePrivilegeKey, privilege Privilege) RoleID {
if IsSuperUser(key.Role) {
return key.Role
}
// If a routine name was provided, also check for grant option on all routines in the schema.
if len(key.Name) > 0 {
if returnedID := HasRoutinePrivilegeGrantOption(RoutinePrivilegeKey{
Role: key.Role,
Schema: key.Schema,
Name: "",
}, privilege); returnedID.IsValid() {
return returnedID
}
}
if routinePrivilegeValue, ok := globalDatabase.routinePrivileges.Data[key]; ok {
if privilegeMap, ok := routinePrivilegeValue.Privileges[privilege]; ok {
for _, withGrantOption := range privilegeMap {
if withGrantOption {
return key.Role
}
}
}
}
for _, group := range GetAllGroupsWithMember(key.Role, true) {
if returnedID := HasRoutinePrivilegeGrantOption(RoutinePrivilegeKey{
Role: group,
Schema: key.Schema,
Name: key.Name,
ArgTypes: key.ArgTypes,
}, privilege); returnedID.IsValid() {
return returnedID
}
}
return 0
}
// RemoveRoutinePrivilege removes the privilege from the global database. If `grantOptionOnly` is true, then only the
// WITH GRANT OPTION portion is revoked. If `grantOptionOnly` is false, then the full privilege is removed.
func RemoveRoutinePrivilege(key RoutinePrivilegeKey, privilege GrantedPrivilege, grantOptionOnly bool) {
if routinePrivilegeValue, ok := globalDatabase.routinePrivileges.Data[key]; ok {
if privilegeMap, ok := routinePrivilegeValue.Privileges[privilege.Privilege]; ok {
if grantOptionOnly {
if privilege.GrantedBy.IsValid() {
if _, ok = privilegeMap[privilege]; ok {
privilegeMap[privilege] = false
}
} else {
for privilegeMapKey := range privilegeMap {
privilegeMap[privilegeMapKey] = false
}
}
} else {
if privilege.GrantedBy.IsValid() {
delete(privilegeMap, privilege)
} else {
privilegeMap = nil
}
if len(privilegeMap) == 0 {
delete(routinePrivilegeValue.Privileges, privilege.Privilege)
}
}
}
if len(routinePrivilegeValue.Privileges) == 0 {
delete(globalDatabase.routinePrivileges.Data, key)
}
}
}
// serialize writes the RoutinePrivileges to the given writer.
func (rp *RoutinePrivileges) serialize(writer *utils.Writer) {
// Version 1
// Write the total number of values
writer.Uint64(uint64(len(rp.Data)))
for _, value := range rp.Data {
writer.Uint64(uint64(value.Key.Role))
writer.String(value.Key.Schema)
writer.String(value.Key.Name)
writer.String(value.Key.ArgTypes)
// Write the total number of privileges
writer.Uint64(uint64(len(value.Privileges)))
for privilege, privilegeMap := range value.Privileges {
writer.String(string(privilege))
writer.Uint32(uint32(len(privilegeMap)))
for grantedPrivilege, withGrantOption := range privilegeMap {
writer.Uint64(uint64(grantedPrivilege.GrantedBy))
writer.Bool(withGrantOption)
}
}
}
}
// deserialize reads the RoutinePrivileges from the given reader.
func (rp *RoutinePrivileges) deserialize(version uint32, reader *utils.Reader) {
rp.Data = make(map[RoutinePrivilegeKey]RoutinePrivilegeValue)
switch version {
case 0:
case 1:
// Read the total number of values
dataCount := reader.Uint64()
for dataIdx := uint64(0); dataIdx < dataCount; dataIdx++ {
rpv := RoutinePrivilegeValue{Privileges: make(map[Privilege]map[GrantedPrivilege]bool)}
rpv.Key.Role = RoleID(reader.Uint64())
rpv.Key.Schema = reader.String()
rpv.Key.Name = reader.String()
rpv.Key.ArgTypes = reader.String()
// Read the total number of privileges
privilegeCount := reader.Uint64()
for privilegeIdx := uint64(0); privilegeIdx < privilegeCount; privilegeIdx++ {
privilege := Privilege(reader.String())
grantedCount := reader.Uint32()
grantedMap := make(map[GrantedPrivilege]bool)
for grantedIdx := uint32(0); grantedIdx < grantedCount; grantedIdx++ {
grantedPrivilege := GrantedPrivilege{}
grantedPrivilege.Privilege = privilege
grantedPrivilege.GrantedBy = RoleID(reader.Uint64())
grantedMap[grantedPrivilege] = reader.Bool()
}
rpv.Privileges[privilege] = grantedMap
}
rp.Data[rpv.Key] = rpv
}
default:
panic("unexpected version in RoutinePrivileges")
}
}
+202
View File
@@ -0,0 +1,202 @@
// 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 (
"github.com/dolthub/doltgresql/utils"
)
// SchemaPrivileges contains the privileges given to a role on a schema.
type SchemaPrivileges struct {
Data map[SchemaPrivilegeKey]SchemaPrivilegeValue
}
// SchemaPrivilegeKey points to a specific schema object.
type SchemaPrivilegeKey struct {
Role RoleID
Schema string
}
// SchemaPrivilegeValue is the value associated with the SchemaPrivilegeKey.
type SchemaPrivilegeValue struct {
Key SchemaPrivilegeKey
Privileges map[Privilege]map[GrantedPrivilege]bool
}
// NewSchemaPrivileges returns a new *SchemaPrivileges.
func NewSchemaPrivileges() *SchemaPrivileges {
return &SchemaPrivileges{make(map[SchemaPrivilegeKey]SchemaPrivilegeValue)}
}
// AddSchemaPrivilege adds the given schema privilege to the global database.
func AddSchemaPrivilege(key SchemaPrivilegeKey, privilege GrantedPrivilege, withGrantOption bool) {
schemaPrivilegeValue, ok := globalDatabase.schemaPrivileges.Data[key]
if !ok {
schemaPrivilegeValue = SchemaPrivilegeValue{
Key: key,
Privileges: make(map[Privilege]map[GrantedPrivilege]bool),
}
globalDatabase.schemaPrivileges.Data[key] = schemaPrivilegeValue
}
privilegeMap, ok := schemaPrivilegeValue.Privileges[privilege.Privilege]
if !ok {
privilegeMap = make(map[GrantedPrivilege]bool)
schemaPrivilegeValue.Privileges[privilege.Privilege] = privilegeMap
}
privilegeMap[privilege] = withGrantOption
}
// HasSchemaPrivilege checks whether the user has the given privilege on the associated schema.
func HasSchemaPrivilege(key SchemaPrivilegeKey, privilege Privilege) bool {
if IsSuperUser(key.Role) {
return true
}
if schemaPrivilegeValue, ok := globalDatabase.schemaPrivileges.Data[key]; ok {
if privilegeMap, ok := schemaPrivilegeValue.Privileges[privilege]; ok && len(privilegeMap) > 0 {
return true
}
}
for _, group := range GetAllGroupsWithMember(key.Role, true) {
if HasSchemaPrivilege(SchemaPrivilegeKey{
Role: group,
Schema: key.Schema,
}, privilege) {
return true
}
}
return false
}
// HasSchemaPrivilegeGrantOption checks whether the user has WITH GRANT OPTION for the given privilege on the associated
// schema. Returns the role that has WITH GRANT OPTION, or an invalid role if WITH GRANT OPTION is not available.
func HasSchemaPrivilegeGrantOption(key SchemaPrivilegeKey, privilege Privilege) RoleID {
if IsSuperUser(key.Role) {
return key.Role
}
if schemaPrivilegeValue, ok := globalDatabase.schemaPrivileges.Data[key]; ok {
if privilegeMap, ok := schemaPrivilegeValue.Privileges[privilege]; ok {
for _, withGrantOption := range privilegeMap {
if withGrantOption {
return key.Role
}
}
}
}
for _, group := range GetAllGroupsWithMember(key.Role, true) {
if returnedID := HasSchemaPrivilegeGrantOption(SchemaPrivilegeKey{
Role: group,
Schema: key.Schema,
}, privilege); returnedID.IsValid() {
return returnedID
}
}
return 0
}
// RemoveSchemaPrivilege removes the privilege from the global database. If `grantOptionOnly` is true, then only the WITH
// GRANT OPTION portion is revoked. If `grantOptionOnly` is false, then the full privilege is removed. If the GrantedBy
// field contains a valid RoleID, then only the privilege associated with that granter is removed. Otherwise, the
// privilege is completely removed for the grantee.
func RemoveSchemaPrivilege(key SchemaPrivilegeKey, privilege GrantedPrivilege, grantOptionOnly bool) {
if schemaPrivilegeValue, ok := globalDatabase.schemaPrivileges.Data[key]; ok {
if privilegeMap, ok := schemaPrivilegeValue.Privileges[privilege.Privilege]; ok {
if grantOptionOnly {
// This is provided when we only want to revoke the WITH GRANT OPTION, and not the privilege itself.
// If a role is provided in GRANTED BY, then we specifically delete the option associated with that role.
// If no role was given, then we'll remove WITH GRANT OPTION from all of the associated roles.
if privilege.GrantedBy.IsValid() {
if _, ok = privilegeMap[privilege]; ok {
privilegeMap[privilege] = false
}
} else {
for privilegeMapKey := range privilegeMap {
privilegeMap[privilegeMapKey] = false
}
}
} else {
// If a role is provided in GRANTED BY, then we specifically delete the privilege associated with that role.
// If no role was given, then we'll delete the privileges granted by all roles.
if privilege.GrantedBy.IsValid() {
delete(privilegeMap, privilege)
} else {
privilegeMap = nil
}
if len(privilegeMap) == 0 {
delete(schemaPrivilegeValue.Privileges, privilege.Privilege)
}
}
}
if len(schemaPrivilegeValue.Privileges) == 0 {
delete(globalDatabase.schemaPrivileges.Data, key)
}
}
}
// serialize writes the SchemaPrivileges to the given writer.
func (sp *SchemaPrivileges) serialize(writer *utils.Writer) {
// Version 0
// Write the total number of values
writer.Uint64(uint64(len(sp.Data)))
for _, value := range sp.Data {
// Write the key
writer.Uint64(uint64(value.Key.Role))
writer.String(value.Key.Schema)
// Write the total number of privileges
writer.Uint64(uint64(len(value.Privileges)))
for privilege, privilegeMap := range value.Privileges {
writer.String(string(privilege))
// Write the number of granted privileges
writer.Uint32(uint32(len(privilegeMap)))
for grantedPrivilege, withGrantOption := range privilegeMap {
writer.Uint64(uint64(grantedPrivilege.GrantedBy))
writer.Bool(withGrantOption)
}
}
}
}
// deserialize reads the SchemaPrivileges from the given reader.
func (sp *SchemaPrivileges) deserialize(version uint32, reader *utils.Reader) {
sp.Data = make(map[SchemaPrivilegeKey]SchemaPrivilegeValue)
switch version {
case 0, 1:
// Read the total number of values
dataCount := reader.Uint64()
for dataIdx := uint64(0); dataIdx < dataCount; dataIdx++ {
// Read the key
spv := SchemaPrivilegeValue{Privileges: make(map[Privilege]map[GrantedPrivilege]bool)}
spv.Key.Role = RoleID(reader.Uint64())
spv.Key.Schema = reader.String()
// Read the total number of privileges
privilegeCount := reader.Uint64()
for privilegeIdx := uint64(0); privilegeIdx < privilegeCount; privilegeIdx++ {
privilege := Privilege(reader.String())
// Read the number of granted privileges
grantedCount := reader.Uint32()
grantedMap := make(map[GrantedPrivilege]bool)
for grantedIdx := uint32(0); grantedIdx < grantedCount; grantedIdx++ {
grantedPrivilege := GrantedPrivilege{}
grantedPrivilege.Privilege = privilege
grantedPrivilege.GrantedBy = RoleID(reader.Uint64())
grantedMap[grantedPrivilege] = reader.Bool()
}
spv.Privileges[privilege] = grantedMap
}
sp.Data[spv.Key] = spv
}
default:
panic("unexpected version in SchemaPrivileges")
}
}
+217
View File
@@ -0,0 +1,217 @@
// Copyright 2026 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 (
"github.com/dolthub/doltgresql/utils"
)
// SequencePrivileges contains the privileges given to a role on a sequence.
type SequencePrivileges struct {
Data map[SequencePrivilegeKey]SequencePrivilegeValue
}
// SequencePrivilegeKey points to a specific sequence object. An empty Name represents all sequences in the schema.
type SequencePrivilegeKey struct {
Role RoleID
Schema string
Name string
}
// SequencePrivilegeValue is the value associated with the SequencePrivilegeKey.
type SequencePrivilegeValue struct {
Key SequencePrivilegeKey
Privileges map[Privilege]map[GrantedPrivilege]bool
}
// NewSequencePrivileges returns a new *SequencePrivileges.
func NewSequencePrivileges() *SequencePrivileges {
return &SequencePrivileges{make(map[SequencePrivilegeKey]SequencePrivilegeValue)}
}
// AddSequencePrivilege adds the given sequence privilege to the global database.
func AddSequencePrivilege(key SequencePrivilegeKey, privilege GrantedPrivilege, withGrantOption bool) {
seqPrivilegeValue, ok := globalDatabase.sequencePrivileges.Data[key]
if !ok {
seqPrivilegeValue = SequencePrivilegeValue{
Key: key,
Privileges: make(map[Privilege]map[GrantedPrivilege]bool),
}
globalDatabase.sequencePrivileges.Data[key] = seqPrivilegeValue
}
privilegeMap, ok := seqPrivilegeValue.Privileges[privilege.Privilege]
if !ok {
privilegeMap = make(map[GrantedPrivilege]bool)
seqPrivilegeValue.Privileges[privilege.Privilege] = privilegeMap
}
privilegeMap[privilege] = withGrantOption
}
// HasSequencePrivilege checks whether the user has the given privilege on the associated sequence.
func HasSequencePrivilege(key SequencePrivilegeKey, privilege Privilege) bool {
if IsSuperUser(key.Role) {
return true
}
// If a sequence name was provided, also check for privileges on all sequences in the schema.
if len(key.Name) > 0 {
if HasSequencePrivilege(SequencePrivilegeKey{
Role: key.Role,
Schema: key.Schema,
Name: "",
}, privilege) {
return true
}
}
if seqPrivilegeValue, ok := globalDatabase.sequencePrivileges.Data[key]; ok {
if privilegeMap, ok := seqPrivilegeValue.Privileges[privilege]; ok && len(privilegeMap) > 0 {
return true
}
}
for _, group := range GetAllGroupsWithMember(key.Role, true) {
if HasSequencePrivilege(SequencePrivilegeKey{
Role: group,
Schema: key.Schema,
Name: key.Name,
}, privilege) {
return true
}
}
return false
}
// HasSequencePrivilegeGrantOption checks whether the user has WITH GRANT OPTION for the given privilege on the
// associated sequence. Returns the role that has WITH GRANT OPTION, or an invalid role if not available.
func HasSequencePrivilegeGrantOption(key SequencePrivilegeKey, privilege Privilege) RoleID {
if IsSuperUser(key.Role) {
return key.Role
}
// If a sequence name was provided, also check for grant option on all sequences in the schema.
if len(key.Name) > 0 {
if returnedID := HasSequencePrivilegeGrantOption(SequencePrivilegeKey{
Role: key.Role,
Schema: key.Schema,
Name: "",
}, privilege); returnedID.IsValid() {
return returnedID
}
}
if seqPrivilegeValue, ok := globalDatabase.sequencePrivileges.Data[key]; ok {
if privilegeMap, ok := seqPrivilegeValue.Privileges[privilege]; ok {
for _, withGrantOption := range privilegeMap {
if withGrantOption {
return key.Role
}
}
}
}
for _, group := range GetAllGroupsWithMember(key.Role, true) {
if returnedID := HasSequencePrivilegeGrantOption(SequencePrivilegeKey{
Role: group,
Schema: key.Schema,
Name: key.Name,
}, privilege); returnedID.IsValid() {
return returnedID
}
}
return 0
}
// RemoveSequencePrivilege removes the privilege from the global database. If `grantOptionOnly` is true, then only the
// WITH GRANT OPTION portion is revoked. If `grantOptionOnly` is false, then the full privilege is removed.
func RemoveSequencePrivilege(key SequencePrivilegeKey, privilege GrantedPrivilege, grantOptionOnly bool) {
if seqPrivilegeValue, ok := globalDatabase.sequencePrivileges.Data[key]; ok {
if privilegeMap, ok := seqPrivilegeValue.Privileges[privilege.Privilege]; ok {
if grantOptionOnly {
if privilege.GrantedBy.IsValid() {
if _, ok = privilegeMap[privilege]; ok {
privilegeMap[privilege] = false
}
} else {
for privilegeMapKey := range privilegeMap {
privilegeMap[privilegeMapKey] = false
}
}
} else {
if privilege.GrantedBy.IsValid() {
delete(privilegeMap, privilege)
} else {
privilegeMap = nil
}
if len(privilegeMap) == 0 {
delete(seqPrivilegeValue.Privileges, privilege.Privilege)
}
}
}
if len(seqPrivilegeValue.Privileges) == 0 {
delete(globalDatabase.sequencePrivileges.Data, key)
}
}
}
// serialize writes the SequencePrivileges to the given writer.
func (sp *SequencePrivileges) serialize(writer *utils.Writer) {
// Version 1
// Write the total number of values
writer.Uint64(uint64(len(sp.Data)))
for _, value := range sp.Data {
writer.Uint64(uint64(value.Key.Role))
writer.String(value.Key.Schema)
writer.String(value.Key.Name)
// Write the total number of privileges
writer.Uint64(uint64(len(value.Privileges)))
for privilege, privilegeMap := range value.Privileges {
writer.String(string(privilege))
writer.Uint32(uint32(len(privilegeMap)))
for grantedPrivilege, withGrantOption := range privilegeMap {
writer.Uint64(uint64(grantedPrivilege.GrantedBy))
writer.Bool(withGrantOption)
}
}
}
}
// deserialize reads the SequencePrivileges from the given reader.
func (sp *SequencePrivileges) deserialize(version uint32, reader *utils.Reader) {
sp.Data = make(map[SequencePrivilegeKey]SequencePrivilegeValue)
switch version {
case 0:
case 1:
// Read the total number of values
dataCount := reader.Uint64()
for dataIdx := uint64(0); dataIdx < dataCount; dataIdx++ {
spv := SequencePrivilegeValue{Privileges: make(map[Privilege]map[GrantedPrivilege]bool)}
spv.Key.Role = RoleID(reader.Uint64())
spv.Key.Schema = reader.String()
spv.Key.Name = reader.String()
// Read the total number of privileges
privilegeCount := reader.Uint64()
for privilegeIdx := uint64(0); privilegeIdx < privilegeCount; privilegeIdx++ {
privilege := Privilege(reader.String())
grantedCount := reader.Uint32()
grantedMap := make(map[GrantedPrivilege]bool)
for grantedIdx := uint32(0); grantedIdx < grantedCount; grantedIdx++ {
grantedPrivilege := GrantedPrivilege{}
grantedPrivilege.Privilege = privilege
grantedPrivilege.GrantedBy = RoleID(reader.Uint64())
grantedMap[grantedPrivilege] = reader.Bool()
}
spv.Privileges[privilege] = grantedMap
}
sp.Data[spv.Key] = spv
}
default:
panic("unexpected version in SequencePrivileges")
}
}
+126
View File
@@ -0,0 +1,126 @@
// 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 (
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/utils"
)
// PersistChanges will save the state of the global database to disk (assuming we are not using the pure in-memory
// implementation).
func PersistChanges() error {
if fileSystem != nil {
return fileSystem.WriteFile(authFileName, globalDatabase.serialize(), 0644)
}
return nil
}
// serialize returns the Database as a byte slice.
func (db *Database) serialize() []byte {
writer := utils.NewWriter(16384)
// Write the version
writer.Uint32(1)
// Write the roles
writer.Uint32(uint32(len(db.rolesByID)))
for _, role := range db.rolesByID {
role.serialize(writer)
}
// Write the database privileges
db.databasePrivileges.serialize(writer)
// Write the schema privileges
db.schemaPrivileges.serialize(writer)
// Write the table privileges
db.tablePrivileges.serialize(writer)
// Write the sequence privileges
db.sequencePrivileges.serialize(writer)
// Write the routine privileges
db.routinePrivileges.serialize(writer)
// Write the role chain
db.roleMembership.serialize(writer)
return writer.Data()
}
// deserialize creates a Database from a byte slice.
func (db *Database) deserialize(data []byte) error {
if len(data) < 4 {
return errors.New("invalid auth database format")
}
reader := utils.NewReader(data)
version := reader.Uint32()
switch version {
case 0:
return db.deserializeV0(reader)
case 1:
return db.deserializeV1(reader)
default:
return errors.Errorf("Authorization database format %d is not supported, please upgrade Doltgres", version)
}
}
// deserializeV0 creates a Database from a byte slice. Expects a reader that has already read the version.
func (db *Database) deserializeV0(reader *utils.Reader) error {
// Read the roles
clear(db.rolesByName)
clear(db.rolesByID)
roleCount := reader.Uint32()
for i := uint32(0); i < roleCount; i++ {
r := Role{}
r.deserialize(0, reader)
db.rolesByName[r.Name] = r.id
db.rolesByID[r.id] = r
}
// Read the database privileges
db.databasePrivileges.deserialize(0, reader)
// Read the schema privileges
db.schemaPrivileges.deserialize(0, reader)
// Read the table privileges
db.tablePrivileges.deserialize(0, reader)
// Read the sequence privileges
db.sequencePrivileges.deserialize(0, reader)
// Read the routine privileges
db.routinePrivileges.deserialize(0, reader)
// Read the role membership
db.roleMembership.deserialize(0, reader)
return nil
}
// deserializeV1 creates a Database from a byte slice. Expects a reader that has already read the version.
func (db *Database) deserializeV1(reader *utils.Reader) error {
// Read the roles
clear(db.rolesByName)
clear(db.rolesByID)
roleCount := reader.Uint32()
for i := uint32(0); i < roleCount; i++ {
r := Role{}
r.deserialize(1, reader)
db.rolesByName[r.Name] = r.id
db.rolesByID[r.id] = r
}
// Read the database privileges
db.databasePrivileges.deserialize(1, reader)
// Read the schema privileges
db.schemaPrivileges.deserialize(1, reader)
// Read the table privileges
db.tablePrivileges.deserialize(1, reader)
// Read the sequence privileges
db.sequencePrivileges.deserialize(1, reader)
// Read the routine privileges
db.routinePrivileges.deserialize(1, reader)
// Read the role membership
db.roleMembership.deserialize(1, reader)
return nil
}
+226
View File
@@ -0,0 +1,226 @@
// 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 (
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/doltgresql/utils"
)
// TablePrivileges contains the privileges given to a role on a table.
type TablePrivileges struct {
Data map[TablePrivilegeKey]TablePrivilegeValue
}
// TablePrivilegeKey points to a specific table object.
type TablePrivilegeKey struct {
Role RoleID
Table doltdb.TableName
}
// TablePrivilegeValue is the value associated with the TablePrivilegeKey.
type TablePrivilegeValue struct {
Key TablePrivilegeKey
Privileges map[Privilege]map[GrantedPrivilege]bool
}
// NewTablePrivileges returns a new *TablePrivileges.
func NewTablePrivileges() *TablePrivileges {
return &TablePrivileges{make(map[TablePrivilegeKey]TablePrivilegeValue)}
}
// AddTablePrivilege adds the given table privilege to the global database.
func AddTablePrivilege(key TablePrivilegeKey, privilege GrantedPrivilege, withGrantOption bool) {
tablePrivilegeValue, ok := globalDatabase.tablePrivileges.Data[key]
if !ok {
tablePrivilegeValue = TablePrivilegeValue{
Key: key,
Privileges: make(map[Privilege]map[GrantedPrivilege]bool),
}
globalDatabase.tablePrivileges.Data[key] = tablePrivilegeValue
}
privilegeMap, ok := tablePrivilegeValue.Privileges[privilege.Privilege]
if !ok {
privilegeMap = make(map[GrantedPrivilege]bool)
tablePrivilegeValue.Privileges[privilege.Privilege] = privilegeMap
}
privilegeMap[privilege] = withGrantOption
}
// HasTablePrivilege checks whether the user has the given privilege on the associated table.
func HasTablePrivilege(key TablePrivilegeKey, privilege Privilege) bool {
if IsSuperUser(key.Role) {
return true
}
// If a table name was provided, then we also want to search for privileges provided to all tables in the schema
// space. Since those are saved with an empty table name, we can easily do another search by removing the table.
if len(key.Table.Name) > 0 {
if ok := HasTablePrivilege(TablePrivilegeKey{
Role: key.Role,
Table: doltdb.TableName{Name: "", Schema: key.Table.Schema},
}, privilege); ok {
return true
}
}
if tablePrivilegeValue, ok := globalDatabase.tablePrivileges.Data[key]; ok {
if privilegeMap, ok := tablePrivilegeValue.Privileges[privilege]; ok && len(privilegeMap) > 0 {
return true
}
}
for _, group := range GetAllGroupsWithMember(key.Role, true) {
if HasTablePrivilege(TablePrivilegeKey{
Role: group,
Table: key.Table,
}, privilege) {
return true
}
}
return false
}
// HasTablePrivilegeGrantOption checks whether the user has WITH GRANT OPTION for the given privilege on the associated
// table. Returns the role that has WITH GRANT OPTION, or an invalid role if WITH GRANT OPTION is not available.
func HasTablePrivilegeGrantOption(key TablePrivilegeKey, privilege Privilege) RoleID {
if IsSuperUser(key.Role) {
return key.Role
}
// If a table name was provided, then we also want to search for privileges provided to all tables in the schema
// space. Since those are saved with an empty table name, we can easily do another search by removing the table.
if len(key.Table.Name) > 0 {
if returnedID := HasTablePrivilegeGrantOption(TablePrivilegeKey{
Role: key.Role,
Table: doltdb.TableName{Name: "", Schema: key.Table.Schema},
}, privilege); returnedID.IsValid() {
return returnedID
}
}
if tablePrivilegeValue, ok := globalDatabase.tablePrivileges.Data[key]; ok {
if privilegeMap, ok := tablePrivilegeValue.Privileges[privilege]; ok {
for _, withGrantOption := range privilegeMap {
if withGrantOption {
return key.Role
}
}
}
}
for _, group := range GetAllGroupsWithMember(key.Role, true) {
if returnedID := HasTablePrivilegeGrantOption(TablePrivilegeKey{
Role: group,
Table: key.Table,
}, privilege); returnedID.IsValid() {
return returnedID
}
}
return 0
}
// RemoveTablePrivilege removes the privilege from the global database. If `grantOptionOnly` is true, then only the WITH
// GRANT OPTION portion is revoked. If `grantOptionOnly` is false, then the full privilege is removed. If the GrantedBy
// field contains a valid RoleID, then only the privilege associated with that granter is removed. Otherwise, the
// privilege is completely removed for the grantee.
func RemoveTablePrivilege(key TablePrivilegeKey, privilege GrantedPrivilege, grantOptionOnly bool) {
if tablePrivilegeValue, ok := globalDatabase.tablePrivileges.Data[key]; ok {
if privilegeMap, ok := tablePrivilegeValue.Privileges[privilege.Privilege]; ok {
if grantOptionOnly {
// This is provided when we only want to revoke the WITH GRANT OPTION, and not the privilege itself.
// If a role is provided in GRANTED BY, then we specifically delete the option associated with that role.
// If no role was given, then we'll remove WITH GRANT OPTION from all of the associated roles.
if privilege.GrantedBy.IsValid() {
if _, ok = privilegeMap[privilege]; ok {
privilegeMap[privilege] = false
}
} else {
for privilegeMapKey := range privilegeMap {
privilegeMap[privilegeMapKey] = false
}
}
} else {
// If a role is provided in GRANTED BY, then we specifically delete the privilege associated with that role.
// If no role was given, then we'll delete the privileges granted by all roles.
if privilege.GrantedBy.IsValid() {
delete(privilegeMap, privilege)
} else {
privilegeMap = nil
}
if len(privilegeMap) == 0 {
delete(tablePrivilegeValue.Privileges, privilege.Privilege)
}
}
}
if len(tablePrivilegeValue.Privileges) == 0 {
delete(globalDatabase.tablePrivileges.Data, key)
}
}
}
// serialize writes the TablePrivileges to the given writer.
func (tp *TablePrivileges) serialize(writer *utils.Writer) {
// Version 0
// Write the total number of values
writer.Uint64(uint64(len(tp.Data)))
for _, value := range tp.Data {
// Write the key
writer.Uint64(uint64(value.Key.Role))
writer.String(value.Key.Table.Name)
writer.String(value.Key.Table.Schema)
// Write the total number of privileges
writer.Uint64(uint64(len(value.Privileges)))
for privilege, privilegeMap := range value.Privileges {
writer.String(string(privilege))
// Write the number of granted privileges
writer.Uint32(uint32(len(privilegeMap)))
for grantedPrivilege, withGrantOption := range privilegeMap {
writer.Uint64(uint64(grantedPrivilege.GrantedBy))
writer.Bool(withGrantOption)
}
}
}
}
// deserialize reads the TablePrivileges from the given reader.
func (tp *TablePrivileges) deserialize(version uint32, reader *utils.Reader) {
tp.Data = make(map[TablePrivilegeKey]TablePrivilegeValue)
switch version {
case 0, 1:
// Read the total number of values
dataCount := reader.Uint64()
for dataIdx := uint64(0); dataIdx < dataCount; dataIdx++ {
// Read the key
tpv := TablePrivilegeValue{Privileges: make(map[Privilege]map[GrantedPrivilege]bool)}
tpv.Key.Role = RoleID(reader.Uint64())
tpv.Key.Table.Name = reader.String()
tpv.Key.Table.Schema = reader.String()
// Read the total number of privileges
privilegeCount := reader.Uint64()
for privilegeIdx := uint64(0); privilegeIdx < privilegeCount; privilegeIdx++ {
privilege := Privilege(reader.String())
// Read the number of granted privileges
grantedCount := reader.Uint32()
grantedMap := make(map[GrantedPrivilege]bool)
for grantedIdx := uint32(0); grantedIdx < grantedCount; grantedIdx++ {
grantedPrivilege := GrantedPrivilege{}
grantedPrivilege.Privilege = privilege
grantedPrivilege.GrantedBy = RoleID(reader.Uint64())
grantedMap[grantedPrivilege] = reader.Bool()
}
tpv.Privileges[privilege] = grantedMap
}
tp.Data[tpv.Key] = tpv
}
default:
panic("unexpected version in TablePrivileges")
}
}