< 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: 141
Uncovered lines: 15
Coverable lines: 156
Total lines: 267
Line coverage: 90.3%
Branch coverage
80%
Covered branches: 55
Total branches: 68
Branch coverage: 80.8%
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%
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
 313public class DefaultSecretManager(ISecretNameValidator nameValidator, ISecretStoreRegistry storeRegistry, ISecretTypeReg
 4{
 5    public async Task<Secret> CreateAsync(CreateSecretRequest request, CancellationToken cancellationToken = default)
 6    {
 377        ValidateName(request.Name);
 338        var secret = await CreateSecretAsync(request, cancellationToken);
 289        if (!await repository.TryAddOrReplaceDeletedAsync(secret, cancellationToken))
 210            throw new InvalidOperationException($"A secret named '{request.Name}' already exists.");
 11
 2612        return secret;
 2613    }
 14
 15    public async Task<Secret?> GetAsync(string name, CancellationToken cancellationToken = default)
 16    {
 2317        var secret = await repository.GetAsync(nameValidator.Normalize(name), cancellationToken);
 2318        return secret is { Status: SecretStatus.Deleted } ? null : secret;
 2319    }
 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> RotateAsync(string name, RotateSecretRequest request, CancellationToken cancellationToken 
 51    {
 552        var secret = await GetExistingAsync(name, cancellationToken);
 553        if (secret.Status == SecretStatus.Revoked)
 154            throw new InvalidOperationException($"Secret '{secret.Name}' is revoked and cannot be rotated.");
 55
 456        var provider = typeRegistry.Get(secret.TypeName);
 457        if (!provider.ValidateRotation(request, secret.StoreName, out var error))
 158            throw new InvalidOperationException(error);
 59
 360        var store = storeRegistry.Get(secret.StoreName);
 361        EnsureCanWrite(store);
 662        var nextVersion = secret.Versions.Count == 0 ? 1 : secret.Versions.Max(x => x.Version) + 1;
 363        var version = new SecretVersion { Version = nextVersion, ExpiresAt = request.ExpiresAt };
 364        version.Payload = await store.WriteAsync(secret, version, CreatePayload(request), cancellationToken);
 65
 1566        foreach (var activeVersion in secret.Versions.Where(x => x.Status == SecretStatus.Active))
 367            activeVersion.Status = SecretStatus.Retired;
 68
 369        secret.Versions.Add(version);
 370        secret.Status = SecretStatus.Active;
 371        secret.UpdatedAt = DateTimeOffset.UtcNow;
 372        await repository.SaveAsync(secret, cancellationToken);
 73
 374        return secret;
 375    }
 76
 77    public async Task<Secret?> RevokeAsync(string name, CancellationToken cancellationToken = default)
 78    {
 379        var secret = await GetAsync(name, cancellationToken);
 380        if (secret == null)
 081            return null;
 82
 383        secret.Status = SecretStatus.Revoked;
 384        secret.UpdatedAt = DateTimeOffset.UtcNow;
 1585        foreach (var version in secret.Versions.Where(x => x.Status == SecretStatus.Active))
 386            version.Status = SecretStatus.Revoked;
 87
 388        await repository.SaveAsync(secret, cancellationToken);
 389        return secret;
 390    }
 91
 92    public async Task<bool> DeleteAsync(string name, CancellationToken cancellationToken = default)
 93    {
 394        var secret = await GetAsync(name, cancellationToken);
 395        if (secret == null)
 096            return false;
 97
 398        await storeRegistry.Get(secret.StoreName).DeleteAsync(secret, cancellationToken);
 399        secret.Status = SecretStatus.Deleted;
 3100        secret.UpdatedAt = DateTimeOffset.UtcNow;
 3101        await repository.SaveAsync(secret, cancellationToken);
 3102        return true;
 3103    }
 104
 105    public async Task<SecretTestResponse> TestAsync(string name, CancellationToken cancellationToken = default)
 106    {
 107        try
 108        {
 3109            var secret = await GetExistingAsync(name, cancellationToken);
 2110            var version = GetLatestActiveVersion(secret);
 2111            var succeeded = await storeRegistry.Get(secret.StoreName).TestAsync(secret, version, cancellationToken);
 1112            return new SecretTestResponse { Succeeded = succeeded, Error = succeeded ? null : "Secret value is unavailab
 113        }
 0114        catch (InvalidOperationException e)
 115        {
 0116            return new SecretTestResponse { Succeeded = false, Error = e.Message };
 117        }
 1118        catch (KeyNotFoundException e)
 119        {
 1120            return new SecretTestResponse { Succeeded = false, Error = e.Message };
 121        }
 0122        catch (ArgumentException e)
 123        {
 0124            return new SecretTestResponse { Succeeded = false, Error = e.Message };
 125        }
 0126        catch (System.Security.Cryptography.CryptographicException e)
 127        {
 0128            return new SecretTestResponse { Succeeded = false, Error = e.Message };
 129        }
 1130        catch (FormatException e)
 131        {
 1132            return new SecretTestResponse { Succeeded = false, Error = e.Message };
 133        }
 3134    }
 135
 136    public async Task<SecretPayload> ResolvePayloadAsync(string name, CancellationToken cancellationToken = default)
 137    {
 0138        var secret = await GetExistingAsync(name, cancellationToken);
 0139        return await ResolvePayloadAsync(secret, cancellationToken);
 0140    }
 141
 142    public async Task<SecretPayload> ResolvePayloadAsync(Secret secret, CancellationToken cancellationToken = default)
 143    {
 6144        var version = GetLatestActiveVersion(secret);
 5145        var store = storeRegistry.Get(secret.StoreName);
 5146        var payload = await store.ReadAsync(secret, version, cancellationToken);
 147
 5148        if (payload?.Value == null)
 0149            throw new InvalidOperationException($"Secret '{secret.Name}' could not be resolved.");
 150
 5151        return payload;
 5152    }
 153
 154    private async Task<Secret> CreateSecretAsync(CreateSecretRequest request, CancellationToken cancellationToken)
 155    {
 33156        var typeProvider = typeRegistry.Get(request.TypeName);
 33157        var store = storeRegistry.Get(request.StoreName);
 33158        EnsureCanWrite(store);
 159
 33160        if (!typeProvider.Descriptor.SupportedStoreNames.Contains(store.Name, StringComparer.OrdinalIgnoreCase))
 0161            throw new InvalidOperationException($"Secret type '{request.TypeName}' does not support store '{request.Stor
 162
 33163        if (!typeProvider.Validate(request, out var error))
 5164            throw new InvalidOperationException(error);
 165
 28166        var secret = new Secret
 28167        {
 28168            Name = nameValidator.Normalize(request.Name),
 28169            DisplayName = string.IsNullOrWhiteSpace(request.DisplayName) ? request.Name.Trim() : request.DisplayName.Tri
 28170            Description = request.Description,
 28171            TypeName = typeProvider.Descriptor.Name,
 28172            StoreName = store.Name,
 28173            Scope = request.Scope,
 28174            Tags = request.Tags.ToHashSet(StringComparer.OrdinalIgnoreCase)
 28175        };
 176
 28177        var version = new SecretVersion { Version = 1, ExpiresAt = request.ExpiresAt };
 28178        version.Payload = await store.WriteAsync(secret, version, CreatePayload(request), cancellationToken);
 28179        secret.Versions.Add(version);
 180
 28181        return secret;
 28182    }
 183
 184    private async Task<Secret> GetExistingAsync(string name, CancellationToken cancellationToken)
 185    {
 8186        var secret = await GetAsync(name, cancellationToken);
 8187        return secret == null ? throw new KeyNotFoundException($"Secret '{name}' was not found.") : secret;
 7188    }
 189
 190    private static SecretVersion GetLatestActiveVersion(Secret secret)
 191    {
 8192        if (secret.Status != SecretStatus.Active)
 1193            throw new InvalidOperationException($"Secret '{secret.Name}' is not active.");
 194
 7195        return secret.LatestActiveVersion ?? throw new InvalidOperationException($"Secret '{secret.Name}' has no active 
 196    }
 197
 198    private void ValidateName(string name)
 199    {
 37200        if (!nameValidator.IsValid(name, out var error))
 4201            throw new InvalidOperationException(error);
 33202    }
 203
 204    private static void EnsureCanWrite(ISecretStore store)
 205    {
 36206        if (!store.Descriptor.Capabilities.HasFlag(SecretStoreCapabilities.Write))
 0207            throw new InvalidOperationException($"Secret store '{store.Name}' does not support writing secrets.");
 36208    }
 209
 210    private static IEnumerable<Secret> ApplyFilters(IEnumerable<Secret> secrets, ListSecretsRequest request)
 211    {
 20212        var query = secrets.Where(x => x.Status != SecretStatus.Deleted);
 213
 3214        if (!string.IsNullOrWhiteSpace(request.Search))
 215        {
 1216            var search = request.Search.Trim();
 1217            query = query.Where(x =>
 9218                x.Name.Contains(search, StringComparison.OrdinalIgnoreCase) ||
 9219                x.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase) ||
 9220                (x.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false));
 221        }
 222
 3223        var typeNames = GetFilterValues(request.TypeName, request.TypeNames);
 3224        if (typeNames.Count > 0)
 9225            query = query.Where(x => typeNames.Contains(x.TypeName));
 226
 3227        var storeNames = GetFilterValues(request.StoreName, request.StoreNames);
 3228        if (storeNames.Count > 0)
 9229            query = query.Where(x => storeNames.Contains(x.StoreName));
 230
 3231        if (!string.IsNullOrWhiteSpace(request.Scope))
 9232            query = query.Where(x => string.Equals(x.Scope, request.Scope, StringComparison.OrdinalIgnoreCase));
 233
 3234        if (request.Status != null)
 7235            query = query.Where(x => x.Status == request.Status);
 236
 3237        return query;
 238    }
 239
 240    private static HashSet<string> GetFilterValues(string? value, IEnumerable<string> values)
 241    {
 6242        var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 6243        if (!string.IsNullOrWhiteSpace(value))
 0244            result.Add(value.Trim());
 245
 18246        foreach (var item in values.Where(x => !string.IsNullOrWhiteSpace(x)))
 2247            result.Add(item.Trim());
 248
 6249        return result;
 250    }
 251
 252    private static SecretPayload CreatePayload(CreateSecretRequest request)
 253    {
 28254        var payload = new SecretPayload { Value = request.Value, Metadata = new Dictionary<string, string>(request.Metad
 28255        if (!string.IsNullOrWhiteSpace(request.ConfigurationKey))
 4256            payload.Metadata["configurationKey"] = request.ConfigurationKey.Trim();
 28257        return payload;
 258    }
 259
 260    private static SecretPayload CreatePayload(RotateSecretRequest request)
 261    {
 3262        var payload = new SecretPayload { Value = request.Value, Metadata = new Dictionary<string, string>(request.Metad
 3263        if (!string.IsNullOrWhiteSpace(request.ConfigurationKey))
 1264            payload.Metadata["configurationKey"] = request.ConfigurationKey.Trim();
 3265        return payload;
 266    }
 267}