Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/encrypt secret properties #4347

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/core/Elsa.Abstractions/Events/SerializingProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Elsa.Services.Models;
using MediatR;

namespace Elsa.Events;

public class SerializingProperty : INotification
{
public IWorkflowBlueprint WorkflowBlueprint { get; }
public string ActivityId { get; }
public string PropertyName { get; }

public SerializingProperty(IWorkflowBlueprint workflowBlueprint, string activityId, string propertyName)
{
WorkflowBlueprint = workflowBlueprint;
ActivityId = activityId;
PropertyName = propertyName;
}

public bool CanSerialize { get; private set; } = true;
public void PreventSerialization() => CanSerialize = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Elsa.Services.Models
{
public class ActivityBlueprintWrapper : IActivityBlueprintWrapper
{
protected ActivityExecutionContext ActivityExecutionContext { get; }
public ActivityExecutionContext ActivityExecutionContext { get; }

public ActivityBlueprintWrapper(ActivityExecutionContext activityExecutionContext)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Elsa.Services.Models
{
public interface IActivityBlueprintWrapper
{
ActivityExecutionContext ActivityExecutionContext { get; }
IActivityBlueprint ActivityBlueprint { get; }
IActivityBlueprintWrapper<TActivity> As<TActivity>() where TActivity : IActivity;
ValueTask<object?> EvaluatePropertyValueAsync(string propertyName, CancellationToken cancellationToken = default);
Expand Down
18 changes: 17 additions & 1 deletion src/core/Elsa.Core/Handlers/PersistActivityPropertyState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ namespace Elsa.Handlers
public class PersistActivityPropertyState : INotificationHandler<ActivityExecuted>
{
private readonly IWorkflowStorageService _workflowStorageService;
private readonly IMediator _mediator;

public PersistActivityPropertyState(IWorkflowStorageService workflowStorageService)
public PersistActivityPropertyState(IWorkflowStorageService workflowStorageService, IMediator mediator)
{
_workflowStorageService = workflowStorageService;
_mediator = mediator;
}

public async Task Handle(ActivityExecuted notification, CancellationToken cancellationToken)
Expand All @@ -35,6 +37,13 @@ public async Task Handle(ActivityExecuted notification, CancellationToken cancel
// Persist input properties.
foreach (var property in inputProperties)
{
var serializingProperty = new SerializingProperty(activityExecutionContext.WorkflowExecutionContext.WorkflowBlueprint, activity.Id, property.Name);
await _mediator.Publish(serializingProperty, cancellationToken);
if (!serializingProperty.CanSerialize)
{
continue;
}

var value = property.GetValue(activity);
var inputAttr = property.GetCustomAttribute<ActivityInputAttribute>();
var defaultProviderName = inputAttr.DefaultWorkflowStorageProvider;
Expand All @@ -44,6 +53,13 @@ public async Task Handle(ActivityExecuted notification, CancellationToken cancel
// Persist output properties.
foreach (var property in outputProperties)
{
var serializingProperty = new SerializingProperty(activityExecutionContext.WorkflowExecutionContext.WorkflowBlueprint, activity.Id, property.Name);
await _mediator.Publish(serializingProperty, cancellationToken);
if (!serializingProperty.CanSerialize)
{
continue;
}

var value = property.GetValue(activity);
var outputAttr = property.GetCustomAttribute<ActivityOutputAttribute>();
var defaultProviderName = outputAttr.DefaultWorkflowStorageProvider;
Expand Down
35 changes: 28 additions & 7 deletions src/core/Elsa.Core/Services/Workflows/ActivityActivator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,27 @@
using System.Threading.Tasks;
using Elsa.Activities.ControlFlow;
using Elsa.Attributes;
using Elsa.Events;
using Elsa.Options;
using Elsa.Providers.WorkflowStorage;
using Elsa.Services.Models;
using Elsa.Services.WorkflowStorage;
using MediatR;
using Microsoft.Extensions.DependencyInjection;

namespace Elsa.Services.Workflows
{
public class ActivityActivator : IActivityActivator
{
private readonly ElsaOptions _elsaOptions;
private readonly IWorkflowStorageService _workflowStorageService;
private readonly IServiceProvider _serviceProvider;

public ActivityActivator(ElsaOptions options, IWorkflowStorageService workflowStorageService)
public ActivityActivator(ElsaOptions options, IWorkflowStorageService workflowStorageService, IServiceProvider serviceProvider)
{
_elsaOptions = options;
_workflowStorageService = workflowStorageService;
_serviceProvider = serviceProvider;
}

public async Task<IActivity> ActivateActivityAsync(ActivityExecutionContext context, Type type, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -82,20 +87,36 @@ private async ValueTask ApplyStoredObjectValuesAsync(ActivityExecutionContext co

private async ValueTask StoreAppliedValuesAsync(ActivityExecutionContext context, IActivity activity, CancellationToken cancellationToken)
{
await StoreAppliedObjectValuesAsync(context, activity, cancellationToken);
await StoreAppliedObjectValuesAsync(context, activity, activity, cancellationToken);
}

private async ValueTask StoreAppliedObjectValuesAsync(ActivityExecutionContext context, object activity, CancellationToken cancellationToken, string? parentName = null)
/// <summary>
/// Recursively store activity's properties
/// </summary>
/// <param name="activity">The parent activity of all the activity properties</param>
/// <param name="nestedInstance">The activity or the recursively generated object from the activity's properties</param>
private async ValueTask StoreAppliedObjectValuesAsync(ActivityExecutionContext context, IActivity activity, object nestedInstance, CancellationToken cancellationToken, string? parentName = null)
sfmskywalker marked this conversation as resolved.
Show resolved Hide resolved
{
var properties = activity.GetType().GetProperties().Where(IsActivityProperty).ToList();
var nestedProperties = activity.GetType().GetProperties().Where(IsActivityObjectProperty).ToList();
using var scope = _serviceProvider.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

var properties = nestedInstance.GetType().GetProperties().Where(IsActivityProperty).ToList();
var nestedProperties = nestedInstance.GetType().GetProperties().Where(IsActivityObjectProperty).ToList();
var propertyStorageProviderDictionary = context.ActivityBlueprint.PropertyStorageProviders;
var workflowStorageContext = new WorkflowStorageContext(context.WorkflowInstance, context.ActivityId);

foreach (var property in properties)
{
var propertyName = parentName == null ? property.Name : $"{parentName}_{property.Name}";
var value = property.GetValue(activity);

var serializingProperty = new SerializingProperty(context.WorkflowExecutionContext.WorkflowBlueprint, activity.Id, propertyName);
await mediator.Publish(serializingProperty, cancellationToken);
if (!serializingProperty.CanSerialize)
{
continue;
}

var value = property.GetValue(nestedInstance);
var attr = property.GetCustomAttributes<ActivityPropertyAttributeBase>().First();
var providerName = propertyStorageProviderDictionary.GetItem(propertyName) ?? attr.DefaultWorkflowStorageProvider;
await _workflowStorageService.SaveAsync(providerName, workflowStorageContext, propertyName, value, cancellationToken);
Expand All @@ -105,7 +126,7 @@ private async ValueTask StoreAppliedObjectValuesAsync(ActivityExecutionContext c
{
var instance = Activator.CreateInstance(nestedProperty.PropertyType);
var propertyName = parentName == null ? nestedProperty.Name : $"{parentName}_{nestedProperty.Name}";
await StoreAppliedObjectValuesAsync(context, instance, cancellationToken, propertyName);
await StoreAppliedObjectValuesAsync(context, activity, instance, cancellationToken, propertyName);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/designer/elsa-workflows-studio/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export namespace Components {
}
interface ElsaSingleLineProperty {
"activityModel": ActivityModel;
"isEncypted"?: boolean;
"propertyDescriptor": ActivityPropertyDescriptor;
"propertyModel": ActivityDefinitionProperty;
}
Expand Down Expand Up @@ -1146,6 +1147,7 @@ declare namespace LocalJSX {
}
interface ElsaSingleLineProperty {
"activityModel"?: ActivityModel;
"isEncypted"?: boolean;
"propertyDescriptor"?: ActivityPropertyDescriptor;
"propertyModel"?: ActivityDefinitionProperty;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class ElsaSingleLineProperty {
@Prop() activityModel: ActivityModel;
@Prop() propertyDescriptor: ActivityPropertyDescriptor;
@Prop() propertyModel: ActivityDefinitionProperty;
@Prop() isEncypted?: boolean;
@State() currentValue: string;

onChange(e: Event) {
Expand All @@ -18,6 +19,14 @@ export class ElsaSingleLineProperty {
this.propertyModel.expressions[defaultSyntax] = this.currentValue = input.value;
}

onFocus(e: Event) {
if (this.isEncypted) {
const input = e.currentTarget as HTMLInputElement;
const defaultSyntax = this.propertyDescriptor.defaultSyntax || SyntaxNames.Literal;
input.value = this.propertyModel.expressions[defaultSyntax] = this.currentValue = "";
}
}

componentWillLoad() {
const defaultSyntax = this.propertyDescriptor.defaultSyntax || SyntaxNames.Literal;
this.currentValue = this.propertyModel.expressions[defaultSyntax] || undefined;
Expand Down Expand Up @@ -54,7 +63,7 @@ export class ElsaSingleLineProperty {
onDefaultSyntaxValueChanged={e => this.onDefaultSyntaxValueChanged(e)}
editor-height="5em"
single-line={true}>
<input type="text" id={fieldId} name={fieldName} value={value} onChange={e => this.onChange(e)}
<input type="text" id={fieldId} name={fieldName} value={value} onFocus={e => this.onFocus(e)} onChange={e => this.onChange(e)}
class="disabled:elsa-opacity-50 disabled:elsa-cursor-not-allowed focus:elsa-ring-blue-500 focus:elsa-border-blue-500 elsa-block elsa-w-full elsa-min-w-0 elsa-rounded-md sm:elsa-text-sm elsa-border-gray-300"
disabled={isReadOnly}/>
</elsa-property-editor>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {getOrCreateProperty} from "../utils/utils";

export class SingleLineDriver implements PropertyDisplayDriver {

display(activity: ActivityModel, property: ActivityPropertyDescriptor) {
display(activity: ActivityModel, property: ActivityPropertyDescriptor, onUpdated?: () => void, isEncypted?: boolean) {
const prop = getOrCreateProperty(activity, property.name);
return <elsa-single-line-property activityModel={activity} propertyDescriptor={property} propertyModel={prop}/>;
return <elsa-single-line-property activityModel={activity} propertyDescriptor={property} propertyModel={prop} isEncypted={isEncypted} />;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { eventBus } from "../../..";
import { WebhookDefinitionSummary } from "../../elsa-webhooks/models";
import Tunnel from "../../../data/dashboard";
import { createElsaSecretsClient, ElsaSecretsClient } from "../services/credential-manager.client";
import { SecretDescriptor, SecretModel } from "../models/secret.model";
import { SecretDefinitionProperty, SecretDescriptor, SecretModel } from "../models/secret.model";
import { SecretEventTypes } from "../models/secret.events";

@Component({
Expand Down Expand Up @@ -68,13 +68,14 @@ export class CredentialManagerListScreen {
await this.showSecretEditorInternal(secretModel, true);
}

mapProperties(properties) {
mapProperties(properties: SecretDefinitionProperty[]) {
return properties.map(prop => {
return {
expressions: {
Literal: prop.expressions.Literal
},
name: prop.name
name: prop.name,
isEncrypted: prop.isEncrypted
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { loadTranslations } from "../../../components/i18n/i18n-loader";
import { eventBus, propertyDisplayManager } from "../../../services";
import { FormContext, textInput } from "../../../utils/forms";
import secretState from "../utils/secret.store";
import { SecretDescriptor, SecretEditorRenderProps, SecretModel, SecretPropertyDescriptor } from "../models/secret.model";
import { SecretDefinitionProperty, SecretDescriptor, SecretEditorRenderProps, SecretModel, SecretPropertyDescriptor } from "../models/secret.model";
import { SecretEventTypes } from "../models/secret.events";
import state from '../../../utils/store';
import { SyntaxNames } from '../../../models';
Expand Down Expand Up @@ -146,6 +146,7 @@ export class ElsaSecretEditorModal {
};

onShowSecretEditor = async (secret: SecretModel, animate: boolean) => {

this.secretModel = JSON.parse(JSON.stringify(secret));
this.secretDescriptor = secretState.secretsDescriptors.find(x => x.type == secret.type);
this.formContext = new FormContext(this.secretModel, newValue => this.secretModel = newValue);
Expand Down Expand Up @@ -250,11 +251,21 @@ export class ElsaSecretEditorModal {
);
}

propertyChanged(property: SecretDefinitionProperty) {
this.updateCounter++

if (property?.isEncrypted) {
property.isEncrypted = false;
}
}

renderPropertyEditor(secret: SecretModel, property: SecretPropertyDescriptor) {
var propertyValue = secret.properties.find(x => x.name === property.name);

const key = `secret-property-input:${secret.id}:${property.name}`;
const display = propertyDisplayManager.display(secret, property);
const display = propertyDisplayManager.display(secret, property, null, propertyValue?.isEncrypted);
const id = `${property.name}Control`;

return <elsa-control key={key} id={id} class="sm:elsa-col-span-6" content={display} onChange={() => this.updateCounter++}/>;
return <elsa-control key={key} id={id} class="sm:elsa-col-span-6" content={display} onChange={() => this.propertyChanged(propertyValue)}/>;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface SecretDefinitionProperty {
syntax?: string;
expressions: Map<string>;
value?: any;
isEncrypted?: boolean;
}

export interface SecretDescriptor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import {SecretModel, SecretPropertyDescriptor} from "../modules/credential-manager/models/secret.model";

export interface PropertyDisplayDriver {
display(model: ActivityModel | SecretModel, property: ActivityPropertyDescriptor | SecretPropertyDescriptor, onUpdated?: () => void)
display(model: ActivityModel | SecretModel, property: ActivityPropertyDescriptor | SecretPropertyDescriptor, onUpdated?: () => void, isEncrypted?: boolean)

update?(model: ActivityModel | SecretModel, property: ActivityPropertyDescriptor | SecretPropertyDescriptor, form: FormData)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export class PropertyDisplayManager {
this.drivers[controlType] = driverFactory;
}

display(model: ActivityModel | SecretModel, property: ActivityPropertyDescriptor | SecretPropertyDescriptor, onUpdated?: () => void) {
display(model: ActivityModel | SecretModel, property: ActivityPropertyDescriptor | SecretPropertyDescriptor, onUpdated?: () => void, isEncrypted?: boolean) {
const driver = this.getDriver(property.uiHint);
return driver.display(model, property, onUpdated);
return driver.display(model, property, onUpdated, isEncrypted);
}

update(model: ActivityModel | SecretModel, property: ActivityPropertyDescriptor | SecretPropertyDescriptor, form: FormData) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Elsa.Secrets.Extensions;
using Elsa.Secrets.Http.Services;
using Elsa.Secrets.Manager;
using Elsa.Secrets.Persistence;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
Expand All @@ -14,21 +15,21 @@ namespace Elsa.Secrets.Api.Endpoints.OAuth2
[Produces("application/json")]
public class SetAuthCodeCallback : Controller
{
private readonly ISecretsStore _secretsStore;
private readonly ISecretsManager _secretsManager;
private readonly IOAuth2TokenService _tokenService;
private readonly IConfiguration _configuration;

public SetAuthCodeCallback(ISecretsStore secretsStore, IOAuth2TokenService tokenService, IConfiguration configuration)
public SetAuthCodeCallback(ISecretsManager secretsManager, IOAuth2TokenService tokenService, IConfiguration configuration)
{
_secretsStore = secretsStore;
_secretsManager = secretsManager;
_tokenService = tokenService;
_configuration = configuration;
}

[HttpGet]
public async Task<ActionResult> Handle(string state, string code, CancellationToken cancellationToken = default)
{
var secret = await _secretsStore.FindByIdAsync(state, cancellationToken);
var secret = await _secretsManager.GetSecretById(state, cancellationToken);

if (secret == null)
return NotFound();
Expand Down
11 changes: 5 additions & 6 deletions src/modules/secrets/Elsa.Secrets.Api/Endpoints/OAuth2/GetUrl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Elsa.Secrets.Extensions;
using Elsa.Secrets.Manager;
using Elsa.Secrets.Models;
using Elsa.Secrets.Persistence;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
Expand All @@ -19,20 +18,20 @@ namespace Elsa.Secrets.Api.Endpoints.OAuth2;
[Produces(MediaTypeNames.Application.Json)]
public class GetUrl : Controller
{
private readonly ISecretsStore _secretStore;
private readonly ISecretsManager _secretsManager;
private readonly IConfiguration _configuration;

public GetUrl(ISecretsStore secretStore, IConfiguration configuration)
public GetUrl(ISecretsManager secretsManager, IConfiguration configuration)
{
_secretStore = secretStore;
_secretsManager = secretsManager;
_configuration = configuration;
}

[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))]
public async Task<ActionResult<IEnumerable<Secret>>> Handle(string secretId, CancellationToken cancellationToken = default)
{
var secret = await _secretStore.FindByIdAsync(secretId, cancellationToken);
var secret = await _secretsManager.GetSecretById(secretId, cancellationToken);
if (secret == null)
return NotFound();

Expand Down
Loading
Loading