// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using EpicGames.Horde.Accounts; using EpicGames.Horde.Server; using HordeServer.Server; using HordeServer.Users; using HordeServer.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace HordeServer.Accounts { /// /// Controller for the /api/v1/accounts endpoint /// [ApiController] [Authorize] public class AccountsController : HordeControllerBase { readonly IAccountCollection _accountCollection; readonly GlobalConfig _globalConfig; readonly IOptionsMonitor _settings; /// /// Constructor /// public AccountsController(IAccountCollection accountCollection, IOptionsSnapshot globalConfig, IOptionsMonitor settings) { _accountCollection = accountCollection; _globalConfig = globalConfig.Value; _settings = settings; } /// /// Create a new account /// [HttpPost] [Route("/api/v1/accounts")] [ProducesResponseType(typeof(CreateAccountResponse), 200)] public async Task> CreateAccountAsync([FromBody] CreateAccountRequest request, CancellationToken cancellationToken = default) { if (!_globalConfig.Authorize(AccountAclAction.CreateAccount, User)) { return Forbid(AccountAclAction.CreateAccount); } List claims = request.Claims.ConvertAll(x => new UserClaim(x.Type, x.Value)); IAccount account = await _accountCollection.CreateAsync(new CreateAccountOptions(request.Name, request.Login, claims, request.Description, request.Email, request.Password, request.Enabled), cancellationToken); return new CreateAccountResponse(account.Id); } /// /// Gets a list of accounts /// [HttpGet] [Route("/api/v1/accounts")] [ProducesResponseType(typeof(List), 200)] public async Task>> FindAccountsAsync([FromQuery] int? index = null, [FromQuery] int? count = null, CancellationToken cancellationToken = default) { if (!_globalConfig.Authorize(AccountAclAction.ViewAccount, User)) { return Forbid(AccountAclAction.ViewAccount); } List responses = new List(); IReadOnlyList accounts = await _accountCollection.FindAsync(index, count, cancellationToken); foreach (IAccount account in accounts) { responses.Add(CreateGetAccountResponse(account)); } return responses; } /// /// Gets information about the current account /// [HttpGet] [Route("/api/v1/accounts/current")] [ProducesResponseType(typeof(GetAccountResponse), 200)] [ProducesResponseType(404)] public async Task> GetCurrentAccountAsync([FromQuery] PropertyFilter? filter = null, CancellationToken cancellationToken = default) { AccountId? accountId = User.GetAccountId(); if (accountId == null) { return BadRequest("User is not logged in through a Horde account"); } IAccount? account = await _accountCollection.GetAsync(accountId.Value, cancellationToken); if (account == null) { return NotFound(accountId.Value); } GetAccountResponse response = CreateGetAccountResponse(account); return PropertyFilter.Apply(response, filter); } /// /// Gets information about the current account /// [HttpPut] [Route("/api/v1/accounts/current")] [ProducesResponseType(200)] [ProducesResponseType(404)] public async Task UpdateCurrentAccountAsync(UpdateCurrentAccountRequest request, CancellationToken cancellationToken = default) { AccountId? accountId = User.GetAccountId(); if (accountId == null) { return BadRequest("User is not logged in through a Horde account"); } for (; ; ) { IAccount? account = await _accountCollection.GetAsync(accountId.Value, cancellationToken); if (account == null) { return NotFound(accountId.Value); } if (request.NewPassword != null && !account.ValidatePassword(request.OldPassword ?? String.Empty)) { return Unauthorized($"Invalid password for user"); } account = await account.TryUpdateAsync(new UpdateAccountOptions { Password = request.NewPassword }, cancellationToken); if (account != null) { break; } } return Ok(); } /// /// Gets information about an account by id /// [HttpGet] [Route("/api/v1/accounts/{id}")] [ProducesResponseType(typeof(GetAccountResponse), 200)] [ProducesResponseType(404)] public async Task> GetAccountAsync(AccountId id, [FromQuery] PropertyFilter? filter = null, CancellationToken cancellationToken = default) { if (!_globalConfig.Authorize(AccountAclAction.ViewAccount, User)) { return Forbid(AccountAclAction.ViewAccount); } IAccount? account = await _accountCollection.GetAsync(id, cancellationToken); if (account == null) { return NotFound(id); } GetAccountResponse response = CreateGetAccountResponse(account); return PropertyFilter.Apply(response, filter); } /// /// Gets information about the current account /// [HttpGet] [Route("/api/v1/accounts/{id}/entitlements")] [ProducesResponseType(typeof(GetAccountEntitlementsResponse), 200)] [ProducesResponseType(404)] public async Task> GetAccountEntitlementsAsync(AccountId id, [FromQuery] PropertyFilter? filter = null, CancellationToken cancellationToken = default) { if (!_globalConfig.Authorize(AccountAclAction.ViewAccount, User)) { return Forbid(AccountAclAction.ViewAccount); } IAccount? account = await _accountCollection.GetAsync(id, cancellationToken); if (account == null) { return NotFound(id); } GetAccountEntitlementsResponse response = AccountController.CreateGetAccountEntitlementsResponse(_globalConfig.Acl, claim => account.HasClaim(claim)); return PropertyFilter.Apply(response, filter); } /// /// Updates an account by id /// [HttpPut] [Route("/api/v1/accounts/{id}")] [ProducesResponseType(200)] [ProducesResponseType(404)] public async Task UpdateAccountAsync(AccountId id, UpdateAccountRequest request, CancellationToken cancellationToken = default) { if (!_globalConfig.Authorize(AccountAclAction.UpdateAccount, User)) { return Forbid(AccountAclAction.UpdateAccount); } IAccount? account = await _accountCollection.GetAsync(id, cancellationToken); if (account == null) { return NotFound(id); } IReadOnlyList? claims = null; if (request.Claims != null) { claims = request.Claims.ConvertAll(x => new UserClaim(x.Type, x.Value)); } UpdateAccountOptions options = new UpdateAccountOptions(request.Name, request.Login, claims, request.Description, request.Email, request.Password, request.Enabled); account = await account.UpdateAsync(options, cancellationToken); if (account == null) { return NotFound(id); } return Ok(); } /// /// Deletes an account by id /// [HttpDelete] [Route("/api/v1/accounts/{id}")] [ProducesResponseType(200)] public async Task DeleteAccountAsync(AccountId id, CancellationToken cancellationToken = default) { if (!_globalConfig.Authorize(AccountAclAction.DeleteAccount, User)) { return Forbid(AccountAclAction.DeleteAccount); } await _accountCollection.DeleteAsync(id, cancellationToken); return Ok(); } /// /// Gets information about the current account /// [HttpPost] [AllowAnonymous] [Route("/api/v1/accounts/admin/create")] public async Task CreateAdminAccountAsync(CreateAdminAccountRequest request, CancellationToken cancellationToken = default) { if (_settings.CurrentValue.AuthMethod != AuthMethod.Horde) { return NotFound(); } IAccount? account = await _accountCollection.FindByLoginAsync("Admin", cancellationToken); if (account != null) { return Forbid("Admin account already exists"); } await _accountCollection.CreateAdminAccountAsync(request.Password, cancellationToken); account = await _accountCollection.FindByLoginAsync("Admin", cancellationToken); if (account != null) { return Ok(new CreateAccountResponse(account.Id)); } return Forbid("Unable to find Admin account after creation"); } static GetAccountResponse CreateGetAccountResponse(IAccount account) { List claims = new List(); foreach (IUserClaim claim in account.Claims) { claims.Add(new AccountClaimMessage(claim.Type, claim.Value)); } return new GetAccountResponse(account.Id, account.Name, account.Login, claims, account.Description, account.Email, account.Enabled); } } }