chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user