// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using EpicGames.Horde.ServiceAccounts;
using HordeServer.Acls;
using HordeServer.Server;
using HordeServer.Users;
using HordeServer.Utilities;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
namespace HordeServer.ServiceAccounts
{
///
/// Collection of service account documents
///
public class ServiceAccountCollection : IServiceAccountCollection
{
record class ClaimDocument(string Type, string Value) : IUserClaim
{
public ClaimDocument(IUserClaim claim) : this(claim.Type, claim.Value)
{ }
public ClaimDocument(AclClaimConfig claim) : this(claim.Type, claim.Value)
{ }
}
///
/// Concrete implementation of IHordeAccount
///
private class ServiceAccountDocument : IServiceAccount
{
[BsonIgnore]
ServiceAccountCollection? _accountCollection;
///
[BsonRequired, BsonId]
public ServiceAccountId Id { get; set; }
///
public string Name { get; set; } = "";
public string SecretToken { get; set; } = String.Empty;
[BsonElement("Claims2")]
public List Claims { get; set; } = new List();
///
public bool Enabled { get; set; }
///
public string Description { get; set; } = "";
[BsonIgnoreIfDefault, BsonDefaultValue(0)]
public int UpdateIndex { get; set; }
IReadOnlyList IServiceAccount.Claims => Claims;
[BsonConstructor]
private ServiceAccountDocument()
{
}
public ServiceAccountDocument(ServiceAccountId id, string name, string description)
{
Id = id;
Name = name;
Description = description;
}
public void PostLoad(ServiceAccountCollection accountCollection)
{
_accountCollection = accountCollection;
}
public async Task RefreshAsync(CancellationToken cancellationToken)
=> await _accountCollection!.GetAsync(Id, cancellationToken);
public async Task<(IServiceAccount?, string?)> TryUpdateAsync(UpdateServiceAccountOptions options, CancellationToken cancellationToken)
=> await _accountCollection!.TryUpdateAsync(this, options, cancellationToken);
}
private readonly IMongoCollection _accounts;
///
/// Constructor
///
/// The database service
public ServiceAccountCollection(IMongoService mongoService)
{
_accounts = mongoService.GetCollection("ServiceAccounts", keys => keys.Ascending(x => x.SecretToken));
}
static string CreateToken()
=> StringUtils.FormatHexString(RandomNumberGenerator.GetBytes(32));
///
public async Task<(IServiceAccount, string)> CreateAsync(CreateServiceAccountOptions options, CancellationToken cancellationToken = default)
{
string newToken = CreateToken();
ServiceAccountDocument account = new (new ServiceAccountId(BinaryIdUtils.CreateNew()), options.Name, options.Description)
{
SecretToken = newToken,
Enabled = options.Enabled ?? true,
};
if (options.Claims != null)
{
account.Claims = options.Claims.ConvertAll(x => new ClaimDocument(x));
}
await _accounts.InsertOneAsync(account, (InsertOneOptions?)null, cancellationToken);
account.PostLoad(this);
return (account, newToken);
}
///
public async Task> FindAsync(int? index = null, int? count = null, CancellationToken cancellationToken = default)
{
List accounts = new List();
await foreach (ServiceAccountDocument account in _accounts.Find(FilterDefinition.Empty).Range(index, count).ToAsyncEnumerable(cancellationToken))
{
account.PostLoad(this);
accounts.Add(account);
}
return accounts;
}
///
public async Task GetAsync(ServiceAccountId id, CancellationToken cancellationToken = default)
{
ServiceAccountDocument? account = await _accounts.Find(x => x.Id == id).FirstOrDefaultAsync(cancellationToken);
account?.PostLoad(this);
return account;
}
///
public async Task FindBySecretTokenAsync(string secretToken, CancellationToken cancellationToken = default)
{
ServiceAccountDocument? account = await _accounts.Find(x => x.SecretToken == secretToken).FirstOrDefaultAsync(cancellationToken);
account?.PostLoad(this);
return account;
}
///
async Task<(ServiceAccountDocument?, string?)> TryUpdateAsync(ServiceAccountDocument document, UpdateServiceAccountOptions options, CancellationToken cancellationToken = default)
{
string? newToken = null;
UpdateDefinition update = Builders.Update.Set(x => x.UpdateIndex, document.UpdateIndex + 1);
if (options.Name != null)
{
update = update.Set(x => x.Name, options.Name);
}
if (options.Description != null)
{
update = update.Set(x => x.Description, options.Description);
}
if (options.Claims != null)
{
update = update.Set(x => x.Claims, options.Claims.ConvertAll(x => new ClaimDocument(x)));
}
if (options.ResetToken ?? false)
{
newToken = CreateToken();
update = update.Set(x => x.SecretToken, newToken);
}
if (options.Enabled != null)
{
update = update.Set(x => x.Enabled, options.Enabled);
}
FilterDefinition filter;
if (document.UpdateIndex == 0)
{
filter = Builders.Filter.Eq(x => x.Id, document.Id) & Builders.Filter.Exists(x => x.UpdateIndex, false);
}
else
{
filter = Builders.Filter.Eq(x => x.Id, document.Id) & Builders.Filter.Eq(x => x.UpdateIndex, document.UpdateIndex);
}
ServiceAccountDocument? updated = await _accounts.FindOneAndUpdateAsync(filter, update, new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }, cancellationToken);
if (updated == null)
{
return (null, null);
}
return (updated, newToken);
}
///
public Task DeleteAsync(ServiceAccountId id, CancellationToken cancellationToken = default)
{
return _accounts.DeleteOneAsync(x => x.Id == id, cancellationToken);
}
}
}