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