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:
Sipke Schoorstra 2024-10-30 09:19:14 +01:00 committed by GitHub
parent 123a95c07c
commit a6ff1f5506
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 40 additions and 48 deletions

View file

@ -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;

View file

@ -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)

View file

@ -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));
}

View file

@ -0,0 +1,5 @@
using Elsa.Mediator.Contracts;
namespace Elsa.Secrets.Management.Notifications;
public record SecretExpired(Secret Secret) : INotification;

View file

@ -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);
}
}
}

View file

@ -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)

View file

@ -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
}

View file

@ -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>"
});
}