fix: populate Privileges on PvePermission from access/permissions (#163)

UserService.GetPermissions unwrapped the path-keyed /access/permissions
response and discarded prop.Value, so every returned PvePermission had a
path but no privilege data. Add a Privileges dictionary populated from
the privilege map; a key's presence means the privilege is granted and
its value is whether the grant propagates to sub-paths, matching PVE's
documented "propagate boolean" contract for that endpoint.

Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
This commit is contained in:
goodolclint-claude[bot]
2026-09-02 13:19:53 -05:00
committed by GitHub
parent 4cc18f1cc2
commit a7d2b877f5
4 changed files with 65 additions and 5 deletions
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Users;
@@ -33,6 +34,16 @@ public class PvePermission
[JsonProperty("ugid")]
public string? UserId { get; set; }
/// <summary>
/// The privileges granted on <see cref="Path"/>, keyed by privilege name (e.g. "VM.Audit").
/// A key's presence means the privilege is granted; its value is whether that grant
/// propagates to sub-paths, per the PVE /access/permissions contract ("propagate boolean").
/// Populated only when this entry comes from the path-keyed /access/permissions response
/// with a privilege map for the path; null for entries from the flat /access/acl array,
/// or when PVE returned no privilege map for the path.
/// </summary>
public IReadOnlyDictionary<string, bool>? Privileges { get; set; }
/// <inheritdoc />
public override string ToString()
{
+9 -1
View File
@@ -623,7 +623,15 @@ namespace PSProxmoxVE.Core.Services
var result = new List<PvePermission>();
foreach (var prop in ((JObject)data).Properties())
{
var perm = new PvePermission { Path = prop.Name };
var privileges = prop.Value is JObject privMap
? privMap.Properties().ToDictionary(
p => p.Name,
p => p.Value.Type == JTokenType.Boolean
? p.Value.Value<bool>()
: p.Value.Type == JTokenType.Integer && p.Value.Value<long>() != 0,
StringComparer.OrdinalIgnoreCase)
: null;
var perm = new PvePermission { Path = prop.Name, Privileges = privileges };
result.Add(perm);
}
return result.ToArray();
@@ -8,7 +8,8 @@ namespace PSProxmoxVE.Cmdlets.Users
/// <summary>
/// <para type="synopsis">Lists ACL entries (permissions) in Proxmox VE.</para>
/// <para type="description">
/// Returns Access Control List entries from the Proxmox VE access management system.
/// Returns Access Control List entries from the Proxmox VE access management system,
/// each carrying the privileges granted on its path in the Privileges property.
/// Optionally filter by path or user/group ID.
/// </para>
/// </summary>
@@ -768,16 +768,56 @@ namespace PSProxmoxVE.Core.Tests.Services
_mockClient.Setup(c => c.GetAsync("access/permissions"))
.ReturnsAsync(@"{""data"":{
""/"":{ ""Datastore.Audit"":1, ""VM.Audit"":1 },
""/nodes/pve1"":{ ""Sys.Console"":1 }
""/vms/100"":{ ""VM.Audit"":1, ""VM.PowerMgmt"":0 }
}}");
// Act
var result = _service.GetPermissions(_session);
// Assert
// Assert — a key's presence means the privilege is granted; its value is whether
// that grant propagates to sub-paths (PVE's "propagate boolean" contract).
Assert.Equal(2, result.Length);
Assert.Contains(result, p => p.Path == "/");
Assert.Contains(result, p => p.Path == "/nodes/pve1");
var vmPerm = Assert.Single(result, p => p.Path == "/vms/100");
Assert.NotNull(vmPerm.Privileges);
Assert.True(vmPerm.Privileges!.ContainsKey("VM.Audit"));
Assert.True(vmPerm.Privileges!["VM.Audit"]);
Assert.True(vmPerm.Privileges!.ContainsKey("VM.PowerMgmt"));
Assert.False(vmPerm.Privileges!["VM.PowerMgmt"]);
}
[Fact]
public void GetPermissions_ArrayResponse_LeavesPrivilegesNull()
{
// Arrange — the flat /access/acl-shaped array response carries no privilege map
_mockClient.Setup(c => c.GetAsync("access/permissions"))
.ReturnsAsync(@"{""data"":[
{ ""path"":""/vms/100"", ""roleid"":""PVEVMAdmin"", ""ugid"":""deploy@pve"", ""propagate"":1 }
]}");
// Act
var result = _service.GetPermissions(_session);
// Assert
var perm = Assert.Single(result);
Assert.Equal("/vms/100", perm.Path);
Assert.Null(perm.Privileges);
}
[Fact]
public void GetPermissions_EmptyPrivilegeMap_YieldsNonNullEmptyDictionary()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/permissions"))
.ReturnsAsync(@"{""data"":{""/"":{}}}");
// Act
var result = _service.GetPermissions(_session);
// Assert
var perm = Assert.Single(result);
Assert.NotNull(perm.Privileges);
Assert.Empty(perm.Privileges!);
}
[Fact]