blob: b6d7076e7f1c89dff2ed7abaf6ffda82f1bbdb3c [file]
package backup
import (
"database/sql"
"fmt"
"regexp"
"sort"
"strings"
"github.com/apache/cloudberry-backup/toc"
"github.com/apache/cloudberry-backup/utils"
)
var ACLRegex = regexp.MustCompile(`^(.*)=([a-zA-Z\*]*)/(.*)$`)
/*
* Structs and functions relating to generic metadata handling.
*/
type ObjectMetadata struct {
Privileges []ACL
ObjectType string
Owner string
Comment string
SecurityLabelProvider string
SecurityLabel string
}
type ACL struct {
Grantee string
Select bool
SelectWithGrant bool
Insert bool
InsertWithGrant bool
Update bool
UpdateWithGrant bool
Delete bool
DeleteWithGrant bool
Truncate bool
TruncateWithGrant bool
References bool
ReferencesWithGrant bool
Trigger bool
TriggerWithGrant bool
Usage bool
UsageWithGrant bool
Execute bool
ExecuteWithGrant bool
Create bool
CreateWithGrant bool
Temporary bool
TemporaryWithGrant bool
Connect bool
ConnectWithGrant bool
}
type MetadataMap map[UniqueID]ObjectMetadata
func PrintStatements(metadataFile *utils.FileWithByteCount, objToc *toc.TOC,
obj toc.TOCObject, statements []string, tier []uint32) {
for _, statement := range statements {
start := metadataFile.ByteCount
metadataFile.MustPrintf("\n\n%s\n", statement)
section, entry := obj.GetMetadataEntry()
/*
* Postdata metadata needs to be specifically marked as such to
* help with gprestore postdata parallel restore (--jobs).
*/
if section == "postdata" {
entry.ObjectType = fmt.Sprintf("%s METADATA", entry.ObjectType)
if entry.Schema != "" {
entry.ReferenceObject = utils.MakeFQN(entry.Schema, entry.Name)
} else {
entry.ReferenceObject = entry.Name
}
}
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, tier)
}
}
func PrintObjectMetadata(metadataFile *utils.FileWithByteCount, objToc *toc.TOC,
metadata ObjectMetadata, obj toc.TOCObjectWithMetadata, owningTable string, tier []uint32) {
_, entry := obj.GetMetadataEntry()
if entry.ObjectType == toc.OBJ_DATABASE_METADATA {
entry.ObjectType = toc.OBJ_DATABASE
}
statements := make([]string, 0)
if comment := metadata.GetCommentStatement(obj.FQN(), entry.ObjectType, owningTable); comment != "" {
statements = append(statements, strings.TrimSpace(comment))
}
objectType := entry.ObjectType
if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
switch object := obj.(type) {
case Function:
if object.Kind == "p" {
// Procedures are handled as a kind of function but with different syntax and capabilities.
// Distinction made when printing here
objectType = toc.OBJ_PROCEDURE
}
}
}
if owner := metadata.GetOwnerStatement(obj.FQN(), objectType); owner != "" {
statements = append(statements, strings.TrimSpace(owner))
}
if privileges := metadata.GetPrivilegesStatements(obj.FQN(), entry.ObjectType); privileges != "" {
statements = append(statements, strings.TrimSpace(privileges))
}
if securityLabel := metadata.GetSecurityLabelStatement(obj.FQN(), entry.ObjectType); securityLabel != "" {
statements = append(statements, strings.TrimSpace(securityLabel))
}
PrintStatements(metadataFile, objToc, obj, statements, tier)
}
// Only print grant statements for any functions that belong to extensions
func printExtensionFunctionACLs(metadataFile *utils.FileWithByteCount, objToc *toc.TOC,
metadataMap MetadataMap, funcInfoMap map[uint32]FunctionInfo) {
type objectInfo struct {
FunctionInfo
ObjectMetadata
}
var objects []objectInfo
for uniqueId, obj := range metadataMap {
// e.g. Grants for any functions that belong to extensions
info, ok := funcInfoMap[uniqueId.Oid]
if ok {
objects = append(objects, objectInfo{info, obj})
}
}
// Sort by function signature
sort.SliceStable(objects, func(i, j int) bool {
return objects[i].FQN() < objects[j].FQN()
})
statements := make([]string, 0)
for _, obj := range objects {
if privileges := obj.GetPrivilegesStatements(obj.FQN(), toc.OBJ_FUNCTION); privileges != "" {
statements = append(statements, strings.TrimSpace(privileges))
PrintStatements(metadataFile, objToc, obj, statements, []uint32{0, 0})
}
}
}
func ConstructMetadataMap(results []MetadataQueryStruct) MetadataMap {
metadataMap := make(MetadataMap)
if len(results) == 0 {
return MetadataMap{}
}
var metadata ObjectMetadata
currentUniqueID := UniqueID{}
// Collect all entries for the same object into one ObjectMetadata
for _, result := range results {
privilegesStr := ""
if result.Kind == "Empty" {
privilegesStr = "GRANTEE=/GRANTOR"
} else if result.Privileges.Valid {
privilegesStr = result.Privileges.String
}
if result.UniqueID != currentUniqueID {
if (currentUniqueID != UniqueID{}) {
metadata.Privileges = sortACLs(metadata.Privileges)
metadataMap[currentUniqueID] = metadata
}
currentUniqueID = result.UniqueID
metadata = ObjectMetadata{}
metadata.Privileges = make([]ACL, 0)
metadata.Owner = result.Owner
metadata.ObjectType = result.ObjectType
metadata.Comment = result.Comment
metadata.SecurityLabelProvider = result.SecurityLabelProvider
metadata.SecurityLabel = result.SecurityLabel
}
privileges := ParseACL(privilegesStr)
if privileges != nil {
metadata.Privileges = append(metadata.Privileges, *privileges)
}
}
metadata.Privileges = sortACLs(metadata.Privileges)
metadataMap[currentUniqueID] = metadata
return metadataMap
}
func getColumnACL(privileges sql.NullString, kind string) []ACL {
privilegesStr := ""
if kind == "Empty" {
privilegesStr = "GRANTEE=/GRANTOR"
} else if privileges.Valid {
privilegesStr = privileges.String
}
columnMetadata := make([]ACL, 0)
acl := ParseACL(privilegesStr)
if acl != nil {
columnMetadata = append(columnMetadata, *acl)
}
return columnMetadata
}
func ParseACL(aclStr string) *ACL {
grantee := ""
acl := ACL{}
if matches := ACLRegex.FindStringSubmatch(aclStr); len(matches) != 0 {
if matches[1] != "" {
grantee = matches[1]
} else {
grantee = "" // Empty string indicates privileges granted to PUBLIC
}
permStr := matches[2]
var lastChar rune
for _, char := range permStr {
switch char {
case 'a':
acl.Insert = true
case 'r':
acl.Select = true
case 'w':
acl.Update = true
case 'd':
acl.Delete = true
case 'D':
acl.Truncate = true
case 'x':
acl.References = true
case 't':
acl.Trigger = true
case 'X':
acl.Execute = true
case 'U':
acl.Usage = true
case 'C':
acl.Create = true
case 'T':
acl.Temporary = true
case 'c':
acl.Connect = true
case '*':
switch lastChar {
case 'a':
acl.Insert = false
acl.InsertWithGrant = true
case 'r':
acl.Select = false
acl.SelectWithGrant = true
case 'w':
acl.Update = false
acl.UpdateWithGrant = true
case 'd':
acl.Delete = false
acl.DeleteWithGrant = true
case 'D':
acl.Truncate = false
acl.TruncateWithGrant = true
case 'x':
acl.References = false
acl.ReferencesWithGrant = true
case 't':
acl.Trigger = false
acl.TriggerWithGrant = true
case 'X':
acl.Execute = false
acl.ExecuteWithGrant = true
case 'U':
acl.Usage = false
acl.UsageWithGrant = true
case 'C':
acl.Create = false
acl.CreateWithGrant = true
case 'T':
acl.Temporary = false
acl.TemporaryWithGrant = true
case 'c':
acl.Connect = false
acl.ConnectWithGrant = true
}
}
lastChar = char
}
if quotedRoleName, ok := quotedRoleNames[grantee]; ok {
acl.Grantee = quotedRoleName
} else {
acl.Grantee = grantee
}
return &acl
}
return nil
}
func (obj ObjectMetadata) GetPrivilegesStatements(objectName string, objectType string, columnName ...string) string {
statements := make([]string, 0)
typeStr := fmt.Sprintf("%s ", objectType)
if objectType == toc.OBJ_VIEW || objectType == toc.OBJ_FOREIGN_TABLE || objectType == toc.OBJ_MATERIALIZED_VIEW {
typeStr = "TABLE "
} else if objectType == toc.OBJ_COLUMN {
typeStr = "TABLE "
} else if objectType == toc.OBJ_AGGREGATE {
typeStr = "FUNCTION "
}
columnStr := ""
if len(columnName) == 1 {
columnStr = fmt.Sprintf("(%s) ", columnName[0])
}
if len(obj.Privileges) != 0 {
statements = append(statements, fmt.Sprintf("REVOKE ALL %sON %s%s FROM PUBLIC;", columnStr, typeStr, objectName))
if obj.Owner != "" {
statements = append(statements, fmt.Sprintf("REVOKE ALL %sON %s%s FROM %s;", columnStr, typeStr, objectName, obj.Owner))
}
for _, acl := range obj.Privileges {
grantee := ""
if acl.Grantee == "" {
grantee = "PUBLIC"
} else {
grantee = acl.Grantee
}
privStr, privWithGrantStr := createPrivilegeStrings(acl, objectType)
if privStr != "" {
statements = append(statements, fmt.Sprintf("GRANT %s %sON %s%s TO %s;", privStr, columnStr, typeStr, objectName, grantee))
}
if privWithGrantStr != "" {
statements = append(statements, fmt.Sprintf("GRANT %s %sON %s%s TO %s WITH GRANT OPTION;", privWithGrantStr, columnStr, typeStr, objectName, grantee))
}
}
}
if len(statements) > 0 {
return "\n\n" + strings.Join(statements, "\n")
}
return ""
}
func createPrivilegeStrings(acl ACL, objectType string) (string, string) {
/*
* Determine whether to print "GRANT ALL" instead of granting individual
* privileges. Information on which privileges exist for a given object
* comes from src/include/utils/acl.h in GPDB.
*/
hasAllPrivileges := false
hasAllPrivilegesWithGrant := false
privStr := ""
privWithGrantStr := ""
switch objectType {
case toc.OBJ_COLUMN:
hasAllPrivileges = acl.Select && acl.Insert && acl.Update && acl.References
hasAllPrivilegesWithGrant = acl.SelectWithGrant && acl.InsertWithGrant && acl.UpdateWithGrant && acl.ReferencesWithGrant
case toc.OBJ_DATABASE:
hasAllPrivileges = acl.Create && acl.Temporary && acl.Connect
hasAllPrivilegesWithGrant = acl.CreateWithGrant && acl.TemporaryWithGrant && acl.ConnectWithGrant
case toc.OBJ_FOREIGN_DATA_WRAPPER:
hasAllPrivileges = acl.Usage
hasAllPrivilegesWithGrant = acl.UsageWithGrant
case toc.OBJ_FOREIGN_SERVER:
hasAllPrivileges = acl.Usage
hasAllPrivilegesWithGrant = acl.UsageWithGrant
case toc.OBJ_FOREIGN_TABLE:
hasAllPrivileges = acl.Select && acl.Insert && acl.Update && acl.Delete && acl.References && acl.Trigger
hasAllPrivilegesWithGrant = acl.SelectWithGrant && acl.InsertWithGrant && acl.UpdateWithGrant && acl.DeleteWithGrant &&
acl.ReferencesWithGrant && acl.TriggerWithGrant
case toc.OBJ_FUNCTION:
hasAllPrivileges = acl.Execute
hasAllPrivilegesWithGrant = acl.ExecuteWithGrant
case toc.OBJ_LANGUAGE:
hasAllPrivileges = acl.Usage
hasAllPrivilegesWithGrant = acl.UsageWithGrant
case toc.OBJ_PROTOCOL:
hasAllPrivileges = acl.Select && acl.Insert
hasAllPrivilegesWithGrant = acl.SelectWithGrant && acl.InsertWithGrant
case toc.OBJ_SCHEMA:
hasAllPrivileges = acl.Usage && acl.Create
hasAllPrivilegesWithGrant = acl.UsageWithGrant && acl.CreateWithGrant
case toc.OBJ_SEQUENCE:
hasAllPrivileges = acl.Select && acl.Update && acl.Usage
hasAllPrivilegesWithGrant = acl.SelectWithGrant && acl.UpdateWithGrant && acl.UsageWithGrant
case toc.OBJ_TABLE, toc.OBJ_VIEW, toc.OBJ_MATERIALIZED_VIEW:
hasAllPrivileges = acl.Select && acl.Insert && acl.Update && acl.Delete && acl.Truncate && acl.References && acl.Trigger
hasAllPrivilegesWithGrant = acl.SelectWithGrant && acl.InsertWithGrant && acl.UpdateWithGrant && acl.DeleteWithGrant &&
acl.TruncateWithGrant && acl.ReferencesWithGrant && acl.TriggerWithGrant
case toc.OBJ_TABLESPACE:
hasAllPrivileges = acl.Create
hasAllPrivilegesWithGrant = acl.CreateWithGrant
case toc.OBJ_TYPE:
hasAllPrivileges = acl.Usage
hasAllPrivilegesWithGrant = acl.UsageWithGrant
case toc.OBJ_AGGREGATE:
hasAllPrivileges = acl.Execute
hasAllPrivilegesWithGrant = acl.ExecuteWithGrant
}
if hasAllPrivileges {
privStr = "ALL"
} else {
privList := make([]string, 0)
if acl.Select {
privList = append(privList, "SELECT")
}
if acl.Insert {
privList = append(privList, "INSERT")
}
if acl.Update {
privList = append(privList, "UPDATE")
}
if acl.Delete {
privList = append(privList, "DELETE")
}
if acl.Truncate {
privList = append(privList, "TRUNCATE")
}
if acl.References {
privList = append(privList, "REFERENCES")
}
if acl.Trigger {
privList = append(privList, "TRIGGER")
}
/*
* We skip checking whether acl.Execute is set here because only Functions have Execute,
* and functions only have Execute, so Execute == hasAllPrivileges for Functions.
*/
if acl.Usage {
privList = append(privList, "USAGE")
}
if acl.Create {
privList = append(privList, "CREATE")
}
if acl.Temporary {
privList = append(privList, "TEMPORARY")
}
if acl.Connect {
privList = append(privList, "CONNECT")
}
privStr = strings.Join(privList, ",")
}
if hasAllPrivilegesWithGrant {
privWithGrantStr = "ALL"
} else {
privWithGrantList := make([]string, 0)
if acl.SelectWithGrant {
privWithGrantList = append(privWithGrantList, "SELECT")
}
if acl.InsertWithGrant {
privWithGrantList = append(privWithGrantList, "INSERT")
}
if acl.UpdateWithGrant {
privWithGrantList = append(privWithGrantList, "UPDATE")
}
if acl.DeleteWithGrant {
privWithGrantList = append(privWithGrantList, "DELETE")
}
if acl.TruncateWithGrant {
privWithGrantList = append(privWithGrantList, "TRUNCATE")
}
if acl.ReferencesWithGrant {
privWithGrantList = append(privWithGrantList, "REFERENCES")
}
if acl.TriggerWithGrant {
privWithGrantList = append(privWithGrantList, "TRIGGER")
}
// The comment above regarding Execute applies to ExecuteWithGrant as well.
if acl.UsageWithGrant {
privWithGrantList = append(privWithGrantList, "USAGE")
}
if acl.CreateWithGrant {
privWithGrantList = append(privWithGrantList, "CREATE")
}
if acl.TemporaryWithGrant {
privWithGrantList = append(privWithGrantList, "TEMPORARY")
}
if acl.ConnectWithGrant {
privWithGrantList = append(privWithGrantList, "CONNECT")
}
privWithGrantStr = strings.Join(privWithGrantList, ",")
}
return privStr, privWithGrantStr
}
func (obj ObjectMetadata) GetOwnerStatement(objectName string, objectType string) string {
typeStr := objectType
if (connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6")) && (objectType == toc.OBJ_SEQUENCE || objectType == toc.OBJ_VIEW) {
// these aren't strictly object types but share use with them so we use the const strings here
typeStr = toc.OBJ_TABLE
} else if objectType == toc.OBJ_FOREIGN_SERVER {
typeStr = toc.OBJ_SERVER
}
ownerStr := ""
if obj.Owner != "" {
ownerStr = fmt.Sprintf("ALTER %s %s OWNER TO %s;", typeStr, objectName, obj.Owner)
}
return ownerStr
}
func (obj ObjectMetadata) GetCommentStatement(objectName string, objectType string, owningTable string) string {
commentStr := ""
tableStr := ""
if owningTable != "" {
tableStr = fmt.Sprintf(" ON %s", owningTable)
}
if obj.Comment != "" {
escapedComment := utils.EscapeSingleQuotes(obj.Comment)
commentStr = fmt.Sprintf("COMMENT ON %s %s%s IS '%s';", objectType, objectName, tableStr, escapedComment)
}
return commentStr
}
func (obj ObjectMetadata) GetSecurityLabelStatement(objectName string, objectType string) string {
securityLabelStr := ""
if obj.SecurityLabel != "" {
escapedLabel := utils.EscapeSingleQuotes(obj.SecurityLabel)
securityLabelStr = fmt.Sprintf("SECURITY LABEL FOR %s ON %s %s IS '%s';", obj.SecurityLabelProvider, objectType, objectName, escapedLabel)
}
return securityLabelStr
}
func PrintDefaultPrivilegesStatements(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, privileges []DefaultPrivileges) {
for _, priv := range privileges {
statements := make([]string, 0)
roleStr := ""
if priv.Owner != "" {
roleStr = fmt.Sprintf(" FOR ROLE %s", priv.Owner)
}
schemaStr := ""
if priv.Schema != "" {
schemaStr = fmt.Sprintf(" IN SCHEMA %s", priv.Schema)
}
objectType := ""
switch priv.ObjectType {
case "r":
objectType = toc.OBJ_TABLE
case "S":
objectType = toc.OBJ_SEQUENCE
case "f":
objectType = toc.OBJ_FUNCTION
case "T":
objectType = toc.OBJ_TYPE
}
alterPrefix := fmt.Sprintf("ALTER DEFAULT PRIVILEGES%s%s", roleStr, schemaStr)
statements = append(statements, fmt.Sprintf("%s REVOKE ALL ON %sS FROM PUBLIC;", alterPrefix, objectType))
if priv.Owner != "" {
statements = append(statements, fmt.Sprintf("%s REVOKE ALL ON %sS FROM %s;", alterPrefix, objectType, priv.Owner))
}
for _, acl := range priv.Privileges {
grantee := ""
if acl.Grantee == "" {
grantee = "PUBLIC"
} else {
grantee = acl.Grantee
}
privStr, privWithGrantStr := createPrivilegeStrings(acl, objectType)
if privStr != "" {
statements = append(statements, fmt.Sprintf("%s GRANT %s ON %sS TO %s;", alterPrefix, privStr, objectType, grantee))
}
if privWithGrantStr != "" {
statements = append(statements, fmt.Sprintf("%s GRANT %s ON %sS TO %s WITH GRANT OPTION;", alterPrefix, privWithGrantStr, objectType, grantee))
}
}
start := metadataFile.ByteCount
metadataFile.MustPrintln("\n\n" + strings.Join(statements, "\n"))
section, entry := priv.GetMetadataEntry()
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
}
func ConstructDefaultPrivileges(results []DefaultPrivilegesQueryStruct) []DefaultPrivileges {
if len(results) == 0 {
return []DefaultPrivileges{}
}
defaultPrivileges := make([]DefaultPrivileges, 0)
var priv DefaultPrivileges
var currentOid uint32
for _, result := range results {
privilegesStr := ""
if result.Kind == "Empty" {
privilegesStr = "GRANTEE=/GRANTOR"
} else if result.Privileges.Valid {
privilegesStr = result.Privileges.String
}
if result.Oid != currentOid {
if currentOid != 0 {
priv.Privileges = sortACLs(priv.Privileges)
defaultPrivileges = append(defaultPrivileges, priv)
}
currentOid = result.Oid
priv = DefaultPrivileges{}
priv.Privileges = make([]ACL, 0)
priv.Owner = result.Owner
priv.Schema = result.Schema
priv.ObjectType = result.ObjectType
}
privileges := ParseACL(privilegesStr)
if privileges != nil {
priv.Privileges = append(priv.Privileges, *privileges)
}
}
priv.Privileges = sortACLs(priv.Privileges)
defaultPrivileges = append(defaultPrivileges, priv)
return defaultPrivileges
}