Refactor JS expressions for accessing secrets (#6070)
* Add support for secret accessor functions in JavaScript Refactored the rendering and evaluation of JavaScript to include method definitions alongside existing properties. Enhanced secret handling by generating asynchronous accessor functions, improving the clarity and functionality of secret management in scripting. * Refactor secret retrieval logic in JavaScript engine configuration Refactored the code to use a dedicated method `ResolveSecretAsync` for secret retrieval, improving clarity and maintainability. Added a check to ensure that only active secrets are decrypted, enhancing robustness and error handling. * Add SecretExpired notification and mediator to updater Introduces a new SecretExpired notification class and integrates it within DefaultExpiredSecretsUpdater. The updater now sends a SecretExpired notification via the mediator upon expiring a secret.
This commit is contained in:
parent
123a95c07c
commit
a6ff1f5506
|
|
@ -52,7 +52,6 @@ using Proto.Remote.GrpcNet;
|
|||
using StackExchange.Redis;
|
||||
|
||||
// ReSharper disable RedundantAssignment
|
||||
|
||||
const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore;
|
||||
const SqlDatabaseProvider sqlDatabaseProvider = SqlDatabaseProvider.Sqlite;
|
||||
const bool useHangfire = false;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification
|
|||
{
|
||||
options ??= new ExpressionEvaluatorOptions();
|
||||
|
||||
var engineOptions = new Jint.Options();
|
||||
var engineOptions = new Jint.Options
|
||||
{
|
||||
ExperimentalFeatures = ExperimentalFeature.TaskInterop
|
||||
};
|
||||
|
||||
if (_jintOptions.AllowClrAccess)
|
||||
engineOptions.AllowClr();
|
||||
|
|
@ -111,7 +114,7 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification
|
|||
{
|
||||
var preparedScript = GetOrCreatePrepareScript(expression);
|
||||
var result = engine.Evaluate(preparedScript);
|
||||
return result.ToObject();
|
||||
return result.UnwrapIfPromise().ToObject();
|
||||
}
|
||||
|
||||
private Prepared<Script> GetOrCreatePrepareScript(string expression)
|
||||
|
|
|
|||
|
|
@ -26,12 +26,15 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer
|
|||
|
||||
private void Render(FunctionDefinition functionDefinition, StringBuilder output)
|
||||
{
|
||||
string RenderParameter(ParameterDefinition parameter) => $"{parameter.Name}{(parameter.IsOptional ? "?" : "")}: {parameter.Type}";
|
||||
string RenderParameters(IEnumerable<ParameterDefinition> parameters) => string.Join(", ", parameters.Select(RenderParameter));
|
||||
|
||||
var returnType = functionDefinition.ReturnType != null ? $": {functionDefinition.ReturnType}" : "";
|
||||
output.AppendLine($"declare function {functionDefinition.Name}({RenderParameters(functionDefinition.Parameters)}){returnType};");
|
||||
}
|
||||
|
||||
private void RenderMethod(FunctionDefinition functionDefinition, StringBuilder output)
|
||||
{
|
||||
var returnType = functionDefinition.ReturnType != null ? $" => {functionDefinition.ReturnType}" : "";
|
||||
output.AppendLine($"{functionDefinition.Name}: ({RenderParameters(functionDefinition.Parameters)}){returnType};");
|
||||
}
|
||||
|
||||
private void Render(TypeDefinition typeDefinition, StringBuilder output)
|
||||
{
|
||||
|
|
@ -39,11 +42,15 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer
|
|||
|
||||
foreach (var property in typeDefinition.Properties)
|
||||
Render(property, output);
|
||||
|
||||
foreach (var method in typeDefinition.Methods)
|
||||
RenderMethod(method, output);
|
||||
|
||||
output.AppendLine("}");
|
||||
}
|
||||
|
||||
private void Render(PropertyDefinition property, StringBuilder output) => output.AppendLine($"{property.Name}{(property.IsOptional ? "?" : "")}: {property.Type};");
|
||||
|
||||
private void Render(VariableDefinition variable, StringBuilder output) => output.AppendLine($"declare var {variable.Name}: {variable.Type};");
|
||||
string RenderParameter(ParameterDefinition parameter) => $"{parameter.Name}{(parameter.IsOptional ? "?" : "")}: {parameter.Type}";
|
||||
string RenderParameters(IEnumerable<ParameterDefinition> parameters) => string.Join(", ", parameters.Select(RenderParameter));
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
|
||||
namespace Elsa.Secrets.Management.Notifications;
|
||||
|
||||
public record SecretExpired(Secret Secret) : INotification;
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
using Elsa.Common;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Secrets.Management.Notifications;
|
||||
|
||||
namespace Elsa.Secrets.Management;
|
||||
|
||||
public class DefaultExpiredSecretsUpdater(ISecretStore store, ISystemClock systemClock) : IExpiredSecretsUpdater
|
||||
public class DefaultExpiredSecretsUpdater(ISecretStore store, IMediator mediator, ISystemClock systemClock) : IExpiredSecretsUpdater
|
||||
{
|
||||
public async Task UpdateExpiredSecretsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
@ -21,6 +23,7 @@ public class DefaultExpiredSecretsUpdater(ISecretStore store, ISystemClock syste
|
|||
secret.Status = SecretStatus.Expired;
|
||||
secret.UpdatedAt = now;
|
||||
await store.UpdateAsync(secret, cancellationToken);
|
||||
await mediator.SendAsync(new SecretExpired(secret), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ public class DefaultSecretNameGenerator(ISecretNameValidator validator) : ISecre
|
|||
|
||||
while (attempt < maxAttempts)
|
||||
{
|
||||
var name = $"Secret {++attempt}";
|
||||
var name = $"Secret{++attempt}";
|
||||
var isUnique = await validator.IsNameUniqueAsync(name, null, cancellationToken);
|
||||
|
||||
if (isUnique)
|
||||
|
|
|
|||
|
|
@ -1,67 +1,41 @@
|
|||
using System.Dynamic;
|
||||
using System.Text.RegularExpressions;
|
||||
using Elsa.JavaScript.Notifications;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Secrets.Management;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Secrets.Scripting.JavaScript;
|
||||
|
||||
/// A handler that configures the Jint engine with secrets.
|
||||
[UsedImplicitly]
|
||||
public partial class ConfigureEngineWithSecrets(ISecretManager secretManager, IDecryptor decryptor) : INotificationHandler<EvaluatingJavaScript>
|
||||
public class ConfigureEngineWithSecrets(ISecretManager secretManager, IDecryptor decryptor) : INotificationHandler<EvaluatingJavaScript>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await CopySecretsIntoEngineAsync(notification, cancellationToken);
|
||||
await GenerateSecretAccessorFunctions(notification, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task CopySecretsIntoEngineAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
|
||||
private async Task GenerateSecretAccessorFunctions(EvaluatingJavaScript notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var engine = notification.Engine;
|
||||
var expression = notification.Expression;
|
||||
var secretNames = GetSecretNamesFromExpression(expression);
|
||||
var secrets = (await secretManager.ListAsync(cancellationToken)).ToList();
|
||||
|
||||
if (secretNames.Count == 0)
|
||||
return;
|
||||
|
||||
var filter = new SecretFilter
|
||||
{
|
||||
Names = secretNames,
|
||||
Status = SecretStatus.Active
|
||||
};
|
||||
var secrets = await secretManager.FindManyAsync(filter, cancellationToken);
|
||||
IDictionary<string, object?> secretsContainer = new ExpandoObject();
|
||||
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
var secretValue = await decryptor.DecryptAsync(secret.EncryptedValue, cancellationToken);
|
||||
secretsContainer[secret.Name] = secretValue;
|
||||
secretsContainer[$"get{secret.Name.Pascalize()}Async"] = () => ResolveSecretAsync(secret, cancellationToken);
|
||||
}
|
||||
|
||||
engine.SetValue("secrets", secretsContainer);
|
||||
}
|
||||
|
||||
private ICollection<string> GetSecretNamesFromExpression(string expression)
|
||||
private async Task<string> ResolveSecretAsync(Secret secret, CancellationToken cancellationToken)
|
||||
{
|
||||
var secretNames = new List<string>();
|
||||
|
||||
#if NET6_0
|
||||
const string pattern = @"(?<=secrets\.)\w+";
|
||||
var matches = Regex.Matches(expression, pattern);
|
||||
#elif NET7_0_OR_GREATER
|
||||
var matches = SecretsRegex().Matches(expression);
|
||||
#endif
|
||||
|
||||
foreach (Match match in matches)
|
||||
secretNames.Add(match.Value);
|
||||
|
||||
return secretNames;
|
||||
if (secret.Status != SecretStatus.Active)
|
||||
throw new InvalidOperationException($"Secret '{secret.Name}' is not active.");
|
||||
return await decryptor.DecryptAsync(secret.EncryptedValue, cancellationToken);
|
||||
}
|
||||
|
||||
#if NET7_0_OR_GREATER
|
||||
[GeneratedRegex(@"(?<=secrets\.)\w+")]
|
||||
private static partial Regex SecretsRegex();
|
||||
#endif
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ using Elsa.JavaScript.TypeDefinitions.Builders;
|
|||
using Elsa.JavaScript.TypeDefinitions.Contracts;
|
||||
using Elsa.JavaScript.TypeDefinitions.Models;
|
||||
using Elsa.Secrets.Management;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Secrets.Scripting.JavaScript;
|
||||
|
|
@ -26,10 +27,10 @@ internal class SecretsTypeDefinitionProvider(ISecretManager secretManager) : ITy
|
|||
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
secretsContainerClass.Properties.Add(new PropertyDefinition
|
||||
secretsContainerClass.Methods.Add(new FunctionDefinition
|
||||
{
|
||||
Name = secret.Name,
|
||||
Type = "string"
|
||||
Name = $"get{secret.Name.Pascalize()}Async",
|
||||
ReturnType = "Promise<string>"
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue