-
Notifications
You must be signed in to change notification settings - Fork 12
docs(v3): add v2.x-v3 migration guide #501
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
base: main
Are you sure you want to change the base?
Changes from all commits
ac16121
45f2db2
e9f0ee0
ca61602
8075121
f391cad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| label: 'Guidance' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,228 @@ | ||
| # Migrate your application from Arcus.Security v2.x to v3 | ||
| This guide will walk you through the process of migrating your application from Arcus.Security v2 to the new major v3 release. | ||
|
|
||
| ## General | ||
| * 🗑️ .NET 6 support is removed. All Arcus.Security.* packages support .NET 8 and stop supporting .NET 6. (.NET 10 support starts from v2.1.) | ||
| * 🗑️ Transient GuardNET dependency is replaced by built-in argument checking. | ||
| * 🗑️ Transient Arcus.Observability dependency for auditing is removed. | ||
| * ✏️ The new main core types are now under the `Arcus.Security` namespace. Following types are removed: | ||
| * `Arcus.Security.Core.ISecretProvider` (in favor of `Arcus.Security.ISecretProvider`) | ||
| * `Arcus.Security.Core.ISecretStore` (in favor of `Arcus.Security.ISecretStore`) | ||
| * `Arcus.Security.Core.IVersionedSecretProvider` | ||
| * `Arcus.Security.Core.ISyncSecretProvider` | ||
| * `Arcus.Security.Core.Caching.(I)CachedSecretProvider` | ||
| * `Arcus.Security.Core.Caching.Configuration.(I)CacheConfiguration` | ||
| * `Arcus.Security.Core.CriticalExceptionFilter` | ||
| * `Arcus.Security.Core.SecretAuditingOptions` | ||
| * `Arcus.Security.Core.SecretStoreSource` | ||
| * `Arcus.Security.Core.SecretProviderOptions` | ||
| * `Arcus.Security.Core.SecretNotFoundException` | ||
| * `Arcus.Security.Core.Providers.MutatedSecretNameSecretProvider` | ||
| * `Arcus.Security.Core.Providers.MutatedSecretNameCachedSecretProvider` | ||
|
|
||
| ## 🎯 Use `ISecretStore` instead of `ISecretProvider` as the secret store's main point of contact | ||
| Starting from v3, accessing the secret store now happens via the `Arcus.Security.ISecretStore` interface (in new namespace), instead of previously using the same `Arcus.Security.Core.ISecretProvider` interface as for the external secret provider implementations. | ||
|
|
||
| ```diff | ||
| - using Arcus.Security.Core; | ||
| + using Arcus.Security; | ||
|
|
||
| // ⬇️ Injected into the application | ||
| - ISecretProvider store = ... | ||
| + ISecretStore store = ... | ||
| ``` | ||
|
|
||
| ### `Secret` ➡️ `SecretResult` | ||
| Before v3, failures in the secret store were communicated via exceptions. Starting from v3, the secret store returns a result type that represents a successful/failed interaction with the store. | ||
|
|
||
| ```diff | ||
| - try | ||
| - { | ||
| - Secret secret = await store.GetSecretAsync("<name>"); | ||
| - string secretValue = secret.Value; | ||
|
|
||
| + SecretResult result = await store.GetSecretAsync("<name>"); | ||
| + if (result.IsSuccess) | ||
| + { | ||
| + string secretValue = result.Value; | ||
| + } | ||
| - } | ||
| - catch (SecretNotFoundException exception) | ||
| - { | ||
| - } | ||
| ``` | ||
|
|
||
| :::info | ||
| The `SecretResult.Failure` enumeration has two members: `NotFound` in case a secret could not be retrieved from any of the registered secret providers, and `Interrupted` in case an exception was thrown by one of the secret providers during secret retrieval. | ||
| ::: | ||
|
|
||
| This also means that there is no need for 'critical exceptions' that users could previously register on the store to signal non-transient failures (e.g. authentication problems). The following types/members are removed: | ||
| * 🗑️ `CriticalExceptionFilter` | ||
| * 🗑️ `SecretStoreBuilder.AddCriticalException<...>(...)` | ||
|
|
||
| Custom secret providers can use the `SecretResult` model in case of interrupted/non-transient failures. | ||
|
|
||
| ### 🚛 Move caching from providers to store | ||
| Caching is centralized on the secret store instead of spread across secret providers. Just as before v3, caching happens internally, only now custom secret providers are registered without any mentioning of secret caching. | ||
|
|
||
| This affects the existing caching-types and (extension) members, which are removed/unavailable in v3: | ||
| * 🗑️ `Arcus.Security.Core.Caching.(I)CachedSecretProvider` | ||
| * 🗑️ `Arcus.Security.Core.Caching.Configuration.(I)CacheConfiguration` | ||
| * 🗑️ `Arcus.Security.Core.ISecretProvider.WithCaching(...)` (extension) | ||
| * 🗑️ `Arcus.Security.Core.ISecretStore.GetCachedProvider(...)` | ||
|
|
||
| ```diff | ||
| services.AddSecretStore(store => | ||
| { | ||
| var cacheDuration = TimeSpan.FromMinutes(5); | ||
| - store.AddProvider(new MySecretProvider().WithCaching(cacheDuration)); | ||
| + store.AddProvider(new MySecretProvider()); | ||
| + store.UseCaching(cacheDuration); | ||
| }); | ||
| ``` | ||
|
|
||
| Ignoring the cache at secret retrieval-time can still be done via a new method overload on the `ISecretStore` instead on the secret provider itself. | ||
|
|
||
| ```diff | ||
| - using Arcus.Security.Core.Caching; | ||
| - using Arcus.Security.Core; | ||
| + using Arcus.Security; | ||
|
|
||
| ISecretStore store = ... | ||
|
|
||
| - ICachedSecretProvider provider = store.GetCachedProvider("Admin secrets"); | ||
| - Secret secret = await provider.GetSecretAsync("<name>", ignoreCache: true); | ||
| + SecretResult result = await store.GetSecretAsync("<name>", options => | ||
| + { | ||
| + options.UseCache = false; | ||
| + }); | ||
| ``` | ||
|
|
||
| Invalidating a secret within a custom secret provider now happens via the `ISecretStoreContext` that can be passed upon creating the provider. | ||
|
|
||
| ```diff | ||
| - public class MySecretProvider : ICachedSecretProvider | ||
| + public class MySecretProvider(ISecretStoreContext context) : ISecretProvider | ||
| { | ||
| public async Task SetSecretAsync(string name, string value) | ||
| { | ||
| // Implementation omitted. | ||
|
|
||
| - await InvalidateSecretAsync(name); | ||
| + await context.Cache.InvalidateSecretAsync(name); | ||
| } | ||
|
|
||
| - public Task InvalidateSecretAsync(string secretName) { ... } | ||
|
|
||
| // Implementation omitted. | ||
| } | ||
| ``` | ||
|
|
||
| ### 🗑️ Removed `GetRawSecret*` overloads | ||
| Starting from v3, there is no distinction anymore between 'secrets' and 'raw secrets' (meaning: directly accessing the secret's value). All secret interactions happen via the asynchronous/synchronous `GetSecret(Async)` methods on the `ISecretStore`. | ||
|
|
||
| :::tip[implicit overload on `SecretResult`] | ||
| There exists an `implicit operator` overload on the `SecretResult`, which means that that secret retrievals can also be written without checking for failures. | ||
| ```csharp | ||
| string secretValue = await store.GetSecretAsync("<name>"); | ||
| ``` | ||
| Just be aware that in case of a failure, an exception will still be thrown. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should've implemented that in such a way that we return
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be better communicated to users that it is only there if there is a result, otherwise we could have succes with null or not found being rather similar. |
||
| ::: | ||
|
|
||
| ### 🗑️ Removed `GetVersionedSecrets*` overloads | ||
| Starting from v3, there is no general way of retrieving versioned secrets via the secret store anymore. Secret versioning is highly dependent on the secret provider implementation, which makes a general way of contacting rather troublesome. | ||
|
|
||
| Our Azure Key Vault secret provider is the only provider that supports secret versioning, that is why we introduced a new `KeyVaultSecretProvider.GetVersionedSecretsAsync` operation that can be used as alternative. | ||
|
|
||
| ```diff | ||
| var provider = store.GetProvider<KeyVaultSecretProvider>("Admin secrets"); | ||
|
|
||
| int amountOfVersions = 3; | ||
| - IEnumerable<Secret> secrets = await provider.GetSecretsAsync("<name>", amountOfVersions); | ||
| + SecretsResult result = await provider.GetVersionedSecretsAsync("<name>", amountOfVersions); | ||
| ``` | ||
|
|
||
| :::info | ||
| The `SecretsResult` acts the same way as the `SecretResult`, only for a collection of secrets (implements `IEnumerable<SecretResult>`). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that I read this, maybe we should've named it
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is more a result than a collection of results, though. It's a folded/aggregated result. |
||
| ```csharp | ||
| SecretsResult result = ... | ||
| IEnumerable<SecretResult> secrets = result.ToArray(); | ||
| ``` | ||
| ::: | ||
|
|
||
| ## 🚛 Moved secret provider options | ||
| Additional options on any of the Arcus-provided secret providers are now available via alternative overloads. | ||
|
|
||
| ```diff | ||
| store.AddSecretStore(store => | ||
| { | ||
| - store.AddEnvironmentVariables( | ||
| - name: "Development secrets", | ||
| - mutateSecretName: secretName => secretName.ToUpper()); | ||
| + store.AddEnvironmentVariables(options => | ||
| + { | ||
| + options.ProviderName = "Development secrets"; | ||
| + options.MapSecretName(secretName => secretName.ToUpper()); | ||
| + }) | ||
| }); | ||
| ``` | ||
|
|
||
| :::info[all secret provider-specific options are now consolidated] | ||
| Additional options, specific for secret provider implementations (e.g. 'Prefix` for environment variables) are now also consolidated into a single options model together with the common options 'name' and 'secret name mapping'. | ||
| ::: | ||
|
|
||
| :::tip[provider name default filled-out] | ||
| By default, any secret provider registered in the secret store gets a provider name assigned. If the user does not provide one, the type name is used. This helps better with defect localization and logging -- which is also greatly improved in v3. | ||
| ::: | ||
|
|
||
| ## 🧩 Implementing a custom secret provider differently | ||
| The v3 uses a different `ISecretProvider` interface in the `Arcus.Security` namespace. Different than the previous `Arcus.Security.Core.ISecretProvider`, is that it now always supports synchronous/asynchronous operations (previously, there existed an `ISyncSecretProvider` to do synchronous secret operations). Implementing a custom secret provider should therefore take this into account. | ||
|
|
||
| ```diff | ||
| - using Arcus.Security.Core; | ||
| + using Arcus.Security; | ||
|
|
||
| public class MySecretProvider : ISecretProvider | ||
| { | ||
| - public Task<Secret> GetSecretAsync(string secretName) { ... } | ||
| - public Task<string> GetRawSecretAsync(string secretName) { ... } | ||
| + public Task<SecretResult> GetSecretAsync(string secretName) { ... } | ||
| + public SecretResult GetSecret(string secretName) { ... } | ||
| } | ||
| ``` | ||
|
|
||
| :::tip[default asynchronous implementation] | ||
| The new `ISecretProvider` has a default `Task.FromResult(GetSecret(...))` interface implementation for the asynchronous operation. Which means that synchronous-only secret providers only need to implement the `GetSecret(...)` member. | ||
| ::: | ||
|
|
||
| > 🔗 For custom options on your secret provider, see the [dedicated feature documentation page](../03-Features/secret-store/custom-secret-provider.md). This page also talks about how custom secret provider implementations and interact with the secret store cache. | ||
|
|
||
| :::warning[extending existing secret providers] | ||
| In v3, we stopped the support for extending existing secret providers with inheriting. Mostly because due to the internal refactoring and simplification of the secret store, extending providers becomes unnecessary. | ||
| ::: | ||
|
|
||
| ## 🗑️ Removed secret auditing with Arcus.Observability | ||
| There is no built-in secret auditing anymore in v3 using Arcus.Observability's custom event tracking. This means that no such options can be configured anymore on the secret store and that `Arcus.Observability` is removed from the transient dependencies. | ||
|
|
||
| ```diff | ||
| services.AddSecretStore(store => | ||
| { | ||
| - store.WithAuditing(...); | ||
| }) | ||
| ``` | ||
|
|
||
| ## Secret provider implementations | ||
| ### `KeyVaultSecretProvider.StoreSecretAsync` ➡️ `.SetSecretAsync` | ||
| Storing a secret in Azure Key Vault with the `KeyVaultSecretProvider` happens now with the more streamlined `.SetSecretAsync(...)` method. | ||
|
|
||
| ```diff | ||
| - using Arcus.Security.Core; | ||
| + using Arcus.Security; | ||
| using Arcus.Security.Providers.AzureKeyVault; | ||
|
|
||
| ISecretStore store = ... | ||
| var provider = store.GetProvider<KeyVaultSecretProvider>("Admin secrets"); | ||
|
|
||
| - Secret newSecret = await provider.StoreSecretAsync("<name>", "<new-value>"); | ||
| + SecretResult result = await provider.SetSecretAsync("<name>", "<new-value>"); | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The namespace is mentioned in the typename, so is it necessary to add the '(in new namespace)' here ?
Hmm, maybe to emphasize that the change is in deed in the namespacename.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, would be in favor of a full namespace here to show the different location.