< Summary

Information
Class: Elsa.Secrets.Persistence.EFCore.Repositories.EFCoreSecretRepository
Assembly: Elsa.Secrets.Persistence.EFCore
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Secrets.Persistence.EFCore/Repositories/EFCoreSecretRepository.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 125
Coverable lines: 125
Total lines: 234
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 70
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Secrets.Persistence.EFCore/Repositories/EFCoreSecretRepository.cs

#LineLine coverage
 1using Elsa.Common.Multitenancy;
 2using Elsa.Persistence.EFCore;
 3using Elsa.Secrets.Contracts;
 4using Elsa.Secrets.Models;
 5using Elsa.Tenants.Options;
 6using Microsoft.EntityFrameworkCore;
 7using Microsoft.Extensions.Options;
 8
 9namespace Elsa.Secrets.Persistence.EFCore.Repositories;
 10
 011public class EFCoreSecretRepository(
 012    Store<SecretsElsaDbContext, Secret> store,
 013    ISecretNameValidator secretNameValidator,
 014    IOptions<TenantsOptions>? tenantsOptions = null) : ISecretRepository
 15{
 016    private readonly bool? _tenancyEnabled = tenantsOptions?.Value.IsEnabled;
 17
 18    // Keep the pre-tenancy constructor in the public binary surface.
 19    public EFCoreSecretRepository(Store<SecretsElsaDbContext, Secret> store, ISecretNameValidator secretNameValidator)
 020        : this(store, secretNameValidator, null)
 21    {
 022    }
 23
 24    public async Task<Secret?> GetAsync(string normalizedName, CancellationToken cancellationToken = default)
 25    {
 026        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
 027        var secret = await FindByNameAsync(dbContext, normalizedName, cancellationToken);
 028        SecretSerialization.LoadSerializedProperties(dbContext, secret);
 029        return secret;
 030    }
 31
 32    public async Task<IReadOnlyCollection<Secret>> ListAsync(CancellationToken cancellationToken = default)
 33    {
 034        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
 035        var secrets = await dbContext.Secrets.ToListAsync(cancellationToken);
 36
 037        foreach (var secret in secrets)
 038            SecretSerialization.LoadSerializedProperties(dbContext, secret);
 39
 040        return secrets;
 041    }
 42
 43    public async Task AddAsync(Secret secret, CancellationToken cancellationToken = default)
 44    {
 045        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
 046        AssignDefaultTenantId(secret, dbContext);
 047        EnsureNewSecretTenantIsOwnedByWriter(secret, dbContext, IsTenancyEnabled(dbContext));
 048        var normalizedName = secretNameValidator.Normalize(secret.Name);
 049        if (await ExistsByNormalizedNameAsync(dbContext, normalizedName, cancellationToken))
 050            throw new InvalidOperationException($"A secret named '{secret.Name}' already exists.");
 51
 052        await dbContext.Secrets.AddAsync(secret, cancellationToken);
 053        SetNormalizedName(dbContext, secret);
 054        SecretSerialization.StoreSerializedProperties(dbContext, secret);
 055        await SaveChangesAsync(dbContext, secret.Name, cancellationToken);
 056    }
 57
 58    public async Task<bool> TryAddOrReplaceDeletedAsync(Secret secret, CancellationToken cancellationToken = default)
 59    {
 060        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
 061        AssignDefaultTenantId(secret, dbContext);
 062        var tenancyEnabled = IsTenancyEnabled(dbContext);
 063        var existingSecret = await FindByNameAsync(dbContext, secret.Name, cancellationToken);
 64
 065        if (existingSecret == null)
 66        {
 067            if (!IsNewSecretTenantOwnedByWriter(secret, dbContext, tenancyEnabled))
 068                return false;
 69
 070            await dbContext.Secrets.AddAsync(secret, cancellationToken);
 071            SetNormalizedName(dbContext, secret);
 072            SecretSerialization.StoreSerializedProperties(dbContext, secret);
 073            return await TrySaveChangesAsync(dbContext, secret.Name, cancellationToken);
 74        }
 75
 076        if (existingSecret.IsLifecycleManaged || existingSecret.Status != SecretStatus.Deleted)
 77        {
 078            return false;
 79        }
 80
 081        var incomingTenantId = secret.TenantId ?? dbContext.TenantId;
 082        if (tenancyEnabled && !TenantVisibility.CanReplaceOwnedRow(existingSecret.TenantId, incomingTenantId, dbContext.
 083            return false;
 84
 085        if (tenancyEnabled)
 086            secret.TenantId = existingSecret.TenantId;
 087        await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
 088        dbContext.Secrets.Remove(existingSecret);
 089        await dbContext.SaveChangesAsync(cancellationToken);
 090        await dbContext.Secrets.AddAsync(secret, cancellationToken);
 091        SetNormalizedName(dbContext, secret);
 092        SecretSerialization.StoreSerializedProperties(dbContext, secret);
 93
 094        if (!await TrySaveChangesAsync(dbContext, secret.Name, cancellationToken))
 095            return false;
 96
 097        await transaction.CommitAsync(cancellationToken);
 098        return true;
 099    }
 100
 101    public async Task SaveAsync(Secret secret, CancellationToken cancellationToken = default)
 102    {
 0103        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
 0104        AssignDefaultTenantId(secret, dbContext);
 0105        var tenancyEnabled = IsTenancyEnabled(dbContext);
 0106        var existingSecret = await FindByNameAsync(dbContext, secret.Name, cancellationToken);
 107
 0108        if (existingSecret == null)
 109        {
 0110            EnsureNewSecretTenantIsOwnedByWriter(secret, dbContext, tenancyEnabled);
 0111            await dbContext.Secrets.AddAsync(secret, cancellationToken);
 0112            SetNormalizedName(dbContext, secret);
 0113            SecretSerialization.StoreSerializedProperties(dbContext, secret);
 114        }
 115        else
 116        {
 0117            var incomingTenantId = secret.TenantId ?? dbContext.TenantId;
 0118            if (tenancyEnabled && !TenantVisibility.CanReplaceOwnedRow(existingSecret.TenantId, incomingTenantId, dbCont
 0119                throw new InvalidOperationException($"A secret named '{secret.Name}' belongs to another tenant.");
 120
 0121            Copy(secret, existingSecret, tenancyEnabled);
 0122            SetNormalizedName(dbContext, existingSecret);
 0123            SecretSerialization.StoreSerializedProperties(dbContext, existingSecret);
 124        }
 125
 0126        await SaveChangesAsync(dbContext, secret.Name, cancellationToken);
 0127    }
 128
 129    private static void Copy(Secret source, Secret target, bool tenancyEnabled)
 130    {
 0131        if (!tenancyEnabled)
 0132            target.TenantId = source.TenantId;
 133
 0134        target.Name = source.Name;
 0135        target.DisplayName = source.DisplayName;
 0136        target.Description = source.Description;
 0137        target.TypeName = source.TypeName;
 0138        target.StoreName = source.StoreName;
 0139        target.Scope = source.Scope;
 0140        target.Status = source.Status;
 0141        target.CreatedAt = source.CreatedAt;
 0142        target.UpdatedAt = source.UpdatedAt;
 0143        target.Tags = source.Tags.ToHashSet(StringComparer.OrdinalIgnoreCase);
 0144        target.Versions = source.Versions.ToList();
 0145    }
 146
 147    private Task<Secret?> FindByNameAsync(SecretsElsaDbContext dbContext, string name, CancellationToken cancellationTok
 148    {
 0149        var normalizedName = secretNameValidator.Normalize(name);
 0150        return dbContext.Secrets.FirstOrDefaultAsync(x => EF.Property<string>(x, SecretShadowPropertyNames.NormalizedNam
 151    }
 152
 153    private bool IsTenancyEnabled(SecretsElsaDbContext dbContext)
 154    {
 0155        if (_tenancyEnabled.HasValue)
 0156            return _tenancyEnabled.Value;
 157
 0158        return dbContext.IsTenantFilteringEnabled;
 159    }
 160
 161    private static bool IsNewSecretTenantOwnedByWriter(Secret secret, SecretsElsaDbContext dbContext, bool tenancyEnable
 0162        !tenancyEnabled || secret.TenantId is null || secret.TenantId == (dbContext.TenantId ?? Tenant.DefaultTenantId);
 163
 164    private static void EnsureNewSecretTenantIsOwnedByWriter(Secret secret, SecretsElsaDbContext dbContext, bool tenancy
 165    {
 0166        if (!IsNewSecretTenantOwnedByWriter(secret, dbContext, tenancyEnabled))
 0167            throw new InvalidOperationException($"A secret named '{secret.Name}' belongs to another tenant.");
 0168    }
 169
 170    private static Task<bool> ExistsByNormalizedNameAsync(SecretsElsaDbContext dbContext, string normalizedName, Cancell
 171    {
 0172        return dbContext.Secrets.AnyAsync(x => EF.Property<string>(x, SecretShadowPropertyNames.NormalizedName) == norma
 173    }
 174
 175    // The DbUpdateException-to-name-conflict translation below relies on the (TenantId, NormalizedName)
 176    // unique index. Default-tenant writes are stamped with an empty TenantId before saving so the index
 177    // provides the same concurrency backstop when multitenancy is disabled. See doc/migrations/secrets-tenancy.md.
 178    private async Task SaveChangesAsync(SecretsElsaDbContext dbContext, string name, CancellationToken cancellationToken
 179    {
 180        try
 181        {
 0182            await dbContext.SaveChangesAsync(cancellationToken);
 0183        }
 0184        catch (DbUpdateException e)
 185        {
 0186            if (await IsNameConflictAsync(name, cancellationToken))
 0187                throw new InvalidOperationException($"A secret named '{name}' already exists.", e);
 188
 0189            throw;
 0190        }
 0191    }
 192
 193    private async Task<bool> TrySaveChangesAsync(SecretsElsaDbContext dbContext, string name, CancellationToken cancella
 194    {
 195        try
 196        {
 0197            await dbContext.SaveChangesAsync(cancellationToken);
 0198            return true;
 199        }
 0200        catch (DbUpdateException)
 201        {
 0202            if (await IsNameConflictAsync(name, cancellationToken))
 0203                return false;
 204
 0205            throw;
 206        }
 0207    }
 208
 209    private async Task<bool> IsNameConflictAsync(string name, CancellationToken cancellationToken)
 210    {
 0211        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
 0212        return await ExistsByNormalizedNameAsync(dbContext, secretNameValidator.Normalize(name), cancellationToken);
 0213    }
 214
 215    private void SetNormalizedName(SecretsElsaDbContext dbContext, Secret secret)
 216    {
 0217        dbContext.Entry(secret).Property(SecretShadowPropertyNames.NormalizedName).CurrentValue = secretNameValidator.No
 0218    }
 219
 220    /// <summary>
 221    /// Default-tenant uniqueness uses <see cref="Tenant.DefaultTenantId"/> (<c>""</c>), not null.
 222    /// Named and agnostic ambient tenants are left for <c>ApplyTenantId</c>.
 223    /// </summary>
 224    private static void AssignDefaultTenantId(Secret secret, SecretsElsaDbContext dbContext)
 225    {
 0226        if (secret.TenantId is not null)
 0227            return;
 228
 0229        if (!string.IsNullOrEmpty(dbContext.TenantId) && dbContext.TenantId != Tenant.DefaultTenantId)
 0230            return;
 231
 0232        secret.TenantId = Tenant.DefaultTenantId;
 0233    }
 234}