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();
}
}
}