using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; using PSProxmoxVE.Core.Authentication; using PSProxmoxVE.Core.Client; using PSProxmoxVE.Core.Models.Users; namespace PSProxmoxVE.Core.Services { /// /// Service for Proxmox VE access control — users, roles, and permissions (ACLs). /// public class UserService { // ------------------------------------------------------------------------- // Users // ------------------------------------------------------------------------- /// Returns all users. /// The authenticated PVE session. public PveUser[] GetUsers(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); using var client = new PveHttpClient(session); var response = client.GetAsync("access/users").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } /// Returns a single user by their user ID (e.g. "admin@pam"). /// The authenticated PVE session. /// The user ID in "username@realm" format. public PveUser GetUser(PveSession session, string userId) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); using var client = new PveHttpClient(session); var encodedId = Uri.EscapeDataString(userId); var response = client.GetAsync($"access/users/{encodedId}").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; var user = data?.ToObject() ?? new PveUser(); // The single-user endpoint may not echo back the userid if (string.IsNullOrEmpty(user.UserId)) user.UserId = userId; return user; } /// /// Creates a new user account. /// /// The authenticated PVE session. /// User ID in "username@realm" format. /// Additional fields (password, email, firstname, lastname, etc.). public void CreateUser( PveSession session, string userId, Dictionary? config = null) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); var formData = new Dictionary { ["userid"] = userId }; if (config != null) { foreach (var kvp in config) formData[kvp.Key] = kvp.Value?.ToString() ?? string.Empty; } using var client = new PveHttpClient(session); client.PostAsync("access/users", formData).GetAwaiter().GetResult(); } /// Removes a user account. /// The authenticated PVE session. /// The user ID in "username@realm" format. public void RemoveUser(PveSession session, string userId) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); using var client = new PveHttpClient(session); var encodedId = Uri.EscapeDataString(userId); client.DeleteAsync($"access/users/{encodedId}").GetAwaiter().GetResult(); } /// Updates one or more properties of an existing user. /// The authenticated PVE session. /// The user ID in "username@realm" format. /// User properties to update. public void SetUser( PveSession session, string userId, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); if (config == null) throw new ArgumentNullException(nameof(config)); using var client = new PveHttpClient(session); var encodedId = Uri.EscapeDataString(userId); var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); client.PutAsync($"access/users/{encodedId}", formData).GetAwaiter().GetResult(); } // ------------------------------------------------------------------------- // API Tokens // ------------------------------------------------------------------------- /// Returns all API tokens for the specified user. /// The authenticated PVE session. /// The user ID in "username@realm" format. public PveApiToken[] GetApiTokens(PveSession session, string userId) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); using var client = new PveHttpClient(session); var encodedId = Uri.EscapeDataString(userId); var response = client.GetAsync($"access/users/{encodedId}/token").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; var tokens = data?.ToObject() ?? Array.Empty(); foreach (var t in tokens) t.UserId = userId; return tokens; } /// /// Creates a new API token for the specified user and returns the token object, /// including the secret Value (shown only once). /// /// The authenticated PVE session. /// User ID in "username@realm" format. /// Token identifier (alphanumeric and hyphens). /// Optional description. /// Expiry as a Unix timestamp; 0 = never. /// /// When true, the token's effective permissions are intersected with the user's ACLs. /// public PveApiToken CreateApiToken( PveSession session, string userId, string tokenId, string? comment = null, long? expire = null, bool? privilegeSeparation = null) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId)); var formData = new Dictionary(); if (!string.IsNullOrEmpty(comment)) formData["comment"] = comment!; if (expire.HasValue) formData["expire"] = expire.Value.ToString(); if (privilegeSeparation.HasValue) formData["privsep"] = privilegeSeparation.Value ? "1" : "0"; using var client = new PveHttpClient(session); var encodedUser = Uri.EscapeDataString(userId); var encodedToken = Uri.EscapeDataString(tokenId); var response = client.PostAsync( $"access/users/{encodedUser}/token/{encodedToken}", formData) .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; var token = data?.ToObject() ?? new PveApiToken(); token.UserId = userId; token.TokenId = tokenId; return token; } /// Removes an API token. /// The authenticated PVE session. /// The user ID in "username@realm" format. /// The token identifier to remove. public void RemoveApiToken(PveSession session, string userId, string tokenId) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId)); using var client = new PveHttpClient(session); var encodedUser = Uri.EscapeDataString(userId); var encodedToken = Uri.EscapeDataString(tokenId); client.DeleteAsync($"access/users/{encodedUser}/token/{encodedToken}") .GetAwaiter().GetResult(); } // ------------------------------------------------------------------------- // Roles // ------------------------------------------------------------------------- /// Returns all roles. /// The authenticated PVE session. public PveRole[] GetRoles(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); using var client = new PveHttpClient(session); var response = client.GetAsync("access/roles").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } /// Creates a new role. /// The authenticated PVE session. /// Role name. /// Comma-separated list of privilege strings. public void CreateRole(PveSession session, string roleId, string? privileges = null) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(roleId)) throw new ArgumentNullException(nameof(roleId)); var formData = new Dictionary { ["roleid"] = roleId }; if (!string.IsNullOrEmpty(privileges)) formData["privs"] = privileges!; using var client = new PveHttpClient(session); client.PostAsync("access/roles", formData).GetAwaiter().GetResult(); } /// Removes a role. /// The authenticated PVE session. /// The role name to remove. public void RemoveRole(PveSession session, string roleId) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(roleId)) throw new ArgumentNullException(nameof(roleId)); using var client = new PveHttpClient(session); client.DeleteAsync($"access/roles/{Uri.EscapeDataString(roleId)}") .GetAwaiter().GetResult(); } // ------------------------------------------------------------------------- // Permissions / ACLs // ------------------------------------------------------------------------- /// /// Returns the effective permissions (resolved privilege set) for a user or token. /// /// The authenticated PVE session. /// Optional user ID to query; defaults to the authenticated user. /// Optional path to restrict the query. public PvePermission[] GetPermissions( PveSession session, string? userId = null, string? path = null) { if (session == null) throw new ArgumentNullException(nameof(session)); var resource = "access/permissions"; var queryParts = new List(); if (!string.IsNullOrEmpty(userId)) queryParts.Add($"userid={Uri.EscapeDataString(userId!)}"); if (!string.IsNullOrEmpty(path)) queryParts.Add($"path={Uri.EscapeDataString(path!)}"); if (queryParts.Count > 0) resource += "?" + string.Join("&", queryParts); using var client = new PveHttpClient(session); var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; // /access/permissions returns an object keyed by path, not an array if (data == null) return Array.Empty(); if (data.Type == JTokenType.Array) return data.ToObject() ?? Array.Empty(); // Unwrap path-keyed object into flat list var result = new List(); foreach (var prop in ((JObject)data).Properties()) { var perm = new PvePermission { Path = prop.Name }; result.Add(perm); } return result.ToArray(); } /// /// Sets (adds or updates) an ACL entry. /// /// The authenticated PVE session. /// Access control path (e.g. "/", "/nodes/pve", "/vms/100"). /// Comma-separated role IDs. /// Comma-separated user IDs. /// Comma-separated group names. /// Whether to propagate the permission to sub-paths. /// If true, removes the specified ACL entries. public void SetPermission( PveSession session, string path, string roles, string? users = null, string? groups = null, bool propagate = true, bool delete = false) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path)); if (string.IsNullOrWhiteSpace(roles)) throw new ArgumentNullException(nameof(roles)); var formData = new Dictionary { ["path"] = path, ["roles"] = roles, ["propagate"] = propagate ? "1" : "0", ["delete"] = delete ? "1" : "0" }; if (!string.IsNullOrEmpty(users)) formData["users"] = users!; if (!string.IsNullOrEmpty(groups)) formData["groups"] = groups!; using var client = new PveHttpClient(session); client.PutAsync("access/acl", formData).GetAwaiter().GetResult(); } } }