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