Skip to content

Commit

Permalink
Merge pull request #315 from Tinyblargon/Feature#308
Browse files Browse the repository at this point in the history
Feature: Dynamic permission checking.
  • Loading branch information
Tinyblargon committed Mar 9, 2024
2 parents 4ced342 + 60d15a6 commit 08d2943
Show file tree
Hide file tree
Showing 6 changed files with 853 additions and 9 deletions.
116 changes: 107 additions & 9 deletions proxmox/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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{}, permissions: make(map[permissionPath]privileges)}
}

return client, err_s
Expand Down Expand Up @@ -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 nil
}

// 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 {
Expand Down
44 changes: 44 additions & 0 deletions proxmox/client_test.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions proxmox/config_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 08d2943

Please sign in to comment.