From 7d7642c88de79aad35796ba8bc6ce3de7b92c9b1 Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Tue, 5 Mar 2024 17:27:14 +0100 Subject: [PATCH 1/4] feat: permission checking framework for checking if the user has the right permissons to do a given operation. --- proxmox/client.go | 116 ++++++- proxmox/client_test.go | 44 +++ proxmox/permission.go | 651 +++++++++++++++++++++++++++++++++++++ proxmox/permission_test.go | 39 +++ proxmox/util.go | 9 + 5 files changed, 850 insertions(+), 9 deletions(-) create mode 100644 proxmox/permission.go create mode 100644 proxmox/permission_test.go diff --git a/proxmox/client.go b/proxmox/client.go index ce9c5321..f298d95b 100644 --- a/proxmox/client.go +++ b/proxmox/client.go @@ -27,14 +27,16 @@ const exitStatusSuccess = "OK" // Client - URL, user and password to specific Proxmox node type Client struct { - session *Session - ApiUrl string - Username string - Password string - Otp string - TaskTimeout int - version *Version - versionMutex *sync.Mutex + session *Session + ApiUrl string + Username string + Password string + Otp string + TaskTimeout int + permissionMutex *sync.Mutex + permissions map[permissionPath]privileges + version *Version + versionMutex *sync.Mutex } const ( @@ -112,7 +114,7 @@ func NewClient(apiUrl string, hclient *http.Client, http_headers string, tls *tl return nil, err } if err_s == nil { - client = &Client{session: sess, ApiUrl: apiUrl, TaskTimeout: taskTimeout, versionMutex: &sync.Mutex{}} + client = &Client{session: sess, ApiUrl: apiUrl, TaskTimeout: taskTimeout, versionMutex: &sync.Mutex{}, permissionMutex: &sync.Mutex{}} } return client, err_s @@ -2227,6 +2229,102 @@ func (c *Client) CheckTask(resp *http.Response) (exitStatus string, err error) { return c.WaitForCompletion(taskResponse) } +// return a list of requested permissions from the cache for further processing +func (c *Client) cachedPermissions(paths []permissionPath) (map[permissionPath]privileges, error) { + c.permissionMutex.Lock() + defer c.permissionMutex.Unlock() + if c.permissions == nil { + permissionMap, err := c.getPermissions() + if err != nil { + return nil, err + } + c.permissions = permissionMap + } + extractedPermissions := make(map[permissionPath]privileges) + for _, path := range paths { + if permission, ok := c.permissions[path]; ok { + extractedPermissions[path] = permission + } + } + return extractedPermissions, nil +} + +// Returns an error if the user does not have the required permissions on the given category and itme. +func (c *Client) CheckPermissions(perms []Permission) error { + for _, perm := range perms { + if err := perm.Validate(); err != nil { + return err + } + } + return c.checkPermissions(perms) +} + +// internal function to check permissions, does not validate input. +func (c *Client) checkPermissions(perms []Permission) error { + if c == nil { + return errors.New(Client_Error_Nil) + } + if c.Username == "root@pam" { // no permissions check for root + return nil + } + permissions, err := c.cachedPermissions(Permission{}.buildPathList(perms)) + if err != nil { + return err + } + for _, perm := range perms { + err = perm.check(permissions) + if err != nil { + return err + } + } + return nil +} + +// inserts a permission into the cache, this is useful for when we create an item, as refreshing the whole cache is quite expensive. +func (c *Client) insertCachedPermission(path permissionPath) error { + rawPermissions, err := c.getPermissionsRaw() + if err != nil { + return err + } + if rawPrivileges, ok := rawPermissions[string(path)]; ok { + privileges := privileges{}.mapToSDK(rawPrivileges.(map[string]interface{})) + c.permissionMutex.Lock() + c.permissions[path] = privileges + c.permissionMutex.Unlock() + return nil + } + return errors.New("permission not found") +} + +// get the users permissions from the cache and decodes them for the SDK +func (c *Client) getPermissions() (map[permissionPath]privileges, error) { + permissions, err := c.getPermissionsRaw() + if err != nil { + return nil, err + } + return permissionPath("").mapToSDK(permissions), nil +} + +// returns the raw permissions from the API +func (c *Client) getPermissionsRaw() (map[string]interface{}, error) { + return c.GetItemConfigMapStringInterface("/access/permissions", "", "permissions") +} + +// RefreshPermissions fetches the permissions from the API and updates the cache. +func (c *Client) RefreshPermissions() error { + if c == nil { + return errors.New(Client_Error_Nil) + } + tmpPermsissions, err := c.getPermissions() + if err != nil { + return err + } + c.permissionMutex.Lock() + c.permissions = tmpPermsissions + c.permissionMutex.Unlock() + return nil +} + // Returns the Client's cached version if it exists, otherwise fetches the version from the API. func (c *Client) Version() (Version, error) { if c == nil { diff --git a/proxmox/client_test.go b/proxmox/client_test.go index dec85678..8a8bee27 100644 --- a/proxmox/client_test.go +++ b/proxmox/client_test.go @@ -1,11 +1,55 @@ package proxmox import ( + "errors" + "sync" "testing" "github.com/stretchr/testify/require" ) +func Test_Client_CheckPermissions(t *testing.T) { + pointerClient := func(c Client) *Client { + c.permissionMutex = &sync.Mutex{} + return &c + } + type input struct { + client *Client + perms []Permission + } + tests := []struct { + name string + input input + output error + }{ + {"nil client", input{nil, []Permission{}}, errors.New(Client_Error_Nil)}, + {"user root@pam", input{pointerClient(Client{Username: "root@pam"}), []Permission{}}, nil}, + {name: "direct permissions", + input: input{pointerClient(Client{permissions: map[permissionPath]privileges{ + "/access/pve": {UserModify: privilegeTrue}, + }}), []Permission{ + {Category: PermissionCategory_Access, Item: "pve", Privileges: Privileges{UserModify: true}}}}}, + {name: "propagate permissions", + input: input{pointerClient(Client{permissions: map[permissionPath]privileges{ + "/access": {UserModify: privilegePropagate}, + }}), []Permission{ + {Category: PermissionCategory_Access, Item: "pve", Privileges: Privileges{UserModify: true}}}}}, + {name: "missing permissions", + input: input{pointerClient(Client{permissions: map[permissionPath]privileges{ + "/": {UserModify: privilegeTrue}, + }}), []Permission{ + {Category: PermissionCategory_Root, Privileges: Privileges{PoolAllocate: true}}, + }}, + output: Permission{Category: PermissionCategory_Root, Privileges: Privileges{PoolAllocate: true}}.error(), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.client.CheckPermissions(test.input.perms)) + }) + } +} + func Test_Version_Greater(t *testing.T) { type input struct { a Version diff --git a/proxmox/permission.go b/proxmox/permission.go new file mode 100644 index 00000000..6052313c --- /dev/null +++ b/proxmox/permission.go @@ -0,0 +1,651 @@ +package proxmox + +import ( + "errors" + "strings" +) + +const ( + PermissionErrorPrefix string = "permission error:" // Check if an error starts with this to see if it's a permission error. +) + +const ( + key_Privileges_DatastoreAllocate string = "Datastore.Allocate" + key_Privileges_DatastoreAllocateSpace string = "Datastore.AllocateSpace" + key_Privileges_DatastoreAllocateTemplate string = "Datastore.AllocateTemplate" + key_Privileges_DatastoreAudit string = "Datastore.Audit" + key_Privileges_GroupAllocate string = "Group.Allocate" + key_Privileges_PermissionsModify string = "Permissions.Modify" + key_Privileges_PoolAllocate string = "Pool.Allocate" + key_Privileges_PoolAudit string = "Pool.Audit" + key_Privileges_RealmAllocate string = "Realm.Allocate" + key_Privileges_RealmAllocateUser string = "Realm.AllocateUser" + key_Privileges_SDNAllocate string = "SDN.Allocate" + key_Privileges_SDNAudit string = "SDN.Audit" + key_Privileges_SysAudit string = "Sys.Audit" + key_Privileges_SysConsole string = "Sys.Console" + key_Privileges_SysIncoming string = "Sys.Incoming" + key_Privileges_SysModify string = "Sys.Modify" + key_Privileges_SysPowerMgmt string = "Sys.PowerMgmt" + key_Privileges_SysSyslog string = "Sys.Syslog" + key_Privileges_UserModify string = "User.Modify" + key_Privileges_VMAllocate string = "VM.Allocate" + key_Privileges_VMAudit string = "VM.Audit" + key_Privileges_VMBackup string = "VM.Backup" + key_Privileges_VMClone string = "VM.Clone" + key_Privileges_VMConfigCDROM string = "VM.Config.CDROM" + key_Privileges_VMConfigCPU string = "VM.Config.CPU" + key_Privileges_VMConfigCloudinit string = "VM.Config.Cloudinit" + key_Privileges_VMConfigDisk string = "VM.Config.Disk" + key_Privileges_VMConfigHWType string = "VM.Config.HWType" + key_Privileges_VMConfigMemory string = "VM.Config.Memory" + key_Privileges_VMConfigNetwork string = "VM.Config.Network" + key_Privileges_VMConfigOptions string = "VM.Config.Options" + key_Privileges_VMConsole string = "VM.Console" + key_Privileges_VMMigrate string = "VM.Migrate" + key_Privileges_VMMonitor string = "VM.Monitor" + key_Privileges_VMPowerMgmt string = "VM.PowerMgmt" + key_Privileges_VMSnapshot string = "VM.Snapshot" + key_Privileges_VMSnapshotRollback string = "VM.Snapshot.Rollback" +) + +type Permission struct { + Category PermissionCategory + Item PermissionItem + Privileges Privileges +} + +// build a list of unique paths from the permissions. +func (Permission) buildPathList(perms []Permission) (paths []permissionPath) { + paths = make([]permissionPath, 0) + for _, perm := range perms { + categoryPath := perm.Category.path() + var skipPath bool + for _, path := range paths { + if path == categoryPath { + skipPath = true + break + } + } + if !skipPath { + paths = append(paths, categoryPath) + } + if perm.Item != permissionItemEmpty { + paths = append(paths, categoryPath.append(perm.Item)) + } + } + return +} + +// checks if the permissions map contains the required permissions. +func (p Permission) check(permissions map[permissionPath]privileges) error { + categoryPath := p.Category.path() + if p.Item != permissionItemEmpty { + if v, ok := permissions[categoryPath.append(p.Item)]; ok { + if !v.includes(p.Privileges, privilegeTrue) { + return Permission{Category: p.Category, Item: p.Item, Privileges: p.Privileges}.error() + } + } + } + if v, ok := permissions[categoryPath]; ok { + if !v.includes(p.Privileges, privilegePropagate) { + return Permission{Category: p.Category, Item: p.Item, Privileges: p.Privileges}.error() + } + } + return nil +} + +func (p Permission) error() error { + return errors.New(PermissionErrorPrefix + " the following privileges (" + p.Privileges.String() + ") are missing from path (" + string(p.Category.path().append(p.Item)) + ")") +} + +func (p Permission) Validate() error { + return p.Category.Validate() +} + +type PermissionCategory string // Enum + +const ( + PermissionCategory_Root PermissionCategory = "root" + permissionCategory_RootPath PermissionCategory = "/" + PermissionCategory_Access PermissionCategory = "access" + permissionCategory_AccessPath PermissionCategory = "/access" + PermissionCategory_Group PermissionCategory = "group" + permissionCategory_GroupPath PermissionCategory = "/access/groups" + PermissionCategory_Realm PermissionCategory = "realm" + permissionCategory_RealmPath PermissionCategory = "/access/realm" + PermissionCategory_Node PermissionCategory = "node" + permissionCategory_NodePath PermissionCategory = "/nodes" + PermissionCategory_Guest PermissionCategory = "guest" + permissionCategory_GuestPath PermissionCategory = "/vms" + PermissionCategory_Pool PermissionCategory = "pool" + permissionCategory_PoolPath PermissionCategory = "/pool" + PermissionCategory_Storage PermissionCategory = "storage" + permissionCategory_StoragePath PermissionCategory = "/storage" + PermissionCategory_Zone PermissionCategory = "zone" + permissionCategory_ZonePath PermissionCategory = "/sdn/zones" +) + +func (PermissionCategory) Error() error { + return errors.New("permission category should be one of (" + strings.Join(arrayToStringArray(PermissionCategory("").enumArray()), ",") + ")") +} + +func (PermissionCategory) enumArray() []PermissionCategory { + return []PermissionCategory{PermissionCategory_Root, PermissionCategory_Access, PermissionCategory_Group, PermissionCategory_Realm, PermissionCategory_Node, PermissionCategory_Guest, PermissionCategory_Pool, PermissionCategory_Storage, PermissionCategory_Zone} +} + +// returns the path for the category. +// a raw path may be provided, in which case it will be returned as is. +func (c PermissionCategory) path() permissionPath { + if len(c) > 0 && c[0] == '/' { + return permissionPath(c) + } + switch c { + case PermissionCategory_Access: + return "/access" + case PermissionCategory_Group: + return "/access/groups" + case PermissionCategory_Guest: + return "/vms" + case PermissionCategory_Node: + return "/nodes" + case PermissionCategory_Pool: + return "/pool" + case PermissionCategory_Realm: + return "/access/realm" + case PermissionCategory_Root: + return "/" + case PermissionCategory_Storage: + return "/storage" + case PermissionCategory_Zone: + return "/sdn/zones" + } + return "" +} + +func (c PermissionCategory) String() string { + return string(c) +} + +func (c PermissionCategory) Validate() error { + for _, e := range c.enumArray() { + if c == e { + return nil + } + } + return PermissionCategory("").Error() +} + +type PermissionItem string + +const ( + permissionItemEmpty PermissionItem = "" +) + +type permissionPath string + +func (p permissionPath) append(item PermissionItem) permissionPath { + if item == "" { + return p + } + if p == "/" { + return p + permissionPath(item) + } + return p + "/" + permissionPath(item) +} + +func (permissionPath) mapToSDK(params map[string]interface{}) map[permissionPath]privileges { + permissions := make(map[permissionPath]privileges) + for key, e := range params { + permissions[permissionPath(key)] = privileges{}.mapToSDK(e.(map[string]interface{})) + } + return permissions +} + +type privilege int8 // Enum + +const ( + privilegeFalse privilege = 0 + privilegeTrue privilege = 1 + privilegePropagate privilege = 2 +) + +func (privilege) extract(i interface{}) privilege { + number, isFloat64 := i.(float64) + if !isFloat64 { + return privilegeFalse + } + if int(number) == 1 { + return privilegePropagate + } + return privilegeTrue +} + +type Privileges struct { + DatastoreAllocate bool `json:"Datastore.Allocate,omitempty"` + DatastoreAllocateSpace bool `json:"Datastore.AllocateSpace,omitempty"` + DatastoreAllocateTemplate bool `json:"Datastore.AllocateTemplate,omitempty"` + DatastoreAudit bool `json:"Datastore.Audit,omitempty"` + GroupAllocate bool `json:"Group.Allocate,omitempty"` + PermissionsModify bool `json:"Permissions.Modify,omitempty"` + PoolAllocate bool `json:"Pool.Allocate,omitempty"` + PoolAudit bool `json:"Pool.Audit,omitempty"` + RealmAllocate bool `json:"Realm.Allocate,omitempty"` + RealmAllocateUser bool `json:"Realm.AllocateUser,omitempty"` + SDNAllocate bool `json:"SDN.Allocate,omitempty"` + SDNAudit bool `json:"SDN.Audit,omitempty"` + SysAudit bool `json:"Sys.Audit,omitempty"` + SysConsole bool `json:"Sys.Console,omitempty"` + SysIncoming bool `json:"Sys.Incoming,omitempty"` + SysModify bool `json:"Sys.Modify,omitempty"` + SysPowerMgmt bool `json:"Sys.PowerMgmt,omitempty"` + SysSyslog bool `json:"Sys.Syslog,omitempty"` + UserModify bool `json:"User.Modify,omitempty"` + VMAllocate bool `json:"VM.Allocate,omitempty"` + VMAudit bool `json:"VM.Audit,omitempty"` + VMBackup bool `json:"VM.Backup,omitempty"` + VMClone bool `json:"VM.Clone,omitempty"` + VMConfigCDROM bool `json:"VM.Config.CDROM,omitempty"` + VMConfigCPU bool `json:"VM.Config.CPU,omitempty"` + VMConfigCloudinit bool `json:"VM.Config.Cloudinit,omitempty"` + VMConfigDisk bool `json:"VM.Config.Disk,omitempty"` + VMConfigHWType bool `json:"VM.Config.HWType,omitempty"` + VMConfigMemory bool `json:"VM.Config.Memory,omitempty"` + VMConfigNetwork bool `json:"VM.Config.Network,omitempty"` + VMConfigOptions bool `json:"VM.Config.Options,omitempty"` + VMConsole bool `json:"VM.Console,omitempty"` + VMMigrate bool `json:"VM.Migrate,omitempty"` + VMMonitor bool `json:"VM.Monitor,omitempty"` + VMPowerMgmt bool `json:"VM.PowerMgmt,omitempty"` + VMSnapshot bool `json:"VM.Snapshot,omitempty"` + VMSnapshotRollback bool `json:"VM.Snapshot.Rollback,omitempty"` +} + +func (p Privileges) String() (privileges string) { + if p.DatastoreAllocate { + privileges += key_Privileges_DatastoreAllocate + ", " + } + if p.DatastoreAllocateSpace { + privileges += key_Privileges_DatastoreAllocateSpace + ", " + } + if p.DatastoreAllocateTemplate { + privileges += key_Privileges_DatastoreAllocateTemplate + ", " + } + if p.DatastoreAudit { + privileges += key_Privileges_DatastoreAudit + ", " + } + if p.GroupAllocate { + privileges += key_Privileges_GroupAllocate + ", " + } + if p.PermissionsModify { + privileges += key_Privileges_PermissionsModify + ", " + } + if p.PoolAllocate { + privileges += key_Privileges_PoolAllocate + ", " + } + if p.PoolAudit { + privileges += key_Privileges_PoolAudit + ", " + } + if p.RealmAllocate { + privileges += key_Privileges_RealmAllocate + ", " + } + if p.RealmAllocateUser { + privileges += key_Privileges_RealmAllocateUser + ", " + } + if p.SDNAllocate { + privileges += key_Privileges_SDNAllocate + ", " + } + if p.SDNAudit { + privileges += key_Privileges_SDNAudit + ", " + } + if p.SysAudit { + privileges += key_Privileges_SysAudit + ", " + } + if p.SysConsole { + privileges += key_Privileges_SysConsole + ", " + } + if p.SysIncoming { + privileges += key_Privileges_SysIncoming + ", " + } + if p.SysModify { + privileges += key_Privileges_SysModify + ", " + } + if p.SysPowerMgmt { + privileges += key_Privileges_SysPowerMgmt + ", " + } + if p.SysSyslog { + privileges += key_Privileges_SysSyslog + ", " + } + if p.UserModify { + privileges += key_Privileges_UserModify + ", " + } + if p.VMAllocate { + privileges += key_Privileges_VMAllocate + ", " + } + if p.VMAudit { + privileges += key_Privileges_VMAudit + ", " + } + if p.VMBackup { + privileges += key_Privileges_VMBackup + ", " + } + if p.VMClone { + privileges += key_Privileges_VMClone + ", " + } + if p.VMConfigCDROM { + privileges += key_Privileges_VMConfigCDROM + ", " + } + if p.VMConfigCPU { + privileges += key_Privileges_VMConfigCPU + ", " + } + if p.VMConfigCloudinit { + privileges += key_Privileges_VMConfigCloudinit + ", " + } + if p.VMConfigDisk { + privileges += key_Privileges_VMConfigDisk + ", " + } + if p.VMConfigHWType { + privileges += key_Privileges_VMConfigHWType + ", " + } + if p.VMConfigMemory { + privileges += key_Privileges_VMConfigMemory + ", " + } + if p.VMConfigNetwork { + privileges += key_Privileges_VMConfigNetwork + ", " + } + if p.VMConfigOptions { + privileges += key_Privileges_VMConfigOptions + ", " + } + if p.VMConsole { + privileges += key_Privileges_VMConsole + ", " + } + if p.VMMigrate { + privileges += key_Privileges_VMMigrate + ", " + } + if p.VMMonitor { + privileges += key_Privileges_VMMonitor + ", " + } + if p.VMPowerMgmt { + privileges += key_Privileges_VMPowerMgmt + ", " + } + if p.VMSnapshot { + privileges += key_Privileges_VMSnapshot + ", " + } + if p.VMSnapshotRollback { + privileges += key_Privileges_VMSnapshotRollback + ", " + } + if privileges != "" { + privileges = privileges[:len(privileges)-2] + } + return +} + +// internal struct to map the privileges to the SDK. +type privileges struct { + DatastoreAllocate privilege + DatastoreAllocateSpace privilege + DatastoreAllocateTemplate privilege + DatastoreAudit privilege + GroupAllocate privilege + PermissionsModify privilege + PoolAllocate privilege + PoolAudit privilege + RealmAllocate privilege + RealmAllocateUser privilege + SDNAllocate privilege + SDNAudit privilege + SysAudit privilege + SysConsole privilege + SysIncoming privilege + SysModify privilege + SysPowerMgmt privilege + SysSyslog privilege + UserModify privilege + VMAllocate privilege + VMAudit privilege + VMBackup privilege + VMClone privilege + VMConfigCDROM privilege + VMConfigCPU privilege + VMConfigCloudinit privilege + VMConfigDisk privilege + VMConfigHWType privilege + VMConfigMemory privilege + VMConfigNetwork privilege + VMConfigOptions privilege + VMConsole privilege + VMMigrate privilege + VMMonitor privilege + VMPowerMgmt privilege + VMSnapshot privilege + VMSnapshotRollback privilege +} + +func (p privileges) includes(needed Privileges, number privilege) bool { + if needed.DatastoreAllocate && (p.DatastoreAllocate < number) { + return false + } + if needed.DatastoreAllocateSpace && (p.DatastoreAllocateSpace < number) { + return false + } + if needed.DatastoreAllocateTemplate && (p.DatastoreAllocateTemplate < number) { + return false + } + if needed.DatastoreAudit && (p.DatastoreAudit < number) { + return false + } + if needed.GroupAllocate && (p.GroupAllocate < number) { + return false + } + if needed.PermissionsModify && (p.PermissionsModify < number) { + return false + } + if needed.PoolAllocate && (p.PoolAllocate < number) { + return false + } + if needed.PoolAudit && (p.PoolAudit < number) { + return false + } + if needed.RealmAllocate && (p.RealmAllocate < number) { + return false + } + if needed.RealmAllocateUser && (p.RealmAllocateUser < number) { + return false + } + if needed.SDNAllocate && (p.SDNAllocate < number) { + return false + } + if needed.SDNAudit && (p.SDNAudit < number) { + return false + } + if needed.SysAudit && (p.SysAudit < number) { + return false + } + if needed.SysConsole && (p.SysConsole < number) { + return false + } + if needed.SysIncoming && (p.SysIncoming < number) { + return false + } + if needed.SysModify && (p.SysModify < number) { + return false + } + if needed.SysPowerMgmt && (p.SysPowerMgmt < number) { + return false + } + if needed.SysSyslog && (p.SysSyslog < number) { + return false + } + if needed.UserModify && (p.UserModify < number) { + return false + } + if needed.VMAllocate && (p.VMAllocate < number) { + return false + } + if needed.VMAudit && (p.VMAudit < number) { + return false + } + if needed.VMBackup && (p.VMBackup < number) { + return false + } + if needed.VMClone && (p.VMClone < number) { + return false + } + if needed.VMConfigCDROM && (p.VMConfigCDROM < number) { + return false + } + if needed.VMConfigCPU && (p.VMConfigCPU < number) { + return false + } + if needed.VMConfigCloudinit && (p.VMConfigCloudinit < number) { + return false + } + if needed.VMConfigDisk && (p.VMConfigDisk < number) { + return false + } + if needed.VMConfigHWType && (p.VMConfigHWType < number) { + return false + } + if needed.VMConfigMemory && (p.VMConfigMemory < number) { + return false + } + if needed.VMConfigNetwork && (p.VMConfigNetwork < number) { + return false + } + if needed.VMConfigOptions && (p.VMConfigOptions < number) { + return false + } + if needed.VMConsole && (p.VMConsole < number) { + return false + } + if needed.VMMigrate && (p.VMMigrate < number) { + return false + } + if needed.VMMonitor && (p.VMMonitor < number) { + return false + } + if needed.VMPowerMgmt && (p.VMPowerMgmt < number) { + return false + } + if needed.VMSnapshot && (p.VMSnapshot < number) { + return false + } + if needed.VMSnapshotRollback && (p.VMSnapshotRollback < number) { + return false + } + return true +} + +func (privileges) mapToSDK(params map[string]interface{}) (p privileges) { + if v, isSet := params[key_Privileges_DatastoreAllocate]; isSet { + p.DatastoreAllocate = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_DatastoreAllocateSpace]; isSet { + p.DatastoreAllocateSpace = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_DatastoreAllocateTemplate]; isSet { + p.DatastoreAllocateTemplate = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_DatastoreAudit]; isSet { + p.DatastoreAudit = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_GroupAllocate]; isSet { + p.GroupAllocate = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_PermissionsModify]; isSet { + p.PermissionsModify = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_PoolAllocate]; isSet { + p.PoolAllocate = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_PoolAudit]; isSet { + p.PoolAudit = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_RealmAllocate]; isSet { + p.RealmAllocate = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_RealmAllocateUser]; isSet { + p.RealmAllocateUser = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_SDNAllocate]; isSet { + p.SDNAllocate = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_SDNAudit]; isSet { + p.SDNAudit = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_SysAudit]; isSet { + p.SysAudit = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_SysConsole]; isSet { + p.SysConsole = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_SysIncoming]; isSet { + p.SysIncoming = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_SysModify]; isSet { + p.SysModify = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_SysPowerMgmt]; isSet { + p.SysPowerMgmt.extract(v) + } + if v, isSet := params[key_Privileges_SysSyslog]; isSet { + p.SysSyslog = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_UserModify]; isSet { + p.UserModify = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMAllocate]; isSet { + p.VMAllocate = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMAudit]; isSet { + p.VMAudit = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMBackup]; isSet { + p.VMBackup = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMClone]; isSet { + p.VMClone = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMConfigCDROM]; isSet { + p.VMConfigCDROM = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMConfigCPU]; isSet { + p.VMConfigCPU = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMConfigCloudinit]; isSet { + p.VMConfigCloudinit = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMConfigDisk]; isSet { + p.VMConfigDisk = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMConfigHWType]; isSet { + p.VMConfigHWType = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMConfigMemory]; isSet { + p.VMConfigMemory = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMConfigNetwork]; isSet { + p.VMConfigNetwork = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMConfigOptions]; isSet { + p.VMConfigOptions = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMConsole]; isSet { + p.VMConsole = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMMigrate]; isSet { + p.VMMigrate = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMMonitor]; isSet { + p.VMMonitor = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMPowerMgmt]; isSet { + p.VMPowerMgmt = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMSnapshot]; isSet { + p.VMSnapshot = privilege(0).extract(v) + } + if v, isSet := params[key_Privileges_VMSnapshotRollback]; isSet { + p.VMSnapshotRollback = privilege(0).extract(v) + } + return +} diff --git a/proxmox/permission_test.go b/proxmox/permission_test.go new file mode 100644 index 00000000..c16703b8 --- /dev/null +++ b/proxmox/permission_test.go @@ -0,0 +1,39 @@ +package proxmox + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Permission_Validate(t *testing.T) { + tests := []struct { + name string + input Permission + output error + }{ + {"valid category", Permission{Category: PermissionCategory_Access}, nil}, + {"invalid category", Permission{Category: "abc"}, PermissionCategory("").Error()}, + } + for _, test := range tests { + t.Run(test.name, func(*testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_PermissionCategory_Validate(t *testing.T) { + tests := []struct { + name string + input PermissionCategory + output error + }{ + {"valid category", PermissionCategory_Access, nil}, + {"invalid category", "abc", PermissionCategory("").Error()}, + } + for _, test := range tests { + t.Run(test.name, func(*testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} diff --git a/proxmox/util.go b/proxmox/util.go index e39fefe8..bf1d7678 100644 --- a/proxmox/util.go +++ b/proxmox/util.go @@ -1,6 +1,7 @@ package proxmox import ( + "fmt" "log" "regexp" "strconv" @@ -198,6 +199,14 @@ func failError(err error) { } } +func arrayToStringArray[T fmt.Stringer](arr []T) []string { + strArr := make([]string, len(arr)) + for i, v := range arr { + strArr[i] = v.String() + } + return strArr +} + // Create list of http.Header out of string, separator is "," func createHeaderList(header_string string, sess *Session) (*Session, error) { if header_string == "" { From a8821f06d2400c2ab99ccad33088c80bd21622c4 Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Tue, 5 Mar 2024 17:59:17 +0100 Subject: [PATCH 2/4] feat: add example to update the permisson cache --- proxmox/config_qemu.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/proxmox/config_qemu.go b/proxmox/config_qemu.go index 1a0de4c6..c1423bbf 100644 --- a/proxmox/config_qemu.go +++ b/proxmox/config_qemu.go @@ -964,6 +964,9 @@ func (newConfig ConfigQemu) setAdvanced(currentConfig *ConfigQemu, rebootIfNeede if err = resizeNewDisks(vmr, client, newConfig.Disks, nil); err != nil { return } + if err = client.insertCachedPermission(permissionPath(permissionCategory_GuestPath) + "/" + permissionPath(strconv.Itoa(vmr.vmId))); err != nil { + return + } } _, err = client.UpdateVMHA(vmr, newConfig.HaState, newConfig.HaGroup) From 4b38dbf4da39d20547362602be48909b33dc607f Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:16:46 +0100 Subject: [PATCH 3/4] fix: returned error when permission does not exist --- proxmox/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxmox/client.go b/proxmox/client.go index f298d95b..09e39685 100644 --- a/proxmox/client.go +++ b/proxmox/client.go @@ -2293,7 +2293,7 @@ func (c *Client) insertCachedPermission(path permissionPath) error { c.permissionMutex.Unlock() return nil } - return errors.New("permission not found") + return nil } // get the users permissions from the cache and decodes them for the SDK From 60d15a664276b33fe11d45fe00cb8cb05534295e Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:17:52 +0100 Subject: [PATCH 4/4] fix: panic permissions nil --- proxmox/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxmox/client.go b/proxmox/client.go index 09e39685..1da3a14e 100644 --- a/proxmox/client.go +++ b/proxmox/client.go @@ -114,7 +114,7 @@ func NewClient(apiUrl string, hclient *http.Client, http_headers string, tls *tl return nil, err } if err_s == nil { - client = &Client{session: sess, ApiUrl: apiUrl, TaskTimeout: taskTimeout, versionMutex: &sync.Mutex{}, permissionMutex: &sync.Mutex{}} + client = &Client{session: sess, ApiUrl: apiUrl, TaskTimeout: taskTimeout, versionMutex: &sync.Mutex{}, permissionMutex: &sync.Mutex{}, permissions: make(map[permissionPath]privileges)} } return client, err_s