Harden EF Core secret name uniqueness
This commit is contained in:
parent
8c0a720056
commit
9fac0c83c0
|
|
@ -47,6 +47,11 @@ namespace Elsa.Secrets.Persistence.EFCore.MySql.Migrations.Secrets
|
|||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
|
@ -81,9 +86,9 @@ namespace Elsa.Secrets.Persistence.EFCore.MySql.Migrations.Secrets
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Secret_Name");
|
||||
.HasDatabaseName("IX_Secret_NormalizedName");
|
||||
|
||||
b.HasIndex("Scope")
|
||||
.HasDatabaseName("IX_Secret_Scope");
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ namespace Elsa.Secrets.Persistence.EFCore.MySql.Migrations.Secrets
|
|||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Name = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
NormalizedName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DisplayName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Description = table.Column<string>(type: "longtext", nullable: true)
|
||||
|
|
@ -60,10 +62,10 @@ namespace Elsa.Secrets.Persistence.EFCore.MySql.Migrations.Secrets
|
|||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Secret_Name",
|
||||
name: "IX_Secret_NormalizedName",
|
||||
schema: _schema.Schema,
|
||||
table: "Secrets",
|
||||
column: "Name",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ namespace Elsa.Secrets.Persistence.EFCore.MySql.Migrations.Secrets
|
|||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
|
@ -78,9 +83,9 @@ namespace Elsa.Secrets.Persistence.EFCore.MySql.Migrations.Secrets
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Secret_Name");
|
||||
.HasDatabaseName("IX_Secret_NormalizedName");
|
||||
|
||||
b.HasIndex("Scope")
|
||||
.HasDatabaseName("IX_Secret_Scope");
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ namespace Elsa.Secrets.Persistence.EFCore.Oracle.Migrations.Secrets
|
|||
.HasMaxLength(200)
|
||||
.HasColumnType("NVARCHAR2(200)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("NVARCHAR2(200)");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("NVARCHAR2(200)");
|
||||
|
|
@ -81,9 +86,9 @@ namespace Elsa.Secrets.Persistence.EFCore.Oracle.Migrations.Secrets
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Secret_Name");
|
||||
.HasDatabaseName("IX_Secret_NormalizedName");
|
||||
|
||||
b.HasIndex("Scope")
|
||||
.HasDatabaseName("IX_Secret_Scope");
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ namespace Elsa.Secrets.Persistence.EFCore.Oracle.Migrations.Secrets
|
|||
{
|
||||
Id = table.Column<string>(type: "NVARCHAR2(450)", nullable: false),
|
||||
Name = table.Column<string>(type: "NVARCHAR2(200)", maxLength: 200, nullable: false),
|
||||
NormalizedName = table.Column<string>(type: "NVARCHAR2(200)", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "NVARCHAR2(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "NVARCHAR2(2000)", nullable: true),
|
||||
TypeName = table.Column<string>(type: "NVARCHAR2(100)", maxLength: 100, nullable: false),
|
||||
|
|
@ -46,10 +47,10 @@ namespace Elsa.Secrets.Persistence.EFCore.Oracle.Migrations.Secrets
|
|||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Secret_Name",
|
||||
name: "IX_Secret_NormalizedName",
|
||||
schema: _schema.Schema,
|
||||
table: "Secrets",
|
||||
column: "Name",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ namespace Elsa.Secrets.Persistence.EFCore.Oracle.Migrations.Secrets
|
|||
.HasMaxLength(200)
|
||||
.HasColumnType("NVARCHAR2(200)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("NVARCHAR2(200)");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("NVARCHAR2(200)");
|
||||
|
|
@ -78,9 +83,9 @@ namespace Elsa.Secrets.Persistence.EFCore.Oracle.Migrations.Secrets
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Secret_Name");
|
||||
.HasDatabaseName("IX_Secret_NormalizedName");
|
||||
|
||||
b.HasIndex("Scope")
|
||||
.HasDatabaseName("IX_Secret_Scope");
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ namespace Elsa.Secrets.Persistence.EFCore.PostgreSql.Migrations.Secrets
|
|||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
|
@ -81,9 +86,9 @@ namespace Elsa.Secrets.Persistence.EFCore.PostgreSql.Migrations.Secrets
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Secret_Name");
|
||||
.HasDatabaseName("IX_Secret_NormalizedName");
|
||||
|
||||
b.HasIndex("Scope")
|
||||
.HasDatabaseName("IX_Secret_Scope");
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ namespace Elsa.Secrets.Persistence.EFCore.PostgreSql.Migrations.Secrets
|
|||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
NormalizedName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "text", nullable: true),
|
||||
TypeName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
|
|
@ -46,10 +47,10 @@ namespace Elsa.Secrets.Persistence.EFCore.PostgreSql.Migrations.Secrets
|
|||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Secret_Name",
|
||||
name: "IX_Secret_NormalizedName",
|
||||
schema: _schema.Schema,
|
||||
table: "Secrets",
|
||||
column: "Name",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ namespace Elsa.Secrets.Persistence.EFCore.PostgreSql.Migrations.Secrets
|
|||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
|
@ -78,9 +83,9 @@ namespace Elsa.Secrets.Persistence.EFCore.PostgreSql.Migrations.Secrets
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Secret_Name");
|
||||
.HasDatabaseName("IX_Secret_NormalizedName");
|
||||
|
||||
b.HasIndex("Scope")
|
||||
.HasDatabaseName("IX_Secret_Scope");
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ namespace Elsa.Secrets.Persistence.EFCore.SqlServer.Migrations.Secrets
|
|||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
|
@ -81,9 +86,9 @@ namespace Elsa.Secrets.Persistence.EFCore.SqlServer.Migrations.Secrets
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Secret_Name");
|
||||
.HasDatabaseName("IX_Secret_NormalizedName");
|
||||
|
||||
b.HasIndex("Scope")
|
||||
.HasDatabaseName("IX_Secret_Scope");
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ namespace Elsa.Secrets.Persistence.EFCore.SqlServer.Migrations.Secrets
|
|||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
NormalizedName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
TypeName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
|
|
@ -46,10 +47,10 @@ namespace Elsa.Secrets.Persistence.EFCore.SqlServer.Migrations.Secrets
|
|||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Secret_Name",
|
||||
name: "IX_Secret_NormalizedName",
|
||||
schema: _schema.Schema,
|
||||
table: "Secrets",
|
||||
column: "Name",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ namespace Elsa.Secrets.Persistence.EFCore.SqlServer.Migrations.Secrets
|
|||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
|
@ -78,9 +83,9 @@ namespace Elsa.Secrets.Persistence.EFCore.SqlServer.Migrations.Secrets
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Secret_Name");
|
||||
.HasDatabaseName("IX_Secret_NormalizedName");
|
||||
|
||||
b.HasIndex("Scope")
|
||||
.HasDatabaseName("IX_Secret_Scope");
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ namespace Elsa.Secrets.Persistence.EFCore.Sqlite.Migrations.Secrets
|
|||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
|
@ -77,9 +82,9 @@ namespace Elsa.Secrets.Persistence.EFCore.Sqlite.Migrations.Secrets
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Secret_Name");
|
||||
.HasDatabaseName("IX_Secret_NormalizedName");
|
||||
|
||||
b.HasIndex("Scope")
|
||||
.HasDatabaseName("IX_Secret_Scope");
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ namespace Elsa.Secrets.Persistence.EFCore.Sqlite.Migrations.Secrets
|
|||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||
TypeName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
|
|
@ -37,10 +38,10 @@ namespace Elsa.Secrets.Persistence.EFCore.Sqlite.Migrations.Secrets
|
|||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Secret_Name",
|
||||
name: "IX_Secret_NormalizedName",
|
||||
schema: "Elsa",
|
||||
table: "Secrets",
|
||||
column: "Name",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ namespace Elsa.Secrets.Persistence.EFCore.Sqlite.Migrations.Secrets
|
|||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
|
@ -74,9 +79,9 @@ namespace Elsa.Secrets.Persistence.EFCore.Sqlite.Migrations.Secrets
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Secret_Name");
|
||||
.HasDatabaseName("IX_Secret_NormalizedName");
|
||||
|
||||
b.HasIndex("Scope")
|
||||
.HasDatabaseName("IX_Secret_Scope");
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ namespace Elsa.Secrets.Persistence.EFCore.Repositories;
|
|||
|
||||
public class EFCoreSecretRepository(Store<SecretsElsaDbContext, Secret> store) : ISecretRepository
|
||||
{
|
||||
private static readonly SemaphoreSlim Semaphore = new(1, 1);
|
||||
|
||||
public async Task<Secret?> GetAsync(string normalizedName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
|
||||
|
|
@ -30,83 +28,66 @@ public class EFCoreSecretRepository(Store<SecretsElsaDbContext, Secret> store) :
|
|||
|
||||
public async Task AddAsync(Secret secret, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Semaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
|
||||
var normalizedName = NormalizeName(secret.Name);
|
||||
if (await dbContext.Secrets.AnyAsync(x => x.Name.ToLower() == normalizedName, cancellationToken))
|
||||
throw new InvalidOperationException($"A secret named '{secret.Name}' already exists.");
|
||||
await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
|
||||
var normalizedName = NormalizeName(secret.Name);
|
||||
if (await ExistsByNormalizedNameAsync(dbContext, normalizedName, cancellationToken))
|
||||
throw new InvalidOperationException($"A secret named '{secret.Name}' already exists.");
|
||||
|
||||
await dbContext.Secrets.AddAsync(secret, cancellationToken);
|
||||
SecretSerialization.StoreSerializedProperties(dbContext, secret);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Semaphore.Release();
|
||||
}
|
||||
await dbContext.Secrets.AddAsync(secret, cancellationToken);
|
||||
SetNormalizedName(dbContext, secret);
|
||||
SecretSerialization.StoreSerializedProperties(dbContext, secret);
|
||||
await SaveChangesAsync(dbContext, secret.Name, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> TryAddOrReplaceDeletedAsync(Secret secret, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Semaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
|
||||
var existingSecret = await FindByNameAsync(dbContext, secret.Name, cancellationToken);
|
||||
|
||||
if (existingSecret == null)
|
||||
{
|
||||
await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
|
||||
var existingSecret = await FindByNameAsync(dbContext, secret.Name, cancellationToken);
|
||||
|
||||
if (existingSecret == null)
|
||||
{
|
||||
await dbContext.Secrets.AddAsync(secret, cancellationToken);
|
||||
SecretSerialization.StoreSerializedProperties(dbContext, secret);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (existingSecret.Status != SecretStatus.Deleted)
|
||||
return false;
|
||||
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
dbContext.Secrets.Remove(existingSecret);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await dbContext.Secrets.AddAsync(secret, cancellationToken);
|
||||
SetNormalizedName(dbContext, secret);
|
||||
SecretSerialization.StoreSerializedProperties(dbContext, secret);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Semaphore.Release();
|
||||
return await TrySaveChangesAsync(dbContext, secret.Name, cancellationToken);
|
||||
}
|
||||
|
||||
if (existingSecret.Status != SecretStatus.Deleted)
|
||||
return false;
|
||||
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
dbContext.Secrets.Remove(existingSecret);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await dbContext.Secrets.AddAsync(secret, cancellationToken);
|
||||
SetNormalizedName(dbContext, secret);
|
||||
SecretSerialization.StoreSerializedProperties(dbContext, secret);
|
||||
|
||||
if (!await TrySaveChangesAsync(dbContext, secret.Name, cancellationToken))
|
||||
return false;
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Secret secret, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Semaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
|
||||
var existingSecret = await FindByNameAsync(dbContext, secret.Name, cancellationToken);
|
||||
await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
|
||||
var existingSecret = await FindByNameAsync(dbContext, secret.Name, cancellationToken);
|
||||
|
||||
if (existingSecret == null)
|
||||
{
|
||||
await dbContext.Secrets.AddAsync(secret, cancellationToken);
|
||||
SecretSerialization.StoreSerializedProperties(dbContext, secret);
|
||||
}
|
||||
else
|
||||
{
|
||||
Copy(secret, existingSecret);
|
||||
SecretSerialization.StoreSerializedProperties(dbContext, existingSecret);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
finally
|
||||
if (existingSecret == null)
|
||||
{
|
||||
Semaphore.Release();
|
||||
await dbContext.Secrets.AddAsync(secret, cancellationToken);
|
||||
SetNormalizedName(dbContext, secret);
|
||||
SecretSerialization.StoreSerializedProperties(dbContext, secret);
|
||||
}
|
||||
else
|
||||
{
|
||||
Copy(secret, existingSecret);
|
||||
SetNormalizedName(dbContext, existingSecret);
|
||||
SecretSerialization.StoreSerializedProperties(dbContext, existingSecret);
|
||||
}
|
||||
|
||||
await SaveChangesAsync(dbContext, secret.Name, cancellationToken);
|
||||
}
|
||||
|
||||
private static void Copy(Secret source, Secret target)
|
||||
|
|
@ -127,8 +108,55 @@ public class EFCoreSecretRepository(Store<SecretsElsaDbContext, Secret> store) :
|
|||
private static Task<Secret?> FindByNameAsync(SecretsElsaDbContext dbContext, string name, CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedName = NormalizeName(name);
|
||||
return dbContext.Secrets.FirstOrDefaultAsync(x => x.Name.ToLower() == normalizedName, cancellationToken);
|
||||
return dbContext.Secrets.FirstOrDefaultAsync(x => EF.Property<string>(x, SecretConfiguration.NormalizedNamePropertyName) == normalizedName, cancellationToken);
|
||||
}
|
||||
|
||||
private static string NormalizeName(string name) => name.ToLowerInvariant();
|
||||
private static Task<bool> ExistsByNormalizedNameAsync(SecretsElsaDbContext dbContext, string normalizedName, CancellationToken cancellationToken)
|
||||
{
|
||||
return dbContext.Secrets.AnyAsync(x => EF.Property<string>(x, SecretConfiguration.NormalizedNamePropertyName) == normalizedName, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task SaveChangesAsync(SecretsElsaDbContext dbContext, string name, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
if (await IsNameConflictAsync(name, cancellationToken))
|
||||
throw new InvalidOperationException($"A secret named '{name}' already exists.", e);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TrySaveChangesAsync(SecretsElsaDbContext dbContext, string name, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
if (await IsNameConflictAsync(name, cancellationToken))
|
||||
return false;
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> IsNameConflictAsync(string name, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
|
||||
return await ExistsByNormalizedNameAsync(dbContext, NormalizeName(name), cancellationToken);
|
||||
}
|
||||
|
||||
private static void SetNormalizedName(SecretsElsaDbContext dbContext, Secret secret)
|
||||
{
|
||||
dbContext.Entry(secret).Property(SecretConfiguration.NormalizedNamePropertyName).CurrentValue = NormalizeName(secret.Name);
|
||||
}
|
||||
|
||||
private static string NormalizeName(string name) => name.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ namespace Elsa.Secrets.Persistence.EFCore;
|
|||
|
||||
internal class SecretConfiguration : IEntityTypeConfiguration<Secret>
|
||||
{
|
||||
public const string NormalizedNamePropertyName = "NormalizedName";
|
||||
|
||||
public void Configure(EntityTypeBuilder<Secret> builder)
|
||||
{
|
||||
builder.HasKey(x => x.Id);
|
||||
|
|
@ -15,12 +17,13 @@ internal class SecretConfiguration : IEntityTypeConfiguration<Secret>
|
|||
builder.Property<string>(SecretSerialization.SerializedTagsPropertyName).HasColumnName("Tags").IsRequired();
|
||||
builder.Property<string>(SecretSerialization.SerializedVersionsPropertyName).HasColumnName("Versions").IsRequired();
|
||||
builder.Property(x => x.Name).HasMaxLength(200).IsRequired();
|
||||
builder.Property<string>(NormalizedNamePropertyName).HasMaxLength(200).IsRequired();
|
||||
builder.Property(x => x.DisplayName).HasMaxLength(200).IsRequired();
|
||||
builder.Property(x => x.TypeName).HasMaxLength(100).IsRequired();
|
||||
builder.Property(x => x.StoreName).HasMaxLength(100).IsRequired();
|
||||
builder.Property(x => x.Scope).HasMaxLength(200);
|
||||
builder.Property(x => x.Status).HasConversion<string>().HasMaxLength(32).IsRequired();
|
||||
builder.HasIndex(x => x.Name).HasDatabaseName($"IX_{nameof(Secret)}_{nameof(Secret.Name)}").IsUnique();
|
||||
builder.HasIndex(NormalizedNamePropertyName).HasDatabaseName($"IX_{nameof(Secret)}_{NormalizedNamePropertyName}").IsUnique();
|
||||
builder.HasIndex(x => x.TypeName).HasDatabaseName($"IX_{nameof(Secret)}_{nameof(Secret.TypeName)}");
|
||||
builder.HasIndex(x => x.StoreName).HasDatabaseName($"IX_{nameof(Secret)}_{nameof(Secret.StoreName)}");
|
||||
builder.HasIndex(x => x.Scope).HasDatabaseName($"IX_{nameof(Secret)}_{nameof(Secret.Scope)}");
|
||||
|
|
|
|||
|
|
@ -92,11 +92,14 @@ public class EFCoreSecretRepositoryTests : IAsyncLifetime
|
|||
await repository.AddAsync(new Secret { Name = "SMTP:PASSWORD", DisplayName = "SMTP password" });
|
||||
|
||||
var reloaded = await repository.GetAsync("smtp:password");
|
||||
var whitespaceReloaded = await repository.GetAsync(" SMTP:PASSWORD ");
|
||||
var activeReplacementResult = await repository.TryAddOrReplaceDeletedAsync(new Secret { Name = "smtp:password", DisplayName = "Replacement password" });
|
||||
var duplicateException = await Assert.ThrowsAsync<InvalidOperationException>(() => repository.AddAsync(new Secret { Name = "smtp:password", DisplayName = "Duplicate password" }));
|
||||
|
||||
Assert.NotNull(reloaded);
|
||||
Assert.NotNull(whitespaceReloaded);
|
||||
Assert.Equal("SMTP:PASSWORD", reloaded.Name);
|
||||
Assert.Equal(reloaded.Id, whitespaceReloaded.Id);
|
||||
Assert.False(activeReplacementResult);
|
||||
Assert.Equal("A secret named 'smtp:password' already exists.", duplicateException.Message);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue