@if (Model.AvailableCertificates.Count != 0)
{
@@ -439,6 +471,30 @@
allowRefreshTokenFlow.parent().parent().collapse("hide");
}
}
+
+ $('#@Html.IdFor(m => m.EncryptionRsaSecret)').on('change', function () {
+ if (this.value === '') {
+ $('#@Html.IdFor(m => m.EncryptionCertificateStoreLocation)').closest('.certificate').show();
+ $('#@Html.IdFor(m => m.EncryptionCertificateStoreName)').closest('.certificate').show();
+ $('#@Html.IdFor(m => m.EncryptionCertificateThumbprint)').closest('.certificate').show();
+ } else {
+ $('#@Html.IdFor(m => m.EncryptionCertificateStoreLocation)').closest('.certificate').hide();
+ $('#@Html.IdFor(m => m.EncryptionCertificateStoreName)').closest('.certificate').hide();
+ $('#@Html.IdFor(m => m.EncryptionCertificateThumbprint)').closest('.certificate').hide();
+ }
+ }).trigger('change');
+
+ $('#@Html.IdFor(m => m.SigningRsaSecret)').on('change', function () {
+ if (this.value === '') {
+ $('#@Html.IdFor(m => m.SigningCertificateStoreLocation)').closest('.certificate').show();
+ $('#@Html.IdFor(m => m.SigningCertificateStoreName)').closest('.certificate').show();
+ $('#@Html.IdFor(m => m.SigningCertificateThumbprint)').closest('.certificate').show();
+ } else {
+ $('#@Html.IdFor(m => m.SigningCertificateStoreLocation)').closest('.certificate').hide();
+ $('#@Html.IdFor(m => m.SigningCertificateStoreName)').closest('.certificate').hide();
+ $('#@Html.IdFor(m => m.SigningCertificateThumbprint)').closest('.certificate').hide();
+ }
+ }).trigger('change');
};
//]]>
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Manifest.cs
new file mode 100644
index 00000000000..dc61646c2b9
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Manifest.cs
@@ -0,0 +1,11 @@
+using OrchardCore.Modules.Manifest;
+
+[assembly: Module(
+ Author = ManifestConstants.OrchardCoreTeam,
+ Website = ManifestConstants.OrchardCoreWebsite,
+ Version = ManifestConstants.OrchardCoreVersion,
+ Name = "Azure KeyVault Secrets Store",
+ Description = "The Azure KeyVault Secrets module provides a KeyVault store for secrets.",
+ Category = "Configuration",
+ Dependencies = ["OrchardCore.Liquid"]
+)]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Models/SecretsKeyVaultOptions.cs b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Models/SecretsKeyVaultOptions.cs
new file mode 100644
index 00000000000..99a05342599
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Models/SecretsKeyVaultOptions.cs
@@ -0,0 +1,7 @@
+namespace OrchardCore.Secrets.Azure.Models;
+
+public class SecretsKeyVaultOptions
+{
+ public string KeyVaultName { get; set; }
+ public string Prefix { get; set; }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/OrchardCore.Secrets.Azure.csproj b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/OrchardCore.Secrets.Azure.csproj
new file mode 100644
index 00000000000..b68571f6a9a
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/OrchardCore.Secrets.Azure.csproj
@@ -0,0 +1,29 @@
+
+
+
+
+ OrchardCore Response Compression
+ $(OCFrameworkDescription)
+
+ The Azure KeyVault Secrets module provides a KeyVault store for secrets.
+
+ $(PackageTags) OrchardCoreCMS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Services/KeyVaultClientService.cs b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Services/KeyVaultClientService.cs
new file mode 100644
index 00000000000..7aad8b9fb16
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Services/KeyVaultClientService.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Identity;
+using Azure.Security.KeyVault.Secrets;
+using Microsoft.Extensions.Options;
+using OrchardCore.Secrets.Azure.Models;
+
+namespace OrchardCore.Secrets.Azure.Services;
+
+public class KeyVaultClientService
+{
+ private readonly SecretClient _client;
+ private readonly string _prefix;
+
+ public KeyVaultClientService(IOptions
options)
+ {
+ var keyVaultEndpointUri = new Uri($"https://{options.Value.KeyVaultName}.vault.azure.net");
+
+ _client = new SecretClient(
+ keyVaultEndpointUri,
+ new DefaultAzureCredential(new DefaultAzureCredentialOptions { ExcludeVisualStudioCodeCredential = true }));
+
+ _prefix = options.Value.Prefix;
+ }
+
+ public async Task GetSecretAsync(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ return null;
+ }
+
+ if (!string.IsNullOrEmpty(_prefix))
+ {
+ name = $"{_prefix}{name}";
+ }
+
+ try
+ {
+ var secret = await _client.GetSecretAsync(name);
+ return secret.Value.Value;
+ }
+ catch (RequestFailedException ex) when (ex.ErrorCode == "SecretNotFound")
+ {
+ return null;
+ }
+ }
+
+ public async Task SetSecretAsync(string name, string secretValue)
+ {
+ if (!string.IsNullOrEmpty(_prefix))
+ {
+ name = $"{_prefix}{name}";
+ }
+
+ await _client.SetSecretAsync(name, secretValue);
+ }
+
+ public async Task RemoveSecretAsync(string name)
+ {
+ try
+ {
+ await _client.StartDeleteSecretAsync(name);
+ }
+ catch (RequestFailedException ex) when (ex.ErrorCode == "SecretNotFound")
+ {
+ }
+
+ // Purging on deletion is not supported, the retention period should be configured
+ // on any key vault, knowing that the 'soft-delete' feature will be mandatory soon.
+ // See https://learn.microsoft.com/en-us/azure/key-vault/general/soft-delete-change
+
+ // await operation.WaitForCompletionAsync();
+ // await _client.PurgeDeletedSecretAsync(operation.Value.Name);
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Services/KeyVaultOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Services/KeyVaultOptionsConfiguration.cs
new file mode 100644
index 00000000000..ac4b8d2f7db
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Services/KeyVaultOptionsConfiguration.cs
@@ -0,0 +1,81 @@
+using System;
+using Fluid;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.Environment.Shell.Configuration;
+using OrchardCore.Modules;
+using OrchardCore.Secrets.Azure.Models;
+
+namespace OrchardCore.Secrets.Azure.Services;
+
+public class KeyVaultOptionsConfiguration : IConfigureOptions
+{
+ private readonly IShellConfiguration _shellConfiguration;
+ private readonly ShellSettings _shellSettings;
+ private readonly ILogger _logger;
+
+ // Local instance since it can be discarded once the startup is over.
+ private readonly FluidParser _fluidParser = new();
+
+ public KeyVaultOptionsConfiguration(
+ IShellConfiguration shellConfiguration,
+ ShellSettings shellSettings,
+ ILogger logger)
+ {
+ _shellConfiguration = shellConfiguration;
+ _shellSettings = shellSettings;
+ _logger = logger;
+ }
+
+ public void Configure(SecretsKeyVaultOptions options)
+ {
+ var section = _shellConfiguration.GetSection("OrchardCore_Secrets:KeyVault");
+
+ options.KeyVaultName = section.GetValue(nameof(options.KeyVaultName), string.Empty);
+ options.Prefix = section.GetValue(nameof(options.Prefix), string.Empty);
+
+ var templateOptions = new TemplateOptions();
+ var templateContext = new TemplateContext(templateOptions);
+ templateOptions.MemberAccessStrategy.Register();
+ templateOptions.MemberAccessStrategy.Register();
+ templateContext.SetValue("ShellSettings", _shellSettings);
+
+ ParseKeyVaultName(options, templateContext);
+ ParsePrefix(options, templateContext);
+ }
+
+ private void ParseKeyVaultName(SecretsKeyVaultOptions options, TemplateContext templateContext)
+ {
+ // Use Fluid directly as this is transient and cannot invoke _liquidTemplateManager.
+ try
+ {
+ var template = _fluidParser.Parse(options.KeyVaultName);
+
+ // Container name must be lowercase.
+ options.KeyVaultName = template.Render(templateContext, NullEncoder.Default).ToLower();
+ options.KeyVaultName = options.KeyVaultName.Replace("\r", string.Empty).Replace("\n", string.Empty);
+ }
+ catch (Exception e) when (!e.IsFatal())
+ {
+ _logger.LogCritical(e, "Unable to parse Azure KeyVault Secrets KeyVaultName.");
+ throw;
+ }
+ }
+
+ private void ParsePrefix(SecretsKeyVaultOptions options, TemplateContext templateContext)
+ {
+ try
+ {
+ var template = _fluidParser.Parse(options.Prefix);
+ options.Prefix = template.Render(templateContext, NullEncoder.Default);
+ options.Prefix = options.Prefix.Replace("\r", string.Empty).Replace("\n", string.Empty);
+ }
+ catch (Exception e) when (!e.IsFatal())
+ {
+ _logger.LogCritical(e, "Unable to parse Azure KeyVault Secrets KeyVault Prefix.");
+ throw;
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Services/KeyVaultSecretStore.cs b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Services/KeyVaultSecretStore.cs
new file mode 100644
index 00000000000..ae41689468d
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Services/KeyVaultSecretStore.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Localization;
+using Newtonsoft.Json;
+using OrchardCore.Secrets.Models;
+
+namespace OrchardCore.Secrets.Azure.Services;
+
+public class KeyVaultSecretStore : ISecretStore
+{
+ private readonly KeyVaultClientService _keyVaultClientService;
+ protected readonly IStringLocalizer S;
+
+ public KeyVaultSecretStore(
+ KeyVaultClientService keyVaultClientService,
+ IStringLocalizer stringLocalizer)
+ {
+ _keyVaultClientService = keyVaultClientService;
+ S = stringLocalizer;
+ }
+
+ public string Name => nameof(KeyVaultSecretStore);
+ public string DisplayName => S["KeyVault Secrets Store"];
+ public bool IsReadOnly => false;
+
+ public async Task GetSecretAsync(string name, Type type)
+ {
+ if (!typeof(SecretBase).IsAssignableFrom(type))
+ {
+ throw new ArgumentException("The type must implement " + nameof(SecretBase));
+ }
+
+ var value = await _keyVaultClientService.GetSecretAsync(name);
+ if (string.IsNullOrEmpty(value))
+ {
+ return null;
+ }
+
+ return JsonConvert.DeserializeObject(value, type) as SecretBase;
+ }
+
+ public Task UpdateSecretAsync(string name, SecretBase secret)
+ {
+ var value = JsonConvert.SerializeObject(secret);
+ return _keyVaultClientService.SetSecretAsync(name, value);
+ }
+
+ public Task RemoveSecretAsync(string name) => _keyVaultClientService.RemoveSecretAsync(name);
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Startup.cs
new file mode 100644
index 00000000000..63dcf093d14
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets.KeyVault/Startup.cs
@@ -0,0 +1,17 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using OrchardCore.Modules;
+using OrchardCore.Secrets.Azure.Models;
+using OrchardCore.Secrets.Azure.Services;
+
+namespace OrchardCore.Secrets.Azure;
+
+public class Startup : StartupBase
+{
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddTransient, KeyVaultOptionsConfiguration>();
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/AdminMenu.cs
new file mode 100644
index 00000000000..ff489806f71
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/AdminMenu.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Localization;
+using OrchardCore.Navigation;
+
+namespace OrchardCore.Secrets
+{
+ public class AdminMenu : INavigationProvider
+ {
+ protected readonly IStringLocalizer S;
+
+ public AdminMenu(IStringLocalizer localizer) => S = localizer;
+
+ public Task BuildNavigationAsync(string name, NavigationBuilder builder)
+ {
+ if (!string.Equals(name, "admin", StringComparison.OrdinalIgnoreCase))
+ {
+ return Task.CompletedTask;
+ }
+
+ builder
+ .Add(S["Configuration"], design => design
+ .Add(S["Secrets"], "Secrets".PrefixPosition(), import => import
+ .Action("Index", "Admin", new { area = "OrchardCore.Secrets" })
+ .Permission(Permissions.ManageSecrets)
+ .LocalNav()
+ )
+ );
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Controllers/AdminController.cs
new file mode 100644
index 00000000000..d6db3b54fac
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Controllers/AdminController.cs
@@ -0,0 +1,344 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Localization;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Options;
+using OrchardCore.Admin;
+using OrchardCore.DisplayManagement;
+using OrchardCore.DisplayManagement.ModelBinding;
+using OrchardCore.DisplayManagement.Notify;
+using OrchardCore.Modules;
+using OrchardCore.Navigation;
+using OrchardCore.Routing;
+using OrchardCore.Secrets.Models;
+using OrchardCore.Secrets.Options;
+using OrchardCore.Secrets.ViewModels;
+using OrchardCore.Settings;
+
+namespace OrchardCore.Secrets.Controllers;
+
+[Admin]
+public class AdminController : Controller
+{
+ private readonly ISecretService _secretService;
+ private readonly IAuthorizationService _authorizationService;
+ private readonly IDisplayManager _displayManager;
+ private readonly IUpdateModelAccessor _updateModelAccessor;
+ private readonly SecretOptions _secretOptions;
+ private readonly ISiteService _siteService;
+ private readonly INotifier _notifier;
+
+ protected readonly dynamic New;
+ protected readonly IStringLocalizer S;
+ protected readonly IHtmlLocalizer H;
+
+ public AdminController(
+ ISecretService secretService,
+ IAuthorizationService authorizationService,
+ IDisplayManager displayManager,
+ IUpdateModelAccessor updateModelAccessor,
+ IOptions secretOptions,
+ ISiteService siteService,
+ INotifier notifier,
+ IShapeFactory shapeFactory,
+ IStringLocalizer stringLocalizer,
+ IHtmlLocalizer htmlLocalizer)
+ {
+ _authorizationService = authorizationService;
+ _secretService = secretService;
+ _displayManager = displayManager;
+ _updateModelAccessor = updateModelAccessor;
+ _secretOptions = secretOptions.Value;
+ _siteService = siteService;
+ _notifier = notifier;
+ New = shapeFactory;
+ S = stringLocalizer;
+ H = htmlLocalizer;
+ }
+
+ public async Task Index(ContentOptions options, PagerParameters pagerParameters)
+ {
+ if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageSecrets))
+ {
+ return Forbid();
+ }
+
+ var siteSettings = await _siteService.GetSiteSettingsAsync();
+ var pager = new Pager(pagerParameters, siteSettings.PageSize);
+
+ var secretInfos = (await _secretService.GetSecretInfosAsync()).ToList();
+ if (!string.IsNullOrWhiteSpace(options.Search))
+ {
+ secretInfos = secretInfos.Where(kv => kv.Key.Contains(options.Search)).ToList();
+ }
+
+ var count = secretInfos.Count;
+
+ secretInfos = secretInfos
+ .OrderBy(kv => kv.Key)
+ .Skip(pager.GetStartIndex())
+ .Take(pager.PageSize)
+ .ToList();
+
+ var pagerShape = (await New.Pager(pager)).TotalItemCount(count);
+
+ var thumbnails = new Dictionary();
+ foreach (var type in _secretOptions.Types)
+ {
+ var secret = _secretService.CreateSecret(type.Name);
+ dynamic thumbnail = await _displayManager.BuildDisplayAsync(secret, _updateModelAccessor.ModelUpdater, "Thumbnail");
+ thumbnail.Secret = secret;
+ thumbnails.Add(type.Name, thumbnail);
+ }
+
+ var entries = new List();
+ foreach (var secretInfo in secretInfos)
+ {
+ var secret = await _secretService.GetSecretAsync(secretInfo.Value.Name);
+ if (secret is null)
+ {
+ continue;
+ }
+
+ dynamic summary = await _displayManager.BuildDisplayAsync(secret, _updateModelAccessor.ModelUpdater, "Summary");
+ summary.Secret = secret;
+ entries.Add(new SecretInfoEntry
+ {
+ Name = secretInfo.Key,
+ Info = secretInfo.Value,
+ Summary = summary,
+ });
+ };
+
+ var model = new SecretIndexViewModel
+ {
+ Entries = entries,
+ Thumbnails = thumbnails,
+ Options = options,
+ Pager = pagerShape,
+ };
+
+ model.Options.ContentsBulkAction =
+ [
+ new() { Text = S["Delete"], Value = nameof(ContentsBulkAction.Remove) },
+ ];
+
+ return View("Index", model);
+ }
+
+ [HttpPost, ActionName("Index")]
+ [FormValueRequired("submit.BulkAction")]
+ public async Task IndexPost(ContentOptions options, IEnumerable itemIds)
+ {
+ if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageSecrets))
+ {
+ return Forbid();
+ }
+
+ if (itemIds is not null && itemIds.Any())
+ {
+ switch (options.BulkAction)
+ {
+ case ContentsBulkAction.None:
+ break;
+ case ContentsBulkAction.Remove:
+ foreach (var itemId in itemIds)
+ {
+ await _secretService.RemoveSecretAsync(itemId);
+ }
+
+ await _notifier.SuccessAsync(H["Secrets successfully removed."]);
+ break;
+ default:
+ throw new InvalidOperationException($"Invalid bulk action '{options.BulkAction}'.");
+ }
+ }
+
+ return RedirectToAction("Index");
+ }
+
+ public async Task Create(string type)
+ {
+ if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageSecrets))
+ {
+ return Forbid();
+ }
+
+ var secret = _secretService.CreateSecret(type);
+ if (secret is null)
+ {
+ return NotFound();
+ }
+
+ var model = new SecretInfoViewModel
+ {
+ Editor = await _displayManager.BuildEditorAsync(secret, _updateModelAccessor.ModelUpdater, isNew: true, "", ""),
+ StoreInfos = _secretService.GetSecretStoreInfos(),
+ Type = type,
+ };
+
+ return View(model);
+ }
+
+ [HttpPost, ActionName("Create")]
+ public async Task CreatePost(SecretInfoViewModel model)
+ {
+ if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageSecrets))
+ {
+ return Forbid();
+ }
+
+ var secret = _secretService.CreateSecret(model.Type);
+ if (secret is null)
+ {
+ return NotFound();
+ }
+
+ if (ModelState.IsValid)
+ {
+ await ValidateViewModelAsync(model);
+ }
+
+ dynamic editor = await _displayManager.UpdateEditorAsync(secret, updater: _updateModelAccessor.ModelUpdater, isNew: true, "", "");
+
+ if (ModelState.IsValid)
+ {
+ var info = new SecretInfo
+ {
+ Name = model.Name,
+ Store = model.SelectedStore,
+ Description = model.Description,
+ Type = model.Type,
+ };
+
+ await _secretService.UpdateSecretAsync(secret, info);
+ await _notifier.SuccessAsync(H["Secret added successfully."]);
+
+ return RedirectToAction(nameof(Index));
+ }
+
+ model.Editor = editor;
+ model.StoreInfos = _secretService.GetSecretStoreInfos();
+
+ // If we got this far, something failed, redisplay form.
+ return View(model);
+ }
+
+ public async Task Edit(string name)
+ {
+ if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageSecrets))
+ {
+ return Forbid();
+ }
+
+ var secretInfos = await _secretService.GetSecretInfosAsync();
+ if (!secretInfos.TryGetValue(name, out var secretInfo))
+ {
+ return RedirectToAction(nameof(Create), new { name });
+ }
+
+ var secret = await _secretService.GetSecretAsync(secretInfo.Name);
+
+ var model = new SecretInfoViewModel
+ {
+ Name = name,
+ SelectedStore = secretInfo.Store,
+ Description = secretInfo.Description,
+ Editor = await _displayManager.BuildEditorAsync(secret, _updateModelAccessor.ModelUpdater, isNew: false, "", ""),
+ StoreInfos = _secretService.GetSecretStoreInfos(),
+ Type = secretInfo.Type,
+ };
+
+ return View(model);
+ }
+
+ [HttpPost]
+ public async Task Edit(string sourceName, SecretInfoViewModel model)
+ {
+ if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageSecrets))
+ {
+ return Forbid();
+ }
+
+ if (ModelState.IsValid)
+ {
+ await ValidateViewModelAsync(model, sourceName);
+ }
+
+ var secret = await _secretService.GetSecretAsync(sourceName);
+ if (secret is null)
+ {
+ return NotFound();
+ }
+
+ var editor = await _displayManager.UpdateEditorAsync(secret, updater: _updateModelAccessor.ModelUpdater, isNew: false, "", "");
+ model.Editor = editor;
+
+ if (ModelState.IsValid)
+ {
+ var info = new SecretInfo
+ {
+ Name = model.Name,
+ Store = model.SelectedStore,
+ Description = model.Description,
+ Type = model.Type,
+ };
+
+ await _secretService.UpdateSecretAsync(secret, info, sourceName);
+
+ return RedirectToAction(nameof(Index));
+ }
+
+ // If we got this far, something failed, redisplay form.
+ model.StoreInfos = _secretService.GetSecretStoreInfos();
+
+ // Prevent a page not found on the next post.
+ model.Name = sourceName;
+
+ return View(model);
+ }
+
+ [HttpPost]
+ public async Task Delete(string name)
+ {
+ if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageSecrets))
+ {
+ return Forbid();
+ }
+
+ if (!await _secretService.RemoveSecretAsync(name))
+ {
+ return NotFound();
+ }
+
+ await _notifier.SuccessAsync(H["Secret deleted successfully."]);
+
+ return RedirectToAction(nameof(Index));
+ }
+
+ private async Task ValidateViewModelAsync(SecretInfoViewModel model, string sourceName = null)
+ {
+ if (string.IsNullOrWhiteSpace(model.Name))
+ {
+ ModelState.AddModelError(nameof(SecretInfoViewModel.Name), S["The secret name is mandatory."]);
+ }
+
+ if (sourceName is null || !model.Name.EqualsOrdinalIgnoreCase(sourceName))
+ {
+ if (!model.Name.EqualsOrdinalIgnoreCase(model.Name.ToSafeSecretName()))
+ {
+ ModelState.AddModelError(nameof(SecretInfoViewModel.Name), S["The secret name contains invalid characters."]);
+ }
+
+ var secretInfos = await _secretService.LoadSecretInfosAsync();
+ if (secretInfos.ContainsKey(model.Name))
+ {
+ ModelState.AddModelError(nameof(SecretInfoViewModel.Name), S["A secret with the same name already exists."]);
+ }
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Deployment/AllSecretsDeploymentSource.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Deployment/AllSecretsDeploymentSource.cs
new file mode 100644
index 00000000000..d1e8f3137a8
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Deployment/AllSecretsDeploymentSource.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using OrchardCore.Deployment;
+using OrchardCore.Modules;
+
+namespace OrchardCore.Secrets.Deployment;
+
+public class AllSecretsDeploymentSource : IDeploymentSource
+{
+ private readonly ISecretService _secretService;
+ private readonly ISecretProtectionProvider _protectionProvider;
+
+ public AllSecretsDeploymentSource(ISecretService secretService, ISecretProtectionProvider protectionProvider)
+ {
+ _secretService = secretService;
+ _protectionProvider = protectionProvider;
+ }
+
+ public async Task ProcessDeploymentStepAsync(DeploymentStep deploymentStep, DeploymentPlanResult result)
+ {
+ if (deploymentStep is not AllSecretsDeploymentStep allSecretsDeploymentStep)
+ {
+ return;
+ }
+
+ // Deployment secrets should already exist on both sides.
+ var secretInfos = (await _secretService.GetSecretInfosAsync())
+ .Where(secret =>
+ !secret.Value.Name.EqualsOrdinalIgnoreCase($"{result.SecretNamespace}.Encryption") &&
+ !secret.Value.Name.EqualsOrdinalIgnoreCase($"{result.SecretNamespace}.Signing"));
+
+ if (!secretInfos.Any())
+ {
+ return;
+ }
+
+ var protector = _protectionProvider.CreateProtector(result.SecretNamespace);
+
+ var secrets = new Dictionary();
+ foreach (var secretInfo in secretInfos)
+ {
+ var store = _secretService.GetSecretStoreInfos().FirstOrDefault(store =>
+ string.Equals(store.Name, secretInfo.Value.Store, StringComparison.OrdinalIgnoreCase));
+
+ if (store is null)
+ {
+ continue;
+ }
+
+ var jsonSecretInfo = JObject.FromObject(secretInfo.Value);
+
+ // Cleanup secret names that will be deduced from their keys.
+ jsonSecretInfo.Remove("Name");
+
+ var jObject = new JObject(new JProperty("SecretInfo", jsonSecretInfo));
+
+ // When the store is readonly we ship the secret info without the secret value.
+ if (!store.IsReadOnly)
+ {
+ var secret = await _secretService.GetSecretAsync(secretInfo.Value.Name);
+ if (secret is not null)
+ {
+ var plaintext = JsonConvert.SerializeObject(secret);
+ var encrypted = await protector.ProtectAsync(plaintext);
+ jObject.Add("SecretData", encrypted);
+ }
+ }
+
+ secrets.Add(secretInfo.Key, jObject);
+ }
+
+ result.Steps.Add(new JObject(
+ new JProperty("name", "Secrets"),
+ new JProperty("Secrets", JObject.FromObject(secrets))
+ ));
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Deployment/AllSecretsDeploymentStep.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Deployment/AllSecretsDeploymentStep.cs
new file mode 100644
index 00000000000..fcb91c91e3b
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Deployment/AllSecretsDeploymentStep.cs
@@ -0,0 +1,11 @@
+using OrchardCore.Deployment;
+
+namespace OrchardCore.Secrets.Deployment;
+
+public class AllSecretsDeploymentStep : DeploymentStep
+{
+ public AllSecretsDeploymentStep()
+ {
+ Name = "AllSecretsDeploymentStep";
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Deployment/AllSecretsDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Deployment/AllSecretsDeploymentStepDriver.cs
new file mode 100644
index 00000000000..ee110b13df8
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Deployment/AllSecretsDeploymentStepDriver.cs
@@ -0,0 +1,20 @@
+using OrchardCore.Deployment;
+using OrchardCore.DisplayManagement.Handlers;
+using OrchardCore.DisplayManagement.Views;
+
+namespace OrchardCore.Secrets.Deployment;
+
+public class AllSecretsDeploymentStepDriver : DisplayDriver
+{
+ public override IDisplayResult Display(AllSecretsDeploymentStep step)
+ {
+ return
+ Combine(
+ View("AllSecretsDeploymentStep_Fields_Summary", step).Location("Summary", "Content"),
+ View("AllSecretsDeploymentStep_Fields_Thumbnail", step).Location("Thumbnail", "Content")
+ );
+ }
+
+ public override IDisplayResult Edit(AllSecretsDeploymentStep step) =>
+ View("AllSecretsDeploymentStep_Fields_Edit", step).Location("Content");
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Drivers/RSASecretDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Drivers/RSASecretDisplayDriver.cs
new file mode 100644
index 00000000000..e1ac17706c8
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Drivers/RSASecretDisplayDriver.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.Extensions.Localization;
+using OrchardCore.DisplayManagement.Handlers;
+using OrchardCore.DisplayManagement.Views;
+using OrchardCore.Mvc.ModelBinding;
+using OrchardCore.Secrets.Models;
+using OrchardCore.Secrets.ViewModels;
+
+namespace OrchardCore.Secrets.Drivers;
+
+public class RSASecretDisplayDriver : DisplayDriver
+{
+ protected readonly IStringLocalizer S;
+
+ public RSASecretDisplayDriver(IStringLocalizer stringLocalizer) => S = stringLocalizer;
+
+ public override IDisplayResult Display(RSASecret secret)
+ {
+ return Combine(
+ View("RsaSecret_Fields_Summary", secret).Location("Summary", "Content"),
+ View("RsaSecret_Fields_Thumbnail", secret).Location("Thumbnail", "Content"));
+ }
+
+ public override Task EditAsync(RSASecret secret, BuildEditorContext context)
+ {
+ return Task.FromResult(Initialize("RsaSecret_Fields_Edit", model =>
+ {
+ // Generate new keys when creating.
+ if (context.IsNew)
+ {
+ using var rsa = RSAGenerator.GenerateRSASecurityKey(2048);
+ if (string.IsNullOrEmpty(secret.PublicKey))
+ {
+ model.PublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
+ }
+ else
+ {
+ model.PublicKey = secret.PublicKey;
+ }
+
+ if (string.IsNullOrEmpty(secret.PrivateKey))
+ {
+ model.PrivateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey());
+ }
+ else
+ {
+ model.PrivateKey = secret.PrivateKey;
+ }
+ }
+ else
+ {
+ // The private key is never returned to the view when editing.
+ model.PublicKey = secret.PublicKey;
+
+ using var rsa = RSAGenerator.GenerateRSASecurityKey(2048);
+ model.NewPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
+ model.NewPrivateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey());
+ }
+
+ model.KeyType = secret.KeyType;
+ model.KeyTypes = new List
+ {
+ new()
+ {
+ Text = S["Public Key"],
+ Value = RSAKeyType.Public.ToString(),
+ Selected = model.KeyType == RSAKeyType.Public,
+ },
+ new()
+ {
+ Text = S["Public / Private Key Pair"],
+ Value = RSAKeyType.PublicPrivate.ToString(),
+ Selected = model.KeyType == RSAKeyType.PublicPrivate,
+ },
+ };
+ model.Context = context;
+ })
+ .Location("Content"));
+ }
+
+ public override async Task UpdateAsync(RSASecret secret, UpdateEditorContext context)
+ {
+ var model = new RsaSecretViewModel();
+
+ if (await context.Updater.TryUpdateModelAsync(model, Prefix))
+ {
+ secret.KeyType = model.KeyType;
+
+ // The view will contain the private key when creating.
+ if (context.IsNew)
+ {
+ secret.PublicKey = model.PublicKey;
+ secret.PrivateKey = model.PrivateKey;
+ }
+
+ if (model.KeyType == RSAKeyType.Public)
+ {
+ secret.PublicKey = model.PublicKey;
+ secret.PrivateKey = null;
+ }
+
+ if (model.HasNewKeys && model.KeyType == RSAKeyType.PublicPrivate)
+ {
+ secret.PublicKey = model.NewPublicKey;
+ secret.PrivateKey = model.NewPrivateKey;
+ }
+
+ if (model.KeyType == RSAKeyType.PublicPrivate)
+ {
+ try
+ {
+ using var rsa = RSAGenerator.GenerateRSASecurityKey(2048);
+ rsa.ImportRSAPrivateKey(secret.PrivateKeyAsBytes(), out _);
+ }
+ catch (CryptographicException)
+ {
+ if (context.IsNew)
+ {
+ context.Updater.ModelState.AddModelError(Prefix, nameof(model.PrivateKey), S["The private key cannot be decoded."]);
+ }
+ else
+ {
+ context.Updater.ModelState.AddModelError(Prefix, nameof(model.NewPrivateKey), S["The private key cannot be decoded."]);
+ }
+ }
+ }
+
+ try
+ {
+ using var rsa = RSAGenerator.GenerateRSASecurityKey(2048);
+ rsa.ImportRSAPublicKey(secret.PublicKeyAsBytes(), out _);
+ }
+ catch (CryptographicException)
+ {
+ if (context.IsNew)
+ {
+ context.Updater.ModelState.AddModelError(Prefix, nameof(model.PublicKey), S["The public key cannot be decoded."]);
+ }
+ else
+ {
+ context.Updater.ModelState.AddModelError(Prefix, nameof(model.NewPublicKey), S["The public key cannot be decoded."]);
+ }
+ }
+ }
+
+ return await EditAsync(secret, context);
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Drivers/TextSecretDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Drivers/TextSecretDisplayDriver.cs
new file mode 100644
index 00000000000..055cb5c5076
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Drivers/TextSecretDisplayDriver.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Localization;
+using OrchardCore.DisplayManagement.Handlers;
+using OrchardCore.DisplayManagement.Views;
+using OrchardCore.Mvc.ModelBinding;
+using OrchardCore.Secrets.Models;
+using OrchardCore.Secrets.ViewModels;
+
+namespace OrchardCore.Secrets.Drivers;
+
+public class TextSecretDisplayDriver : DisplayDriver
+{
+ protected readonly IStringLocalizer S;
+
+ public TextSecretDisplayDriver(IStringLocalizer stringLocalizer) => S = stringLocalizer;
+
+ public override IDisplayResult Display(TextSecret secret)
+ {
+ return Combine(
+ View("TextSecret_Fields_Summary", secret).Location("Summary", "Content"),
+ View("TextSecret_Fields_Thumbnail", secret).Location("Thumbnail", "Content"));
+ }
+
+ public override Task EditAsync(TextSecret secret, BuildEditorContext context)
+ {
+ return Task.FromResult(Initialize("TextSecret_Fields_Edit", model =>
+ {
+ // The text value is never returned to the view.
+ model.Text = string.Empty;
+ model.Context = context;
+ })
+ .Location("Content"));
+ }
+
+ public override async Task UpdateAsync(TextSecret secret, UpdateEditorContext context)
+ {
+ var model = new TextSecretViewModel();
+
+ if (await context.Updater.TryUpdateModelAsync(model, Prefix))
+ {
+ if (context.IsNew && string.IsNullOrEmpty(model.Text))
+ {
+ context.Updater.ModelState.AddModelError(Prefix, nameof(model.Text), S["The text value is required."]);
+ }
+
+ // The text value is only updated when a new value has been provided.
+ if (!string.IsNullOrEmpty(model.Text))
+ {
+ secret.Text = model.Text;
+ }
+ }
+
+ return await EditAsync(secret, context);
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Drivers/X509SecretDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Drivers/X509SecretDisplayDriver.cs
new file mode 100644
index 00000000000..3b64531b2be
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Drivers/X509SecretDisplayDriver.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Immutable;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Localization;
+using OrchardCore.DisplayManagement.Handlers;
+using OrchardCore.DisplayManagement.Views;
+using OrchardCore.Secrets.Models;
+using OrchardCore.Secrets.ViewModels;
+using static OrchardCore.Secrets.ViewModels.X509SecretViewModel;
+
+namespace OrchardCore.Secrets.Drivers;
+
+public class X509SecretDisplayDriver : DisplayDriver
+{
+ protected readonly IStringLocalizer S;
+
+ public X509SecretDisplayDriver(IStringLocalizer stringLocalizer) => S = stringLocalizer;
+
+ public override IDisplayResult Display(X509Secret secret)
+ {
+ return Combine(
+ View("X509Secret_Fields_Summary", secret).Location("Summary", "Content"),
+ View("X509Secret_Fields_Thumbnail", secret).Location("Thumbnail", "Content"));
+ }
+
+ public override Task EditAsync(X509Secret secret, BuildEditorContext context)
+ {
+ return Task.FromResult(Initialize("X509Secret_Fields_Edit", async model =>
+ {
+ model.StoreLocation = secret.StoreLocation;
+ model.StoreName = secret.StoreName;
+ model.Thumbprint = secret.Thumbprint;
+ model.Context = context;
+
+ foreach (var (certificate, location, name) in await GetAvailableCertificatesAsync())
+ {
+ model.AvailableCertificates.Add(new CertificateInfo
+ {
+ StoreLocation = location,
+ StoreName = name,
+ FriendlyName = certificate.FriendlyName,
+ Issuer = certificate.Issuer,
+ Subject = certificate.Subject,
+ NotBefore = certificate.NotBefore,
+ NotAfter = certificate.NotAfter,
+ ThumbPrint = certificate.Thumbprint,
+ HasPrivateKey = certificate.HasPrivateKey,
+ Archived = certificate.Archived
+ });
+ }
+ })
+ .Location("Content"));
+ }
+
+ public override async Task UpdateAsync(X509Secret secret, UpdateEditorContext context)
+ {
+ var model = new X509SecretViewModel();
+
+ if (await context.Updater.TryUpdateModelAsync(model, Prefix))
+ {
+ secret.StoreLocation = model.StoreLocation;
+ secret.StoreName = model.StoreName;
+ secret.Thumbprint = model.Thumbprint;
+ }
+
+ return await EditAsync(secret, context);
+ }
+
+
+ private static Task> GetAvailableCertificatesAsync()
+ {
+ var certificates = ImmutableArray.CreateBuilder<(X509Certificate2, StoreLocation, StoreName)>();
+
+ foreach (StoreLocation location in Enum.GetValues(typeof(StoreLocation)))
+ {
+ foreach (StoreName name in Enum.GetValues(typeof(StoreName)))
+ {
+ // Note: on non-Windows platforms, an exception can
+ // be thrown if the store location/name doesn't exist.
+ try
+ {
+ using var store = new X509Store(name, location);
+ store.Open(OpenFlags.ReadOnly);
+
+ foreach (var certificate in store.Certificates)
+ {
+ if (!certificate.Archived && certificate.HasPrivateKey)
+ {
+ certificates.Add((certificate, location, name));
+ }
+ }
+ }
+ catch (CryptographicException)
+ {
+ continue;
+ }
+ }
+ }
+
+ return Task.FromResult(certificates.ToImmutable());
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Manifest.cs
new file mode 100644
index 00000000000..9d313fd534c
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Manifest.cs
@@ -0,0 +1,16 @@
+using OrchardCore.Modules.Manifest;
+
+[assembly: Module(
+ Name = "Secrets",
+ Author = ManifestConstants.OrchardCoreTeam,
+ Website = ManifestConstants.OrchardCoreWebsite,
+ Version = ManifestConstants.OrchardCoreVersion
+)]
+
+
+[assembly: Feature(
+ Id = "OrchardCore.Secrets",
+ Name = "Secrets",
+ Description = "The secrets feature manages secrets that other modules can access and contribute to.",
+ Category = "Configuration"
+)]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/OrchardCore.Secrets.csproj b/src/OrchardCore.Modules/OrchardCore.Secrets/OrchardCore.Secrets.csproj
new file mode 100644
index 00000000000..b7829655dda
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/OrchardCore.Secrets.csproj
@@ -0,0 +1,39 @@
+
+
+
+ true
+
+ OrchardCore Secrets
+ $(OCCMSDescription)
+
+ The secrets module manages secrets that other modules can contribute to.
+
+ $(PackageTags) OrchardCoreCMS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Never
+
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Permissions.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Permissions.cs
new file mode 100644
index 00000000000..c5c0bc503e3
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Permissions.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using OrchardCore.Security.Permissions;
+
+namespace OrchardCore.Secrets
+{
+ public class Permissions : IPermissionProvider
+ {
+ public static readonly Permission ManageSecrets = new("ManageSecrets", "Manage secrets", true);
+
+ public Task> GetPermissionsAsync()
+ {
+ return Task.FromResult(new[] { ManageSecrets }.AsEnumerable());
+ }
+
+ public IEnumerable GetDefaultStereotypes()
+ {
+ return new[]
+ {
+ new PermissionStereotype
+ {
+ Name = "Administrator",
+ Permissions = new[] { ManageSecrets },
+ }
+ };
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Recipes/SecretsRecipeStep.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Recipes/SecretsRecipeStep.cs
new file mode 100644
index 00000000000..11f9f0d61bf
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Recipes/SecretsRecipeStep.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using OrchardCore.Recipes.Models;
+using OrchardCore.Recipes.Services;
+using OrchardCore.Secrets.Models;
+
+namespace OrchardCore.Secrets.Recipes;
+
+public class SecretsRecipeStep : IRecipeStepHandler
+{
+ private readonly ISecretService _secretService;
+ private readonly ISecretProtector _secretProtector;
+
+ public SecretsRecipeStep(ISecretService secretService, ISecretProtectionProvider protectionProvider)
+ {
+ _secretService = secretService;
+ _secretProtector = protectionProvider.CreateProtector();
+ }
+
+ public async Task ExecuteAsync(RecipeExecutionContext context)
+ {
+ if (!string.Equals(context.Name, "Secrets", StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ var secrets = (JObject)context.Step["Secrets"];
+ foreach (var kvp in secrets)
+ {
+ var info = kvp.Value["SecretInfo"].ToObject();
+ var secret = _secretService.CreateSecret(info.Type);
+
+ var protectedData = kvp.Value["SecretData"]?.ToString();
+ if (!string.IsNullOrEmpty(protectedData))
+ {
+ var (Plaintext, _) = await _secretProtector.UnprotectAsync(protectedData);
+ secret = JsonConvert.DeserializeObject(Plaintext, secret.GetType()) as SecretBase;
+ }
+
+ info.Name = kvp.Key;
+
+ await _secretService.UpdateSecretAsync(secret, info);
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/Startup.cs
new file mode 100644
index 00000000000..7fb274586e4
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Startup.cs
@@ -0,0 +1,72 @@
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using OrchardCore.Admin;
+using OrchardCore.Data.Migration;
+using OrchardCore.Deployment;
+using OrchardCore.DisplayManagement;
+using OrchardCore.DisplayManagement.Handlers;
+using OrchardCore.Modules;
+using OrchardCore.Mvc.Core.Utilities;
+using OrchardCore.Navigation;
+using OrchardCore.Recipes;
+using OrchardCore.Secrets.Controllers;
+using OrchardCore.Secrets.Deployment;
+using OrchardCore.Secrets.Drivers;
+using OrchardCore.Secrets.Models;
+using OrchardCore.Secrets.Recipes;
+using OrchardCore.Security.Permissions;
+
+namespace OrchardCore.Secrets
+{
+ public class Startup : StartupBase
+ {
+ private readonly AdminOptions _adminOptions;
+
+ public Startup(IOptions adminOptions) => _adminOptions = adminOptions.Value;
+
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ services.AddScoped();
+ services.AddScoped();
+
+ services.AddScoped, DisplayManager>();
+ services.AddScoped, RSASecretDisplayDriver>();
+ services.AddScoped, TextSecretDisplayDriver>();
+ services.AddScoped, X509SecretDisplayDriver>();
+
+ services.AddRecipeExecutionStep();
+ services.AddTransient();
+ services.AddSingleton(new DeploymentStepFactory());
+ services.AddScoped, AllSecretsDeploymentStepDriver>();
+ }
+
+ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
+ {
+ var secretControllerName = typeof(AdminController).ControllerName();
+
+ routes.MapAreaControllerRoute(
+ name: "Secrets.Index",
+ areaName: "OrchardCore.Secrets",
+ pattern: _adminOptions.AdminUrlPrefix + "/Secrets",
+ defaults: new { controller = secretControllerName, action = nameof(AdminController.Index) }
+ );
+
+ routes.MapAreaControllerRoute(
+ name: "Secrets.Create",
+ areaName: "OrchardCore.Secrets",
+ pattern: _adminOptions.AdminUrlPrefix + "/Secrets/Create",
+ defaults: new { controller = secretControllerName, action = nameof(AdminController.Create) }
+ );
+
+ routes.MapAreaControllerRoute(
+ name: "Secrets.Edit",
+ areaName: "OrchardCore.Secrets",
+ pattern: _adminOptions.AdminUrlPrefix + "/Secrets/Edit/{name}",
+ defaults: new { controller = secretControllerName, action = nameof(AdminController.Edit) }
+ );
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/ViewComponents/SelectSecretViewComponent.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewComponents/SelectSecretViewComponent.cs
new file mode 100644
index 00000000000..05c93417cab
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewComponents/SelectSecretViewComponent.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.Extensions.Localization;
+using OrchardCore.Secrets.ViewModels;
+
+namespace OrchardCore.Secrets.ViewComponents;
+
+public class SelectSecretViewComponent : ViewComponent
+{
+ private readonly ISecretService _secretService;
+ protected readonly IStringLocalizer S;
+
+ public SelectSecretViewComponent(
+ ISecretService secretService,
+ IStringLocalizer stringLocalizer)
+ {
+ _secretService = secretService;
+ S = stringLocalizer;
+ }
+
+ public async Task InvokeAsync(string secretType, string selectedSecret, string htmlId, string htmlName, bool required)
+ {
+ var secrets = (await _secretService.GetSecretInfosAsync())
+ .Where(kv => string.Equals(secretType, kv.Value.Type, StringComparison.OrdinalIgnoreCase))
+ .Select(kv => new SelectListItem()
+ {
+ Text = kv.Key,
+ Value = kv.Key,
+ Selected = string.Equals(kv.Key, selectedSecret, StringComparison.OrdinalIgnoreCase),
+ })
+ .ToList();
+
+ if (!required)
+ {
+ secrets.Insert(0, new SelectListItem() { Text = S["None"], Value = string.Empty });
+ }
+
+ var model = new SelectSecretViewModel
+ {
+ HtmlId = htmlId,
+ HtmlName = htmlName,
+ Secrets = secrets,
+ };
+
+ return View(model);
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/ContentOptions.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/ContentOptions.cs
new file mode 100644
index 00000000000..f2020a300a0
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/ContentOptions.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace OrchardCore.Secrets.ViewModels;
+
+public class ContentOptions
+{
+ public string Search { get; set; }
+ public ContentsBulkAction BulkAction { get; set; }
+
+ [BindNever]
+ public List ContentsBulkAction { get; set; }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/ContentsBulkAction.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/ContentsBulkAction.cs
new file mode 100644
index 00000000000..92a08600e5b
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/ContentsBulkAction.cs
@@ -0,0 +1,7 @@
+namespace OrchardCore.Secrets.ViewModels;
+
+public enum ContentsBulkAction
+{
+ None,
+ Remove
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/RsaSecretViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/RsaSecretViewModel.cs
new file mode 100644
index 00000000000..34f6d015777
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/RsaSecretViewModel.cs
@@ -0,0 +1,24 @@
+
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using OrchardCore.DisplayManagement.Handlers;
+using OrchardCore.Secrets.Models;
+
+namespace OrchardCore.Secrets.ViewModels;
+
+public class RsaSecretViewModel
+{
+ public string PublicKey { get; set; }
+ public string PrivateKey { get; set; }
+ public RSAKeyType KeyType { get; set; }
+ public bool HasNewKeys { get; set; }
+ public string NewPublicKey { get; set; }
+ public string NewPrivateKey { get; set; }
+
+ [BindNever]
+ public List KeyTypes { get; set; }
+
+ [BindNever]
+ public BuildEditorContext Context { get; set; }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SecretIndexViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SecretIndexViewModel.cs
new file mode 100644
index 00000000000..97b8b9f9e8f
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SecretIndexViewModel.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace OrchardCore.Secrets.ViewModels;
+
+public class SecretIndexViewModel
+{
+ public List Entries { get; set; }
+ public Dictionary Thumbnails { get; set; }
+ public Dictionary Summaries { get; set; }
+ public ContentOptions Options { get; set; } = new ContentOptions();
+ public dynamic Pager { get; set; }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SecretInfoEntry.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SecretInfoEntry.cs
new file mode 100644
index 00000000000..f9cadc260a3
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SecretInfoEntry.cs
@@ -0,0 +1,10 @@
+using OrchardCore.Secrets.Models;
+
+namespace OrchardCore.Secrets.ViewModels;
+
+public class SecretInfoEntry
+{
+ public string Name { get; set; }
+ public SecretInfo Info { get; set; }
+ public dynamic Summary { get; set; }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SecretInfoViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SecretInfoViewModel.cs
new file mode 100644
index 00000000000..8d12c4bf4c6
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SecretInfoViewModel.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using OrchardCore.Secrets.Models;
+
+namespace OrchardCore.Secrets.ViewModels;
+
+public class SecretInfoViewModel
+{
+ public string Name { get; set; }
+ public string SelectedStore { get; set; }
+ public string Description { get; set; }
+ public dynamic Editor { get; set; }
+ public string Type { get; set; }
+
+ [BindNever]
+ public IReadOnlyCollection StoreInfos { get; set; }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SelectSecretViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SelectSecretViewModel.cs
new file mode 100644
index 00000000000..886a6f42a96
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/SelectSecretViewModel.cs
@@ -0,0 +1,15 @@
+
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace OrchardCore.Secrets.ViewModels;
+
+public class SelectSecretViewModel
+{
+ public string HtmlId { get; set; }
+ public string HtmlName { get; set; }
+
+ [BindNever]
+ public List Secrets { get; set; } = [];
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/TextSecretViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/TextSecretViewModel.cs
new file mode 100644
index 00000000000..a4d30b95b3d
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/TextSecretViewModel.cs
@@ -0,0 +1,13 @@
+
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using OrchardCore.DisplayManagement.Handlers;
+
+namespace OrchardCore.Secrets.ViewModels;
+
+public class TextSecretViewModel
+{
+ public string Text { get; set; }
+
+ [BindNever]
+ public BuildEditorContext Context { get; set; }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/X509SecretViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/X509SecretViewModel.cs
new file mode 100644
index 00000000000..2913fe0a58f
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/ViewModels/X509SecretViewModel.cs
@@ -0,0 +1,35 @@
+
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using OrchardCore.DisplayManagement.Handlers;
+
+namespace OrchardCore.Secrets.ViewModels;
+
+public class X509SecretViewModel
+{
+ public StoreLocation? StoreLocation { get; set; }
+ public StoreName? StoreName { get; set; }
+ public string Thumbprint { get; set; }
+
+ [BindNever]
+ public IList AvailableCertificates { get; } = new List();
+
+ [BindNever]
+ public BuildEditorContext Context { get; set; }
+
+ public class CertificateInfo
+ {
+ public string FriendlyName { get; set; }
+ public string Issuer { get; set; }
+ public DateTime NotAfter { get; set; }
+ public DateTime NotBefore { get; set; }
+ public StoreLocation StoreLocation { get; set; }
+ public StoreName StoreName { get; set; }
+ public string Subject { get; set; }
+ public string ThumbPrint { get; set; }
+ public bool HasPrivateKey { get; set; }
+ public bool Archived { get; set; }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Admin/Create.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Admin/Create.cshtml
new file mode 100644
index 00000000000..90ca48e5d6e
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Admin/Create.cshtml
@@ -0,0 +1,50 @@
+@model SecretInfoViewModel
+
+@RenderTitleSegments(T["Create {0}", Model.Type])
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Admin/Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Admin/Edit.cshtml
new file mode 100644
index 00000000000..a2d98978271
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Admin/Edit.cshtml
@@ -0,0 +1,52 @@
+@model SecretInfoViewModel
+
+@RenderTitleSegments(T["Edit {0}", Model.Type])
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Admin/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Admin/Index.cshtml
new file mode 100644
index 00000000000..568a2cc8957
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Admin/Index.cshtml
@@ -0,0 +1,177 @@
+@model SecretIndexViewModel
+
+@{
+ int startIndex = (Model.Pager.Page - 1) * (Model.Pager.PageSize) + 1;
+ int endIndex = startIndex + Model.Entries.Count - 1;
+}
+
+@RenderTitleSegments(T["Secrets"])
+
+@* The form is necessary to generate an anti-forgery token for the delete and toggle actions. *@
+
+
+@await DisplayAsync(Model.Pager)
+
+
+
+
+
+
+
+
+ @foreach (var thumbnail in Model.Thumbnails.OrderBy(t => t.Key))
+ {
+ thumbnail.Value.Type = thumbnail.Key;
+ @await DisplayAsync(thumbnail.Value)
+ }
+
+
+
+
+
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/AllSecretsDeploymentStep.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/AllSecretsDeploymentStep.Fields.Edit.cshtml
new file mode 100644
index 00000000000..751270bcf01
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/AllSecretsDeploymentStep.Fields.Edit.cshtml
@@ -0,0 +1,3 @@
+@model dynamic
+
+@T["All Secrets"]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/AllSecretsDeploymentStep.Fields.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/AllSecretsDeploymentStep.Fields.Summary.cshtml
new file mode 100644
index 00000000000..751270bcf01
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/AllSecretsDeploymentStep.Fields.Summary.cshtml
@@ -0,0 +1,3 @@
+@model dynamic
+
+@T["All Secrets"]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/AllSecretsDeploymentStep.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/AllSecretsDeploymentStep.Fields.Thumbnail.cshtml
new file mode 100644
index 00000000000..7c6b3a64be8
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/AllSecretsDeploymentStep.Fields.Thumbnail.cshtml
@@ -0,0 +1,4 @@
+@model dynamic
+
+@T["All Secrets"]
+@T["Exports all secrets using Hybrid Encryption."]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/RSASecret.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/RSASecret.Fields.Edit.cshtml
new file mode 100644
index 00000000000..30027816bdd
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/RSASecret.Fields.Edit.cshtml
@@ -0,0 +1,92 @@
+@model RsaSecretViewModel
+
+
+
+
+
+
+
+
+ @T["Select the RSA key type. A public key can only be used for encryption."]
+
+
+
+
+
+
+ @if (Model.Context.IsNew || Model.KeyType == RSAKeyType.Public)
+ {
+
+ }
+ else
+ {
+
+ }
+
+ @T["The public key in base64 url encoded format."]
+
+
+ @if (!String.IsNullOrEmpty(Model.PrivateKey))
+ {
+
+
+
+
+ @T["The private key in base64 url encoded format. Once created this key will never be displayed again."]
+
+ }
+ @if (!Model.Context.IsNew)
+ {
+
+ }
+
+
+
+@if (!Model.Context.IsNew)
+{
+
+
+
+
+
+
+ @T["The public key in base64 url encoded format."]
+
+
+
+
+
+
+ @T["The private key in base64 url encoded format. Once created this key will never be displayed again."]
+
+
+}
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/RSASecret.Fields.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/RSASecret.Fields.Summary.cshtml
new file mode 100644
index 00000000000..3fe2fd58ce6
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/RSASecret.Fields.Summary.cshtml
@@ -0,0 +1,3 @@
+@model ShapeViewModel
+
+@T["RSA Secret"]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/RSASecret.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/RSASecret.Fields.Thumbnail.cshtml
new file mode 100644
index 00000000000..63d0f1c5397
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/RSASecret.Fields.Thumbnail.cshtml
@@ -0,0 +1,4 @@
+@model ShapeViewModel
+
+@T["RSA Secret"]
+@T["A public / private RSA key pair used for encryption."]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/TextSecret.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/TextSecret.Fields.Edit.cshtml
new file mode 100644
index 00000000000..1d8cc879c55
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/TextSecret.Fields.Edit.cshtml
@@ -0,0 +1,34 @@
+@model TextSecretViewModel
+
+
+
+
+
@T["The value for the text secret."]
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/TextSecret.Fields.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/TextSecret.Fields.Summary.cshtml
new file mode 100644
index 00000000000..feff8672f6e
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/TextSecret.Fields.Summary.cshtml
@@ -0,0 +1,3 @@
+@model ShapeViewModel
+
+@T["Text Secret"]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/TextSecret.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/TextSecret.Fields.Thumbnail.cshtml
new file mode 100644
index 00000000000..593c37cf7c1
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/TextSecret.Fields.Thumbnail.cshtml
@@ -0,0 +1,4 @@
+@model ShapeViewModel
+
+@T["Text Secret"]
+@T["A secret used to manage a secure text value."]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/X509Secret.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/X509Secret.Fields.Edit.cshtml
new file mode 100644
index 00000000000..b3da6d19127
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/X509Secret.Fields.Edit.cshtml
@@ -0,0 +1,108 @@
+@using System.Security.Cryptography.X509Certificates
+
+@model X509SecretViewModel
+
+
+
+
+
+ @T["Select the certificate location."]
+
+
+
+
+
+
+ @T["Select the certificate store."]
+
+
+
+ @if (Model.AvailableCertificates.Count != 0)
+ {
+
+
+
+
@T["Select the certificate."]
+ }
+ else
+ {
+
@T["You need to add a certificate to your server for setting up this 'X509Secret'."]
+ }
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/X509Secret.Fields.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/X509Secret.Fields.Summary.cshtml
new file mode 100644
index 00000000000..33313d36b67
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/X509Secret.Fields.Summary.cshtml
@@ -0,0 +1,3 @@
+@model ShapeViewModel
+
+@T["X509 Secret"]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/X509Secret.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/X509Secret.Fields.Thumbnail.cshtml
new file mode 100644
index 00000000000..72a42579ad1
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Items/X509Secret.Fields.Thumbnail.cshtml
@@ -0,0 +1,4 @@
+@model ShapeViewModel
+
+@T["X509 Secret"]
+@T["A secret used to manage a X509 certificate."]
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/SecretBase.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/SecretBase.Edit.cshtml
new file mode 100644
index 00000000000..ad1bf3a8cc6
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/SecretBase.Edit.cshtml
@@ -0,0 +1 @@
+@await DisplayAsync(Model.Content)
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/SecretBase.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/SecretBase.Summary.cshtml
new file mode 100644
index 00000000000..8e8f40c1fe8
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/SecretBase.Summary.cshtml
@@ -0,0 +1,3 @@
+
+ @await DisplayAsync(Model.Content)
+
\ No newline at end of file
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/SecretBase.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/SecretBase.Thumbnail.cshtml
new file mode 100644
index 00000000000..87ab633d344
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/SecretBase.Thumbnail.cshtml
@@ -0,0 +1,12 @@
+@model dynamic
+
+
+
+
+ @await DisplayAsync(Model.Content)
+
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Shared/Components/SelectSecret/Default.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Shared/Components/SelectSecret/Default.cshtml
new file mode 100644
index 00000000000..1f451fc6174
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/Shared/Components/SelectSecret/Default.cshtml
@@ -0,0 +1,3 @@
+@model SelectSecretViewModel
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Secrets/Views/_ViewImports.cshtml b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/_ViewImports.cshtml
new file mode 100644
index 00000000000..bbd6808d2da
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Secrets/Views/_ViewImports.cshtml
@@ -0,0 +1,13 @@
+@using Microsoft.Extensions.Localization
+@using Microsoft.AspNetCore.Mvc.Localization
+@using OrchardCore.DisplayManagement.Views
+@using OrchardCore.Secrets
+@using OrchardCore.Secrets.Deployment
+@using OrchardCore.Secrets.Models
+@using OrchardCore.Secrets.ViewModels
+
+@inherits OrchardCore.DisplayManagement.Razor.RazorPage
+
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+@addTagHelper *, OrchardCore.DisplayManagement
+@addTagHelper *, OrchardCore.ResourceManagement
diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Controllers/WorkflowTypeController.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Controllers/WorkflowTypeController.cs
index f0981e41c33..451266bcab3 100644
--- a/src/OrchardCore.Modules/OrchardCore.Workflows/Controllers/WorkflowTypeController.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Controllers/WorkflowTypeController.cs
@@ -42,7 +42,6 @@ public class WorkflowTypeController : Controller
private readonly IAuthorizationService _authorizationService;
private readonly IActivityDisplayManager _activityDisplayManager;
private readonly INotifier _notifier;
- private readonly ISecurityTokenService _securityTokenService;
private readonly IUpdateModelAccessor _updateModelAccessor;
protected readonly dynamic New;
@@ -61,7 +60,6 @@ public WorkflowTypeController
IActivityDisplayManager activityDisplayManager,
IShapeFactory shapeFactory,
INotifier notifier,
- ISecurityTokenService securityTokenService,
IStringLocalizer s,
IHtmlLocalizer h,
IUpdateModelAccessor updateModelAccessor)
@@ -75,7 +73,6 @@ public WorkflowTypeController
_authorizationService = authorizationService;
_activityDisplayManager = activityDisplayManager;
_notifier = notifier;
- _securityTokenService = securityTokenService;
_updateModelAccessor = updateModelAccessor;
New = shapeFactory;
diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Http/Controllers/HttpWorkflowController.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Http/Controllers/HttpWorkflowController.cs
index b0810114202..3edb351409c 100644
--- a/src/OrchardCore.Modules/OrchardCore.Workflows/Http/Controllers/HttpWorkflowController.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Http/Controllers/HttpWorkflowController.cs
@@ -8,6 +8,7 @@
using Microsoft.Extensions.Logging;
using OrchardCore.Admin;
using OrchardCore.Locking.Distributed;
+using OrchardCore.Secrets.Services;
using OrchardCore.Workflows.Helpers;
using OrchardCore.Workflows.Http.Activities;
using OrchardCore.Workflows.Http.Models;
@@ -22,7 +23,7 @@ public class HttpWorkflowController : Controller
private readonly IWorkflowTypeStore _workflowTypeStore;
private readonly IWorkflowStore _workflowStore;
private readonly IActivityLibrary _activityLibrary;
- private readonly ISecurityTokenService _securityTokenService;
+ private readonly ISecretTokenService _secretTokenService;
private readonly IAntiforgery _antiforgery;
private readonly IDistributedLock _distributedLock;
private readonly ILogger _logger;
@@ -34,7 +35,7 @@ public HttpWorkflowController(
IWorkflowTypeStore workflowTypeStore,
IWorkflowStore workflowStore,
IActivityLibrary activityLibrary,
- ISecurityTokenService securityTokenService,
+ ISecretTokenService secretTokenService,
IAntiforgery antiforgery,
IDistributedLock distributedLock,
ILogger logger
@@ -45,7 +46,7 @@ ILogger logger
_workflowTypeStore = workflowTypeStore;
_workflowStore = workflowStore;
_activityLibrary = activityLibrary;
- _securityTokenService = securityTokenService;
+ _secretTokenService = secretTokenService;
_antiforgery = antiforgery;
_distributedLock = distributedLock;
_logger = logger;
@@ -67,7 +68,10 @@ public async Task GenerateUrl(long workflowTypeId, string activit
return NotFound();
}
- var token = _securityTokenService.CreateToken(new WorkflowPayload(workflowType.WorkflowTypeId, activityId), TimeSpan.FromDays(tokenLifeSpan == 0 ? NoExpiryTokenLifespan : tokenLifeSpan));
+ var token = await _secretTokenService.CreateTokenAsync(
+ new WorkflowPayload(workflowType.WorkflowTypeId, activityId),
+ TimeSpan.FromDays(tokenLifeSpan == 0 ? NoExpiryTokenLifespan : tokenLifeSpan));
+
var url = Url.Action("Invoke", "HttpWorkflow", new { token });
return Ok(url);
@@ -77,7 +81,8 @@ public async Task GenerateUrl(long workflowTypeId, string activit
[HttpGet, HttpPost, HttpPut, HttpPatch]
public async Task Invoke(string token)
{
- if (!_securityTokenService.TryDecryptToken(token, out var payload))
+ (var valid, var payload) = await _secretTokenService.TryDecryptTokenAsync(token);
+ if (!valid)
{
_logger.LogWarning("Invalid SAS token provided");
return NotFound();
@@ -218,7 +223,8 @@ public async Task Invoke(string token)
[HttpGet]
public async Task Trigger(string token)
{
- if (!_securityTokenService.TryDecryptToken(token, out var payload))
+ (var valid, var payload) = await _secretTokenService.TryDecryptTokenAsync(token);
+ if (!valid)
{
_logger.LogWarning("Invalid SAS token provided");
return NotFound();
diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Http/Models/WorkflowPayload.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Http/Models/WorkflowPayload.cs
index 4afe97ed2f1..981bd9d8f29 100644
--- a/src/OrchardCore.Modules/OrchardCore.Workflows/Http/Models/WorkflowPayload.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Http/Models/WorkflowPayload.cs
@@ -8,7 +8,7 @@ public WorkflowPayload(string workflowId, string activityId)
ActivityId = activityId;
}
- public string WorkflowId { get; private set; }
- public string ActivityId { get; private set; }
+ public string WorkflowId { get; set; }
+ public string ActivityId { get; set; }
}
}
diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Migrations.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Migrations.cs
index 78a9a89bbde..3930230d321 100644
--- a/src/OrchardCore.Modules/OrchardCore.Workflows/Migrations.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Migrations.cs
@@ -1,6 +1,8 @@
using System;
using System.Threading.Tasks;
using OrchardCore.Data.Migration;
+using OrchardCore.Secrets;
+using OrchardCore.Secrets.Models;
using OrchardCore.Workflows.Indexes;
using YesSql.Sql;
@@ -8,6 +10,10 @@ namespace OrchardCore.Workflows
{
public class Migrations : DataMigration
{
+ private readonly ISecretService _secretService;
+
+ public Migrations(ISecretService secretService) => _secretService = secretService;
+
public async Task CreateAsync()
{
await SchemaBuilder.CreateMapIndexTableAsync(table => table
@@ -84,8 +90,16 @@ await SchemaBuilder.AlterIndexTableAsync(table
"WorkflowCorrelationId")
);
+ await _secretService.GetOrAddSecretAsync(
+ TokenSecret.Encryption,
+ (secret, info) => RSAGenerator.ConfigureRSASecretKeys(secret, RSAKeyType.PublicPrivate));
+
+ await _secretService.GetOrAddSecretAsync(
+ TokenSecret.Signing,
+ (secret, info) => RSAGenerator.ConfigureRSASecretKeys(secret, RSAKeyType.PublicPrivate));
+
// Shortcut other migration steps on new content definition schemas.
- return 3;
+ return 4;
}
// This code can be removed in a later version.
@@ -147,5 +161,19 @@ await SchemaBuilder.AlterIndexTableAsync(table
return 3;
}
+
+ // This code can be removed in a later version.
+ public async Task UpdateFrom3Async()
+ {
+ await _secretService.GetOrAddSecretAsync