< Summary

Information
Class: Elsa.Secrets.Services.DefaultSecretManager
Assembly: Elsa.Secrets
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Secrets/Services/DefaultSecretManager.cs
Line coverage
90%
Covered lines: 148
Uncovered lines: 15
Coverable lines: 163
Total lines: 279
Line coverage: 90.7%
Branch coverage
81%
Covered branches: 59
Total branches: 72
Branch coverage: 81.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
CreateAsync()100%22100%
GetAsync()75%44100%
ListAsync()100%11100%
ListPageAsync()75%88100%
CountAsync()100%11100%
UpdateAsync()100%44100%
RotateAsync()87.5%88100%
RevokeAsync()75%4488.88%
DeleteAsync()50%2288.88%
TestAsync()50%2260%
ResolvePayloadAsync()100%210%
ResolvePayloadAsync()50%4485.71%
CreateSecretAsync()83.33%6695.45%
GetExistingAsync()100%22100%
GetLatestActiveVersion(...)75%44100%
ValidateName(...)100%22100%
EnsureCanWrite(...)50%2266.66%
ApplyFilters(...)93.75%1616100%
GetFilterValues(...)75%4483.33%
CreatePayload(...)100%22100%
CreatePayload(...)100%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Secrets/Services/DefaultSecretManager.cs

#LineLine coverage
 1namespace Elsa.Secrets.Services;
 2
 453public class DefaultSecretManager(ISecretNameValidator nameValidator, ISecretStoreRegistry storeRegistry, ISecretTypeReg
 4{
 5    public async Task<Secret> CreateAsync(CreateSecretRequest request, CancellationToken cancellationToken = default)
 6    {
 427        ValidateName(request.Name);
 388        var secret = await CreateSecretAsync(request, cancellationToken);
 339        if (!await repository.TryAddOrReplaceDeletedAsync(secret, cancellationToken))
 210            throw new InvalidOperationException($"A secret named '{request.Name}' already exists.");
 11
 3112        return secret;
 3113    }
 14
 15    public async Task<Secret?> GetAsync(string name, CancellationToken cancellationToken = default)
 16    {
 3117        var secret = await repository.GetAsync(nameValidator.Normalize(name), cancellationToken);
 3118        return secret is { Status: SecretStatus.Deleted } ? null : secret;
 3119    }
 20
 21    public async Task<IReadOnlyCollection<Secret>> ListAsync(ListSecretsRequest request, CancellationToken cancellationT
 22    {
 123        return (await ListPageAsync(request, cancellationToken)).Items;
 124    }
 25
 26    public async Task<ListSecretsResult> ListPageAsync(ListSecretsRequest request, CancellationToken cancellationToken =
 27    {
 228        var secrets = await repository.ListAsync(cancellationToken);
 229        var query = ApplyFilters(secrets, request);
 230        var totalCount = query.LongCount();
 31
 232        var pageSize = request.PageSize is > 0 ? Math.Min(request.PageSize.Value, 200) : 100;
 233        var page = request.Page is > 0 ? request.Page.Value : 0;
 34
 235        var items = query
 536            .OrderBy(x => x.Name)
 237            .Skip(page * pageSize)
 238            .Take(pageSize)
 239            .ToList();
 40
 241        return new ListSecretsResult { Items = items, TotalCount = totalCount };
 242    }
 43
 44    public async Task<long> CountAsync(ListSecretsRequest request, CancellationToken cancellationToken = default)
 45    {
 146        var secrets = await repository.ListAsync(cancellationToken);
 147        return ApplyFilters(secrets, request).LongCount();
 148    }
 49
 50    public async Task<Secret> UpdateAsync(string name, UpdateSecretRequest request, CancellationToken cancellationToken 
 51    {
 352        var secret = await GetExistingAsync(name, cancellationToken);
 53
 254        secret.DisplayName = string.IsNullOrWhiteSpace(request.DisplayName) ? secret.Name : request.DisplayName.Trim();
 255        secret.Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim();
 256        secret.UpdatedAt = DateTimeOffset.UtcNow;
 57
 258        await repository.SaveAsync(secret, cancellationToken);
 259        return secret;
 260    }
 61
 62    public async Task<Secret> RotateAsync(string name, RotateSecretRequest request, CancellationToken cancellationToken 
 63    {
 564        var secret = await GetExistingAsync(name, cancellationToken);
 565        if (secret.Status == SecretStatus.Revoked)
 166            throw new InvalidOperationException($"Secret '{secret.Name}' is revoked and cannot be rotated.");
 67
 468        var provider = typeRegistry.Get(secret.TypeName);
 469        if (!provider.ValidateRotation(request, secret.StoreName, out var error))
 170            throw new InvalidOperationException(error);
 71
 372        var store = storeRegistry.Get(secret.StoreName);
 373        EnsureCanWrite(store);
 674        var nextVersion = secret.Versions.Count == 0 ? 1 : secret.Versions.Max(x => x.Version) + 1;
 375        var version = new SecretVersion { Version = nextVersion, ExpiresAt = request.ExpiresAt };
 376        version.Payload = await store.WriteAsync(secret, version, CreatePayload(request), cancellationToken);
 77
 1578        foreach (var activeVersion in secret.Versions.Where(x => x.Status == SecretStatus.Active))
 379            activeVersion.Status = SecretStatus.Retired;
 80
 381        secret.Versions.Add(version);
 382        secret.Status = SecretStatus.Active;
 383        secret.UpdatedAt = DateTimeOffset.UtcNow;
 384        await repository.SaveAsync(secret, cancellationToken);
 85
 386        return secret;
 387    }
 88
 89    public async Task<Secret?> RevokeAsync(string name, CancellationToken cancellationToken = default)
 90    {
 391        var secret = await GetAsync(name, cancellationToken);
 392        if (secret == null)
 093            return null;
 94
 395        secret.Status = SecretStatus.Revoked;
 396        secret.UpdatedAt = DateTimeOffset.UtcNow;
 1597        foreach (var version in secret.Versions.Where(x => x.Status == SecretStatus.Active))
 398            version.Status = SecretStatus.Revoked;
 99
 3100        await repository.SaveAsync(secret, cancellationToken);
 3101        return secret;
 3102    }
 103
 104    public async Task<bool> DeleteAsync(string name, CancellationToken cancellationToken = default)
 105    {
 3106        var secret = await GetAsync(name, cancellationToken);
 3107        if (secret == null)
 0108            return false;
 109
 3110        await storeRegistry.Get(secret.StoreName).DeleteAsync(secret, cancellationToken);
 3111        secret.Status = SecretStatus.Deleted;
 3112        secret.UpdatedAt = DateTimeOffset.UtcNow;
 3113        await repository.SaveAsync(secret, cancellationToken);
 3114        return true;
 3115    }
 116
 117    public async Task<SecretTestResponse> TestAsync(string name, CancellationToken cancellationToken = default)
 118    {
 119        try
 120        {
 3121            var secret = await GetExistingAsync(name, cancellationToken);
 2122            var version = GetLatestActiveVersion(secret);
 2123            var succeeded = await storeRegistry.Get(secret.StoreName).TestAsync(secret, version, cancellationToken);
 1124            return new SecretTestResponse { Succeeded = succeeded, Error = succeeded ? null : "Secret value is unavailab
 125        }
 0126        catch (InvalidOperationException e)
 127        {
 0128            return new SecretTestResponse { Succeeded = false, Error = e.Message };
 129        }
 1130        catch (KeyNotFoundException e)
 131        {
 1132            return new SecretTestResponse { Succeeded = false, Error = e.Message };
 133        }
 0134        catch (ArgumentException e)
 135        {
 0136            return new SecretTestResponse { Succeeded = false, Error = e.Message };
 137        }
 0138        catch (System.Security.Cryptography.CryptographicException e)
 139        {
 0140            return new SecretTestResponse { Succeeded = false, Error = e.Message };
 141        }
 1142        catch (FormatException e)
 143        {
 1144            return new SecretTestResponse { Succeeded = false, Error = e.Message };
 145        }
 3146    }
 147
 148    public async Task<SecretPayload> ResolvePayloadAsync(string name, CancellationToken cancellationToken = default)
 149    {
 0150        var secret = await GetExistingAsync(name, cancellationToken);
 0151        return await ResolvePayloadAsync(secret, cancellationToken);
 0152    }
 153
 154    public async Task<SecretPayload> ResolvePayloadAsync(Secret secret, CancellationToken cancellationToken = default)
 155    {
 8156        var version = GetLatestActiveVersion(secret);
 7157        var store = storeRegistry.Get(secret.StoreName);
 7158        var payload = await store.ReadAsync(secret, version, cancellationToken);
 159
 7160        if (payload?.Value == null)
 0161            throw new InvalidOperationException($"Secret '{secret.Name}' could not be resolved.");
 162
 7163        return payload;
 7164    }
 165
 166    private async Task<Secret> CreateSecretAsync(CreateSecretRequest request, CancellationToken cancellationToken)
 167    {
 38168        var typeProvider = typeRegistry.Get(request.TypeName);
 38169        var store = storeRegistry.Get(request.StoreName);
 38170        EnsureCanWrite(store);
 171
 38172        if (!typeProvider.Descriptor.SupportedStoreNames.Contains(store.Name, StringComparer.OrdinalIgnoreCase))
 0173            throw new InvalidOperationException($"Secret type '{request.TypeName}' does not support store '{request.Stor
 174
 38175        if (!typeProvider.Validate(request, out var error))
 5176            throw new InvalidOperationException(error);
 177
 33178        var secret = new Secret
 33179        {
 33180            Name = nameValidator.Normalize(request.Name),
 33181            DisplayName = string.IsNullOrWhiteSpace(request.DisplayName) ? request.Name.Trim() : request.DisplayName.Tri
 33182            Description = request.Description,
 33183            TypeName = typeProvider.Descriptor.Name,
 33184            StoreName = store.Name,
 33185            Scope = request.Scope,
 33186            Tags = request.Tags.ToHashSet(StringComparer.OrdinalIgnoreCase)
 33187        };
 188
 33189        var version = new SecretVersion { Version = 1, ExpiresAt = request.ExpiresAt };
 33190        version.Payload = await store.WriteAsync(secret, version, CreatePayload(request), cancellationToken);
 33191        secret.Versions.Add(version);
 192
 33193        return secret;
 33194    }
 195
 196    private async Task<Secret> GetExistingAsync(string name, CancellationToken cancellationToken)
 197    {
 11198        var secret = await GetAsync(name, cancellationToken);
 11199        return secret == null ? throw new KeyNotFoundException($"Secret '{name}' was not found.") : secret;
 9200    }
 201
 202    private static SecretVersion GetLatestActiveVersion(Secret secret)
 203    {
 10204        if (secret.Status != SecretStatus.Active)
 1205            throw new InvalidOperationException($"Secret '{secret.Name}' is not active.");
 206
 9207        return secret.LatestActiveVersion ?? throw new InvalidOperationException($"Secret '{secret.Name}' has no active 
 208    }
 209
 210    private void ValidateName(string name)
 211    {
 42212        if (!nameValidator.IsValid(name, out var error))
 4213            throw new InvalidOperationException(error);
 38214    }
 215
 216    private static void EnsureCanWrite(ISecretStore store)
 217    {
 41218        if (!store.Descriptor.Capabilities.HasFlag(SecretStoreCapabilities.Write))
 0219            throw new InvalidOperationException($"Secret store '{store.Name}' does not support writing secrets.");
 41220    }
 221
 222    private static IEnumerable<Secret> ApplyFilters(IEnumerable<Secret> secrets, ListSecretsRequest request)
 223    {
 20224        var query = secrets.Where(x => x.Status != SecretStatus.Deleted);
 225
 3226        if (!string.IsNullOrWhiteSpace(request.Search))
 227        {
 1228            var search = request.Search.Trim();
 1229            query = query.Where(x =>
 9230                x.Name.Contains(search, StringComparison.OrdinalIgnoreCase) ||
 9231                x.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase) ||
 9232                (x.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false));
 233        }
 234
 3235        var typeNames = GetFilterValues(request.TypeName, request.TypeNames);
 3236        if (typeNames.Count > 0)
 9237            query = query.Where(x => typeNames.Contains(x.TypeName));
 238
 3239        var storeNames = GetFilterValues(request.StoreName, request.StoreNames);
 3240        if (storeNames.Count > 0)
 9241            query = query.Where(x => storeNames.Contains(x.StoreName));
 242
 3243        if (!string.IsNullOrWhiteSpace(request.Scope))
 9244            query = query.Where(x => string.Equals(x.Scope, request.Scope, StringComparison.OrdinalIgnoreCase));
 245
 3246        if (request.Status != null)
 7247            query = query.Where(x => x.Status == request.Status);
 248
 3249        return query;
 250    }
 251
 252    private static HashSet<string> GetFilterValues(string? value, IEnumerable<string> values)
 253    {
 6254        var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 6255        if (!string.IsNullOrWhiteSpace(value))
 0256            result.Add(value.Trim());
 257
 18258        foreach (var item in values.Where(x => !string.IsNullOrWhiteSpace(x)))
 2259            result.Add(item.Trim());
 260
 6261        return result;
 262    }
 263
 264    private static SecretPayload CreatePayload(CreateSecretRequest request)
 265    {
 33266        var payload = new SecretPayload { Value = request.Value, Metadata = new Dictionary<string, string>(request.Metad
 33267        if (!string.IsNullOrWhiteSpace(request.ConfigurationKey))
 4268            payload.Metadata["configurationKey"] = request.ConfigurationKey.Trim();
 33269        return payload;
 270    }
 271
 272    private static SecretPayload CreatePayload(RotateSecretRequest request)
 273    {
 3274        var payload = new SecretPayload { Value = request.Value, Metadata = new Dictionary<string, string>(request.Metad
 3275        if (!string.IsNullOrWhiteSpace(request.ConfigurationKey))
 1276            payload.Metadata["configurationKey"] = request.ConfigurationKey.Trim();
 3277        return payload;
 278    }
 279}