diff --git a/src/PSProxmoxVE.Core/Models/Users/PvePermission.cs b/src/PSProxmoxVE.Core/Models/Users/PvePermission.cs index d619097..d1fbdf9 100644 --- a/src/PSProxmoxVE.Core/Models/Users/PvePermission.cs +++ b/src/PSProxmoxVE.Core/Models/Users/PvePermission.cs @@ -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; } + /// + /// The privileges granted on , 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. + /// + public IReadOnlyDictionary? Privileges { get; set; } + /// public override string ToString() { diff --git a/src/PSProxmoxVE.Core/Services/UserService.cs b/src/PSProxmoxVE.Core/Services/UserService.cs index e677900..5c07f32 100644 --- a/src/PSProxmoxVE.Core/Services/UserService.cs +++ b/src/PSProxmoxVE.Core/Services/UserService.cs @@ -623,7 +623,15 @@ namespace PSProxmoxVE.Core.Services var result = new List(); 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() + : p.Value.Type == JTokenType.Integer && p.Value.Value() != 0, + StringComparer.OrdinalIgnoreCase) + : null; + var perm = new PvePermission { Path = prop.Name, Privileges = privileges }; result.Add(perm); } return result.ToArray(); diff --git a/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs index c78af4d..6630dbe 100644 --- a/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs @@ -8,7 +8,8 @@ namespace PSProxmoxVE.Cmdlets.Users /// /// Lists ACL entries (permissions) in Proxmox VE. /// - /// 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. /// /// diff --git a/tests/PSProxmoxVE.Core.Tests/Services/UserServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/UserServiceTests.cs index 3f3c367..9468806 100644 --- a/tests/PSProxmoxVE.Core.Tests/Services/UserServiceTests.cs +++ b/tests/PSProxmoxVE.Core.Tests/Services/UserServiceTests.cs @@ -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]