Skip to content

Commit 54dc608

Browse files
Implement per instance import/export
1 parent b10640d commit 54dc608

File tree

9 files changed

+171
-64
lines changed

9 files changed

+171
-64
lines changed

src/Certify.Core/Management/CertifyManager/CertifyManager.ManagementHub.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Certify.Locales;
88
using Certify.Models;
99
using Certify.Models.Config;
10+
using Certify.Models.Config.Migration;
1011
using Certify.Models.Hub;
1112
using Certify.Shared.Core.Utils;
1213

@@ -330,6 +331,22 @@ public async Task<InstanceCommandResult> PerformHubCommandWithResult(InstanceCom
330331

331332
val = await GetDomainOptionsFromSite(serverType, itemArg.Value);
332333
}
334+
else if (arg.CommandType == ManagementHubCommands.PerformImport)
335+
{
336+
var args = JsonSerializer.Deserialize<KeyValuePair<string, string>[]>(arg.Value);
337+
var requestArg = args.FirstOrDefault(a => a.Key == "importRequest");
338+
var importRequest = JsonSerializer.Deserialize<ImportRequest>(requestArg.Value);
339+
340+
val = await PerformImport(importRequest);
341+
}
342+
else if (arg.CommandType == ManagementHubCommands.PerformExport)
343+
{
344+
var args = JsonSerializer.Deserialize<KeyValuePair<string, string>[]>(arg.Value);
345+
var requestArg = args.FirstOrDefault(a => a.Key == "exportRequest");
346+
var exportRequest = JsonSerializer.Deserialize<ExportRequest>(requestArg.Value);
347+
348+
val = await PerformExport(exportRequest);
349+
}
333350
else if (arg.CommandType == ManagementHubCommands.Reconnect)
334351
{
335352
await _managementServerClient.Disconnect();

src/Certify.Core/Management/MigrationManager.cs

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class MigrationManager
2626
private IManagedItemStore _itemManager;
2727
private ICredentialsManager _credentialsManager;
2828
private List<ITargetWebServer> _targetServers;
29+
private string _encryptionScheme = "default";
2930

3031
public MigrationManager(IManagedItemStore itemManager, ICredentialsManager credentialsManager, List<ITargetWebServer> targetServers)
3132
{
@@ -227,32 +228,61 @@ private IEnumerable<EncryptedContent> GetTaskScriptsAndContent(ObservableCollect
227228

228229
private Aes GetAlg(string secret, string salt)
229230
{
230-
var saltBytes = Encoding.ASCII.GetBytes(salt);
231-
var key = new Rfc2898DeriveBytes(secret, saltBytes);
231+
#if NET9_0_OR_GREATER
232+
if (_encryptionScheme == "default")
233+
{
234+
var saltBytes = Encoding.ASCII.GetBytes(salt);
235+
#pragma warning disable SYSLIB0041 // Type or member is obsolete
236+
var key = new Rfc2898DeriveBytes(secret, saltBytes);
237+
#pragma warning restore SYSLIB0041 // Type or member is obsolete
238+
239+
var aesAlg = Aes.Create();
240+
aesAlg.Mode = CipherMode.CBC;
241+
aesAlg.Padding = PaddingMode.PKCS7;
242+
243+
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
244+
aesAlg.IV = key.GetBytes(aesAlg.BlockSize / 8);
245+
246+
return aesAlg;
247+
}
248+
else
249+
{
232250

233-
var aesAlg = Aes.Create();
234-
aesAlg.Mode = CipherMode.CBC;
235-
aesAlg.Padding = PaddingMode.PKCS7;
251+
var saltBytes = Encoding.ASCII.GetBytes(salt);
252+
var key = new Rfc2898DeriveBytes(secret, saltBytes, 600000, HashAlgorithmName.SHA256);
236253

237-
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
238-
aesAlg.IV = key.GetBytes(aesAlg.BlockSize / 8);
254+
var aesAlg = Aes.Create();
255+
aesAlg.Mode = CipherMode.CBC;
256+
aesAlg.Padding = PaddingMode.PKCS7;
239257

240-
return aesAlg;
258+
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
259+
aesAlg.IV = key.GetBytes(aesAlg.BlockSize / 8);
260+
261+
return aesAlg;
262+
}
263+
#else
264+
var saltBytes = Encoding.ASCII.GetBytes(salt);
265+
var key = new Rfc2898DeriveBytes(secret, saltBytes);
266+
267+
var aesAlg = Aes.Create();
268+
aesAlg.Mode = CipherMode.CBC;
269+
aesAlg.Padding = PaddingMode.PKCS7;
270+
271+
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
272+
aesAlg.IV = key.GetBytes(aesAlg.BlockSize / 8);
273+
274+
return aesAlg;
275+
#endif
241276
}
242277

243278
public byte[] EncryptBytes(byte[] source, string secret, string salt)
244279
{
245-
using (var rmCrypto = GetAlg(secret, salt))
246-
{
247-
using (var memoryStream = new MemoryStream())
248-
using (var cryptoStream = new CryptoStream(memoryStream, rmCrypto.CreateEncryptor(), CryptoStreamMode.Write))
249-
{
250-
cryptoStream.Write(source, 0, source.Length);
251-
cryptoStream.FlushFinalBlock();
252-
cryptoStream.Close();
253-
return memoryStream.ToArray();
254-
}
255-
}
280+
using var rmCrypto = GetAlg(secret, salt);
281+
using var memoryStream = new MemoryStream();
282+
using var cryptoStream = new CryptoStream(memoryStream, rmCrypto.CreateEncryptor(), CryptoStreamMode.Write);
283+
cryptoStream.Write(source, 0, source.Length);
284+
cryptoStream.FlushFinalBlock();
285+
return memoryStream.ToArray();
256286
}
257287

258288
public byte[] DecryptBytes(byte[] source, string secret, string salt)
@@ -261,30 +291,12 @@ public byte[] DecryptBytes(byte[] source, string secret, string salt)
261291
{
262292
using (var decryptor = rmCrypto.CreateDecryptor())
263293
{
264-
using (var memoryStream = new MemoryStream(source))
265-
{
266-
using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
267-
{
268-
var decryptedBytes = new byte[source.Length];
269-
270-
var totalRead = 0;
271-
while (totalRead < source.Length)
272-
{
273-
var bytesRead = cryptoStream.Read(decryptedBytes, totalRead, source.Length - totalRead);
274-
if (bytesRead == 0)
275-
{
276-
break;
277-
}
278-
279-
totalRead += bytesRead;
280-
}
294+
using var memoryStream = new MemoryStream(source);
295+
using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
296+
using var resultStream = new MemoryStream();
297+
cryptoStream.CopyTo(resultStream);
281298

282-
memoryStream.Close();
283-
cryptoStream.Close();
284-
285-
return decryptedBytes;
286-
}
287-
}
299+
return resultStream.ToArray();
288300
}
289301
}
290302
}

src/Certify.Models/Hub/ManagementHubMessages.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public class ManagementHubCommands
4848
public const string GetTargetServiceItems = "GetTargetServiceItems";
4949
public const string GetTargetServiceItemIdentifiers = "GetTargetServiceItemIdentifiers";
5050

51+
public const string PerformImport = "PerformImport";
52+
public const string PerformExport = "PerformExport";
53+
5154
public const string Reconnect = "Reconnect";
5255

5356
/// <summary>

src/Certify.Server/Certify.Server.Hub.Api.Client/Certify.Server.Hub.Api.Client.cs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4740,9 +4740,9 @@ public virtual async System.Threading.Tasks.Task<HubHealth> GetHealthAsync(Syste
47404740
/// </summary>
47414741
/// <returns>OK</returns>
47424742
/// <exception cref="ApiException">A server side error occurred.</exception>
4743-
public virtual System.Threading.Tasks.Task<ImportExportPackage> PerformExportAsync(ExportRequest body)
4743+
public virtual System.Threading.Tasks.Task<ImportExportPackage> PerformInstanceExportAsync(string instanceId, ExportRequest body)
47444744
{
4745-
return PerformExportAsync(body, System.Threading.CancellationToken.None);
4745+
return PerformInstanceExportAsync(instanceId, body, System.Threading.CancellationToken.None);
47464746
}
47474747

47484748
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
@@ -4751,8 +4751,11 @@ public virtual System.Threading.Tasks.Task<ImportExportPackage> PerformExportAsy
47514751
/// </summary>
47524752
/// <returns>OK</returns>
47534753
/// <exception cref="ApiException">A server side error occurred.</exception>
4754-
public virtual async System.Threading.Tasks.Task<ImportExportPackage> PerformExportAsync(ExportRequest body, System.Threading.CancellationToken cancellationToken)
4754+
public virtual async System.Threading.Tasks.Task<ImportExportPackage> PerformInstanceExportAsync(string instanceId, ExportRequest body, System.Threading.CancellationToken cancellationToken)
47554755
{
4756+
if (instanceId == null)
4757+
throw new System.ArgumentNullException("instanceId");
4758+
47564759
var client_ = _httpClient;
47574760
var disposeClient_ = false;
47584761
try
@@ -4768,8 +4771,10 @@ public virtual async System.Threading.Tasks.Task<ImportExportPackage> PerformExp
47684771

47694772
var urlBuilder_ = new System.Text.StringBuilder();
47704773
if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl);
4771-
// Operation Path: "api/v1/system/system/migration/export"
4772-
urlBuilder_.Append("api/v1/system/system/migration/export");
4774+
// Operation Path: "api/v1/system/{instanceId}/system/migration/export"
4775+
urlBuilder_.Append("api/v1/system/");
4776+
urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(instanceId, System.Globalization.CultureInfo.InvariantCulture)));
4777+
urlBuilder_.Append("/system/migration/export");
47734778

47744779
PrepareRequest(client_, request_, urlBuilder_);
47754780

@@ -4828,9 +4833,9 @@ public virtual async System.Threading.Tasks.Task<ImportExportPackage> PerformExp
48284833
/// </summary>
48294834
/// <returns>OK</returns>
48304835
/// <exception cref="ApiException">A server side error occurred.</exception>
4831-
public virtual System.Threading.Tasks.Task<System.Collections.Generic.ICollection<ActionStep>> PerformImportAsync(ImportRequest body)
4836+
public virtual System.Threading.Tasks.Task<System.Collections.Generic.ICollection<ActionStep>> PerformInstanceImportAsync(string instanceId, ImportRequest body)
48324837
{
4833-
return PerformImportAsync(body, System.Threading.CancellationToken.None);
4838+
return PerformInstanceImportAsync(instanceId, body, System.Threading.CancellationToken.None);
48344839
}
48354840

48364841
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
@@ -4839,8 +4844,11 @@ public virtual async System.Threading.Tasks.Task<ImportExportPackage> PerformExp
48394844
/// </summary>
48404845
/// <returns>OK</returns>
48414846
/// <exception cref="ApiException">A server side error occurred.</exception>
4842-
public virtual async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<ActionStep>> PerformImportAsync(ImportRequest body, System.Threading.CancellationToken cancellationToken)
4847+
public virtual async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<ActionStep>> PerformInstanceImportAsync(string instanceId, ImportRequest body, System.Threading.CancellationToken cancellationToken)
48434848
{
4849+
if (instanceId == null)
4850+
throw new System.ArgumentNullException("instanceId");
4851+
48444852
var client_ = _httpClient;
48454853
var disposeClient_ = false;
48464854
try
@@ -4856,8 +4864,10 @@ public virtual async System.Threading.Tasks.Task<ImportExportPackage> PerformExp
48564864

48574865
var urlBuilder_ = new System.Text.StringBuilder();
48584866
if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl);
4859-
// Operation Path: "api/v1/system/system/migration/import"
4860-
urlBuilder_.Append("api/v1/system/system/migration/import");
4867+
// Operation Path: "api/v1/system/{instanceId}/system/migration/import"
4868+
urlBuilder_.Append("api/v1/system/");
4869+
urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(instanceId, System.Globalization.CultureInfo.InvariantCulture)));
4870+
urlBuilder_.Append("/system/migration/import");
48614871

48624872
PrepareRequest(client_, request_, urlBuilder_);
48634873

src/Certify.Server/Certify.Server.Hub.Api/Controllers/v1/SystemController.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Certify.Client;
22
using Certify.Models.Hub;
3+
using Certify.Server.Hub.Api.Services;
34
using Microsoft.AspNetCore.Mvc;
45

56
namespace Certify.Server.Hub.Api.Controllers
@@ -16,15 +17,18 @@ public partial class SystemController : ApiControllerBase
1617

1718
private readonly ICertifyInternalApiClient _client;
1819

20+
private ManagementAPI _mgmtAPI;
21+
1922
/// <summary>
2023
/// Constructor
2124
/// </summary>
2225
/// <param name="logger"></param>
2326
/// <param name="client"></param>
24-
public SystemController(ILogger<SystemController> logger, ICertifyInternalApiClient client)
27+
public SystemController(ILogger<SystemController> logger, ICertifyInternalApiClient client, ManagementAPI mgmtApi)
2528
{
2629
_logger = logger;
2730
_client = client;
31+
_mgmtAPI = mgmtApi;
2832
}
2933

3034
/// <summary>

src/Certify.Server/Certify.Server.Hub.Api/Services/ManagementAPI.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System.Text.Json;
2+
using System.Threading.Tasks;
23
using Certify.Client;
34
using Certify.Models;
45
using Certify.Models.Config;
6+
using Certify.Models.Config.Migration;
57
using Certify.Models.Hub;
68
using Certify.Models.Providers;
79
using Certify.Models.Reporting;
@@ -384,6 +386,26 @@ internal async Task PerformManagedCertificateRequest(string instanceId, string m
384386

385387
await SendCommandWithNoResult(instanceId, cmd);
386388
}
389+
390+
internal async Task<List<ActionStep>> PerformInstanceImport(string instanceId, ImportRequest importRequest, AuthContext? currentAuthContext)
391+
{
392+
var args = new KeyValuePair<string, string>[] {
393+
new("instanceId", instanceId) ,
394+
new("importRequest", JsonSerializer.Serialize(importRequest))
395+
};
396+
397+
return await PerformInstanceCommandTaskWithResult<List<ActionStep>>(instanceId, args, ManagementHubCommands.PerformImport) ?? [];
398+
}
399+
400+
internal async Task<Models.Config.Migration.ImportExportPackage> PerformInstanceExport(string instanceId, ExportRequest exportRequest, AuthContext? currentAuthContext)
401+
{
402+
var args = new KeyValuePair<string, string>[] {
403+
new("instanceId", instanceId) ,
404+
new("exportRequest", JsonSerializer.Serialize(exportRequest))
405+
};
406+
407+
return await PerformInstanceCommandTaskWithResult<Models.Config.Migration.ImportExportPackage>(instanceId, args, ManagementHubCommands.PerformExport);
408+
}
387409
}
388410
}
389411

src/Certify.SourceGenerators/ApiMethods.cs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,22 @@ public static List<GeneratedAPI> GetApiDefinitions()
237237
{ "request", "Certify.Models.Hub.ManagedChallengeRequest" }
238238
}
239239
},
240+
new() {
241+
OperationName = "PerformExport",
242+
OperationMethod = HttpPost,
243+
Comment = "Perform an export of all settings",
244+
ServiceAPIRoute = "system/migration/export",
245+
ReturnType = "Models.Config.Migration.ImportExportPackage",
246+
Params = new Dictionary<string, string> { { "exportRequest", "Certify.Models.Config.Migration.ExportRequest" } }
247+
},
248+
new() {
249+
OperationName = "PerformImport",
250+
OperationMethod = HttpPost,
251+
Comment = "Perform an import of all settings",
252+
ServiceAPIRoute = "system/migration/import",
253+
ReturnType = "ICollection<ActionStep>",
254+
Params = new Dictionary<string, string> { { "importRequest", "Certify.Models.Config.Migration.ImportRequest" } }
255+
},
240256
/* per instance API, via management hub */
241257
new() {
242258
OperationName = "GetAcmeAccounts",
@@ -432,26 +448,26 @@ public static List<GeneratedAPI> GetApiDefinitions()
432448
ReturnType = "Models.Config.ActionResult",
433449
Params = new Dictionary<string, string> { { "instanceId", "string" }, { "managedCertId", "string" } }
434450
},
435-
// TODO
451+
436452
new() {
437-
OperationName = "PerformExport",
453+
OperationName = "PerformInstanceExport",
438454
OperationMethod = HttpPost,
439455
Comment = "Perform an export of all settings",
456+
UseManagementAPI = true,
440457
PublicAPIController = "System",
441-
PublicAPIRoute = "system/migration/export",
442-
ServiceAPIRoute = "system/migration/export",
458+
PublicAPIRoute = "{instanceId}/system/migration/export",
443459
ReturnType = "Models.Config.Migration.ImportExportPackage",
444-
Params = new Dictionary<string, string> { { "exportRequest", "Certify.Models.Config.Migration.ExportRequest" } }
460+
Params = new Dictionary<string, string> { { "instanceId", "string" }, { "exportRequest", "Certify.Models.Config.Migration.ExportRequest" } }
445461
},
446462
new() {
447-
OperationName = "PerformImport",
463+
OperationName = "PerformInstanceImport",
448464
OperationMethod = HttpPost,
449465
Comment = "Perform an import of all settings",
466+
UseManagementAPI = true,
450467
PublicAPIController = "System",
451-
PublicAPIRoute = "system/migration/import",
452-
ServiceAPIRoute = "system/migration/import",
468+
PublicAPIRoute = "{instanceId}/system/migration/import",
453469
ReturnType = "ICollection<ActionStep>",
454-
Params = new Dictionary<string, string> { { "importRequest", "Certify.Models.Config.Migration.ImportRequest" } }
470+
Params = new Dictionary<string, string> { { "instanceId", "string" }, { "importRequest", "Certify.Models.Config.Migration.ImportRequest" } }
455471
},
456472
};
457473
}

src/Certify.SourceGenerators/PublicAPISourceGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6464
var apiParamCall = paramSet.Any() ? string.Join(", ", paramSet.Select(p => $"{p.Key}")) : "";
6565
var apiParamCallWithoutAuthContext = config.Params.Any() ? string.Join(", ", config.Params.Select(p => $"{p.Key}")) : "";
6666

67-
if (assemblyName.EndsWith("Hub.Api") && config.PublicAPIController != null)
67+
if (assemblyName.EndsWith("Hub.Api") && !string.IsNullOrEmpty(config.PublicAPIController))
6868
{
6969
ImplementPublicAPI(spc, config, apiParamDeclWithoutAuthContext, apiParamDecl, apiParamCall);
7070
}
7171

72-
if (assemblyName.EndsWith("Certify.UI.Blazor"))
72+
if (assemblyName.EndsWith("Certify.UI.Blazor") && !string.IsNullOrEmpty(config.PublicAPIController))
7373
{
7474
ImplementAppModel(spc, config, apiParamDeclWithoutAuthContext, apiParamCallWithoutAuthContext);
7575
}

0 commit comments

Comments
 (0)