diff --git a/src/.editorconfig b/.editorconfig similarity index 100% rename from src/.editorconfig rename to .editorconfig diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0cfbbc..499107c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,6 @@ jobs: - name: Scan for Vulnerabilities shell: bash run: | - cd src dotnet restore dotnet list package --vulnerable --include-transitive | tee vulnerabilities.txt ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" @@ -53,11 +52,11 @@ jobs: - name: Compile shell: bash run: | - dotnet build --configuration ${{ matrix.configuration }} --framework=${{ matrix.framework }} src/EventStore.Plugins${{ matrix.test }} + dotnet build --configuration ${{ matrix.configuration }} --framework=${{ matrix.framework }} test/EventStore.Plugins${{ matrix.test }} - name: Run Tests shell: bash run: | dotnet test --configuration ${{ matrix.configuration }} --framework=${{ matrix.framework }} --blame \ --logger:"GitHubActions;report-warnings=false" \ --logger:"console;verbosity=normal" \ - src/EventStore.Plugins${{ matrix.test }} + test/EventStore.Plugins${{ matrix.test }} diff --git a/src/EventStore.Plugins.sln b/EventStore.Plugins.sln similarity index 85% rename from src/EventStore.Plugins.sln rename to EventStore.Plugins.sln index fea9b4a..2224bb3 100644 --- a/src/EventStore.Plugins.sln +++ b/EventStore.Plugins.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.8.34309.116 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Plugins", "EventStore.Plugins\EventStore.Plugins.csproj", "{504B01C5-281F-4CEA-A12F-D5333AB901EB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Plugins", "src\EventStore.Plugins\EventStore.Plugins.csproj", "{504B01C5-281F-4CEA-A12F-D5333AB901EB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Plugins.Tests", "EventStore.Plugins.Tests\EventStore.Plugins.Tests.csproj", "{8D893FD3-3D17-4EEB-9F5A-A404237B6E78}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Plugins.Tests", "test\EventStore.Plugins.Tests\EventStore.Plugins.Tests.csproj", "{8D893FD3-3D17-4EEB-9F5A-A404237B6E78}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/EventStore.Plugins.Tests/ConfigurationReaderTests/LdapsSettings.cs b/src/EventStore.Plugins.Tests/ConfigurationReaderTests/LdapsSettings.cs deleted file mode 100644 index d4172f8..0000000 --- a/src/EventStore.Plugins.Tests/ConfigurationReaderTests/LdapsSettings.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; - -namespace EventStore.Plugins.Tests.ConfigurationReaderTests { - public class LdapsSettings { - public LdapsSettings() { - Port = 636; - ObjectClass = "organizationalPerson"; - Filter = "sAMAccountName"; - GroupMembershipAttribute = "memberOf"; - PrincipalCacheDurationSec = 60; - UseSSL = true; - } - - public string Host { get; set; } - public int Port { get; set; } - public bool ValidateServerCertificate { get; set; } - public bool UseSSL { get; set; } - - public bool AnonymousBind { get; set; } - public string BindUser { get; set; } - public string BindPassword { get; set; } - - public string BaseDn { get; set; } - public string ObjectClass { get; set; } - public string Filter { get; set; } - public string GroupMembershipAttribute { get; set; } - - public bool RequireGroupMembership { get; set; } - public string RequiredGroupDn { get; set; } - - public int PrincipalCacheDurationSec { get; set; } - - public Dictionary LdapGroupRoles { get; set; } - } -} diff --git a/src/EventStore.Plugins.Tests/ConfigurationReaderTests/when_reading_valid_configuration.cs b/src/EventStore.Plugins.Tests/ConfigurationReaderTests/when_reading_valid_configuration.cs deleted file mode 100644 index bccd21d..0000000 --- a/src/EventStore.Plugins.Tests/ConfigurationReaderTests/when_reading_valid_configuration.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using Xunit; - -namespace EventStore.Plugins.Tests.ConfigurationReaderTests { - public class when_reading_valid_configuration { - [Fact] - public void should_return_correct_options() { - var settings = ConfigParser.ReadConfiguration - (Path.Combine("ConfigurationReaderTests", "valid_node_config.yaml"), "LdapsAuth"); - Assert.Equal("13.64.104.29", settings.Host); - Assert.Equal(389, settings.Port); - Assert.False(settings.ValidateServerCertificate); - Assert.False(settings.UseSSL); - Assert.False(settings.AnonymousBind); - Assert.Equal("mycompany\\binder", settings.BindUser); - Assert.Equal("p@ssw0rd!", settings.BindPassword); - Assert.Equal("ou=Lab,dc=mycompany,dc=local", settings.BaseDn); - Assert.Equal("organizationalPerson", settings.ObjectClass); - Assert.Equal("memberOf", settings.GroupMembershipAttribute); - Assert.False(settings.RequireGroupMembership); - Assert.Equal("RequiredGroupDn", settings.RequiredGroupDn); - Assert.Equal(120, settings.PrincipalCacheDurationSec); - Assert.Equal(new Dictionary { - {"CN=ES-Accounting,CN=Users,DC=mycompany,DC=local", "accounting"}, - {"CN=ES-Operations,CN=Users,DC=mycompany,DC=local", "it"}, - {"CN=ES-Admins,CN=Users,DC=mycompany,DC=local", "$admins"} - }, - settings.LdapGroupRoles); - } - } -} diff --git a/src/EventStore.Plugins.Tests/EventStore.Plugins.Tests.csproj b/src/EventStore.Plugins.Tests/EventStore.Plugins.Tests.csproj deleted file mode 100644 index 603fd7a..0000000 --- a/src/EventStore.Plugins.Tests/EventStore.Plugins.Tests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - net8.0 - false - - - - - - - - - - - - - - - - - - Always - - - diff --git a/src/EventStore.Plugins/Authentication/AuthenticationRequest.cs b/src/EventStore.Plugins/Authentication/AuthenticationRequest.cs index 1b3f8f6..9ee1848 100644 --- a/src/EventStore.Plugins/Authentication/AuthenticationRequest.cs +++ b/src/EventStore.Plugins/Authentication/AuthenticationRequest.cs @@ -1,72 +1,69 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Security.Cryptography.X509Certificates; +using System.Security.Claims; -namespace EventStore.Plugins.Authentication { - public abstract class AuthenticationRequest { - /// - /// The Identifier for the source that this request came from - /// - public readonly string Id; +namespace EventStore.Plugins.Authentication; - /// - /// The name of the principal for the request - /// - public readonly string Name; +public abstract class AuthenticationRequest { + /// + /// Whether a valid client certificate was supplied with the request + /// + public readonly bool HasValidClientCertificate; - /// - /// The supplied password for the request - /// - public readonly string SuppliedPassword; + /// + /// The Identifier for the source that this request came from + /// + public readonly string Id; - /// - /// Whether or not a valid client certificate was supplied with the request - /// - public readonly bool HasValidClientCertificate; + /// + /// The name of the principal for the request + /// + public readonly string Name; - /// - /// All supplied authentication tokens for the request - /// - public readonly IReadOnlyDictionary Tokens; + /// + /// The supplied password for the request + /// + public readonly string SuppliedPassword; - protected AuthenticationRequest(string id, IReadOnlyDictionary tokens) { - ArgumentNullException.ThrowIfNull(id); - ArgumentNullException.ThrowIfNull(tokens); + /// + /// All supplied authentication tokens for the request + /// + public readonly IReadOnlyDictionary Tokens; - Id = id; - Tokens = tokens; - Name = GetToken("uid"); - SuppliedPassword = GetToken("pwd"); - HasValidClientCertificate = GetToken("client-certificate") != null; - } + protected AuthenticationRequest(string? id, IReadOnlyDictionary? tokens) { + ArgumentNullException.ThrowIfNull(id); + ArgumentNullException.ThrowIfNull(tokens); - /// - /// Gets the token corresponding to . - /// - /// - /// - public string GetToken(string key) => Tokens.TryGetValue(key, out var token) ? token : null; + Id = id; + Tokens = tokens; + Name = GetToken("uid") ?? ""; + SuppliedPassword = GetToken("pwd") ?? ""; + HasValidClientCertificate = GetToken("client-certificate") != null; + } - /// - /// The request is unauthorized - /// - public abstract void Unauthorized(); + /// + /// Gets the token corresponding to . + /// + /// + /// + public string? GetToken(string key) => Tokens.GetValueOrDefault(key); - /// - /// The request was successfully authenticated - /// - /// The of the authenticated request - public abstract void Authenticated(ClaimsPrincipal principal); + /// + /// The request is unauthorized + /// + public abstract void Unauthorized(); - /// - /// An error occurred during authentication - /// - public abstract void Error(); + /// + /// The request was successfully authenticated + /// + /// The of the authenticated request + public abstract void Authenticated(ClaimsPrincipal principal); - /// - /// The authentication provider is not yet ready to service the request - /// - public abstract void NotReady(); - } -} + /// + /// An error occurred during authentication + /// + public abstract void Error(); + + /// + /// The authentication provider is not yet ready to service the request + /// + public abstract void NotReady(); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authentication/HttpAuthenticationRequest.cs b/src/EventStore.Plugins/Authentication/HttpAuthenticationRequest.cs index 5afac9b..e6a7250 100644 --- a/src/EventStore.Plugins/Authentication/HttpAuthenticationRequest.cs +++ b/src/EventStore.Plugins/Authentication/HttpAuthenticationRequest.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace EventStore.Plugins.Authentication; @@ -12,58 +9,53 @@ public enum HttpAuthenticationRequestStatus { Error, NotReady, Unauthenticated, - Authenticated, + Authenticated } public class HttpAuthenticationRequest : AuthenticationRequest { - private readonly TaskCompletionSource<(HttpAuthenticationRequestStatus, ClaimsPrincipal)> _tcs; - private readonly CancellationTokenRegistration _cancellationRegister; + readonly CancellationTokenRegistration _cancellationRegister; + readonly TaskCompletionSource<(HttpAuthenticationRequestStatus, ClaimsPrincipal?)> _tcs; public HttpAuthenticationRequest(HttpContext context, string authToken) : this(context, new Dictionary { ["jwt"] = authToken - }) { - } + }) { } public HttpAuthenticationRequest(HttpContext context, string name, string suppliedPassword) : this(context, new Dictionary { ["uid"] = name, ["pwd"] = suppliedPassword - }) { - } + }) { } - public static HttpAuthenticationRequest CreateWithValidCertificate(HttpContext context, string name, X509Certificate2 clientCertificate) => - new(context, new Dictionary { - ["uid"] = name, - ["client-certificate"] = clientCertificate.ExportCertificatePem(), - }); - - private HttpAuthenticationRequest(HttpContext context, IReadOnlyDictionary tokens) : base( + HttpAuthenticationRequest(HttpContext context, IReadOnlyDictionary tokens) : base( context.TraceIdentifier, tokens) { _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); _cancellationRegister = context.RequestAborted.Register(Cancel); } - private void Cancel() { + public static HttpAuthenticationRequest CreateWithValidCertificate(HttpContext context, string name, X509Certificate2 clientCertificate) => new(context, + new Dictionary { + ["uid"] = name, + ["client-certificate"] = clientCertificate.ExportCertificatePem() + }); + + void Cancel() { _tcs.TrySetCanceled(); _cancellationRegister.Dispose(); } - public override void Unauthorized() { + public override void Unauthorized() => _tcs.TrySetResult((HttpAuthenticationRequestStatus.Unauthenticated, default)); - } - public override void Authenticated(ClaimsPrincipal principal) { + public override void Authenticated(ClaimsPrincipal principal) => _tcs.TrySetResult((HttpAuthenticationRequestStatus.Authenticated, principal)); - } - public override void Error() { + public override void Error() => _tcs.TrySetResult((HttpAuthenticationRequestStatus.Error, default)); - } - public override void NotReady() { + public override void NotReady() => _tcs.TrySetResult((HttpAuthenticationRequestStatus.NotReady, default)); - } - public Task<(HttpAuthenticationRequestStatus, ClaimsPrincipal)> AuthenticateAsync() => _tcs.Task; -} + public Task<(HttpAuthenticationRequestStatus, ClaimsPrincipal?)> AuthenticateAsync() => + _tcs.Task; +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authentication/IAuthenticationPlugin.cs b/src/EventStore.Plugins/Authentication/IAuthenticationPlugin.cs index 51287b5..e357b53 100644 --- a/src/EventStore.Plugins/Authentication/IAuthenticationPlugin.cs +++ b/src/EventStore.Plugins/Authentication/IAuthenticationPlugin.cs @@ -1,14 +1,13 @@ -namespace EventStore.Plugins.Authentication { - public interface IAuthenticationPlugin { - string Name { get; } - string Version { get; } +namespace EventStore.Plugins.Authentication; - string CommandLineName { get; } +public interface IAuthenticationPlugin { + string Name { get; } + string Version { get; } + string CommandLineName { get; } - /// - /// Creates an authentication provider factory for the authentication plugin - /// - /// The path to the configuration file for the authentication plugin - IAuthenticationProviderFactory GetAuthenticationProviderFactory(string authenticationConfigPath); - } -} + /// + /// Creates an authentication provider factory for the authentication plugin + /// + /// The path to the configuration file for the authentication plugin + IAuthenticationProviderFactory GetAuthenticationProviderFactory(string authenticationConfigPath); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authentication/IAuthenticationProvider.cs b/src/EventStore.Plugins/Authentication/IAuthenticationProvider.cs index a076c2e..9c33b1f 100644 --- a/src/EventStore.Plugins/Authentication/IAuthenticationProvider.cs +++ b/src/EventStore.Plugins/Authentication/IAuthenticationProvider.cs @@ -1,40 +1,33 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing; -namespace EventStore.Plugins.Authentication { - public interface IAuthenticationProvider : IPlugableComponent { - /// - /// Initialize the AuthenticationProvider. Event Store will wait until this task completes before becoming ready. - /// - Task Initialize(); +namespace EventStore.Plugins.Authentication; - /// - /// Authenticate an AuthenticationRequest. Call the appropriate method on - /// depending on whether the request succeeded, failed, or errored. - /// - /// - void Authenticate(AuthenticationRequest authenticationRequest); +public interface IAuthenticationProvider : IPlugableComponent { + /// + /// Initialize the AuthenticationProvider. Event Store will wait until this task completes before becoming ready. + /// + Task Initialize(); - /// - /// Return a unique name used to externally identify the authentication provider. - /// - string Name { get; } + /// + /// Authenticate an AuthenticationRequest. Call the appropriate method on + /// depending on whether the request succeeded, failed, or errored. + /// + /// + void Authenticate(AuthenticationRequest authenticationRequest); - /// - /// Get public properties which may be required for the authentication flow. - /// - IEnumerable> GetPublicProperties(); + /// + /// Get public properties which may be required for the authentication flow. + /// + IEnumerable> GetPublicProperties(); - /// - /// Create any required endpoints. - /// - /// - void ConfigureEndpoints(IEndpointRouteBuilder endpointRouteBuilder); + /// + /// Create any required endpoints. + /// + /// + void ConfigureEndpoints(IEndpointRouteBuilder endpointRouteBuilder); - /// - /// Get supported authentication schemes. - /// - IReadOnlyList GetSupportedAuthenticationSchemes(); - } -} + /// + /// Get supported authentication schemes. + /// + IReadOnlyList GetSupportedAuthenticationSchemes(); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authentication/IAuthenticationProviderFactory.cs b/src/EventStore.Plugins/Authentication/IAuthenticationProviderFactory.cs index f56193b..ea4c78d 100644 --- a/src/EventStore.Plugins/Authentication/IAuthenticationProviderFactory.cs +++ b/src/EventStore.Plugins/Authentication/IAuthenticationProviderFactory.cs @@ -1,15 +1,14 @@ -using Serilog; +using Microsoft.Extensions.Logging; -namespace EventStore.Plugins.Authentication { - public interface IAuthenticationProviderFactory { - /// - /// Build an AuthenticationProvider for the authentication plugin - /// - /// - /// Whether the Authentication Provider should log failed authentication - /// attempts - /// - /// The to use when logging in the plugin - IAuthenticationProvider Build(bool logFailedAuthenticationAttempts, ILogger logger); - } -} +namespace EventStore.Plugins.Authentication; + +public interface IAuthenticationProviderFactory { + /// + /// Build an AuthenticationProvider for the authentication plugin + /// + /// + /// Whether the Authentication Provider should log failed authentication attempts + /// + /// The to use when logging in the plugin + IAuthenticationProvider Build(bool logFailedAuthenticationAttempts, ILogger logger); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authentication/IHttpAuthenticationProvider.cs b/src/EventStore.Plugins/Authentication/IHttpAuthenticationProvider.cs index cdfa3d3..1a388e7 100644 --- a/src/EventStore.Plugins/Authentication/IHttpAuthenticationProvider.cs +++ b/src/EventStore.Plugins/Authentication/IHttpAuthenticationProvider.cs @@ -9,4 +9,4 @@ public interface IHttpAuthenticationProvider { string Name { get; } bool Authenticate(HttpContext context, out HttpAuthenticationRequest request); -} +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authorization/IAuthorizationPlugin.cs b/src/EventStore.Plugins/Authorization/IAuthorizationPlugin.cs index 032ce61..52c4454 100644 --- a/src/EventStore.Plugins/Authorization/IAuthorizationPlugin.cs +++ b/src/EventStore.Plugins/Authorization/IAuthorizationPlugin.cs @@ -1,14 +1,13 @@ -namespace EventStore.Plugins.Authorization { - public interface IAuthorizationPlugin { - string Name { get; } - string Version { get; } +namespace EventStore.Plugins.Authorization; - string CommandLineName { get; } +public interface IAuthorizationPlugin { + string Name { get; } + string Version { get; } + string CommandLineName { get; } - /// - /// Creates an authorization provider factory for the authorization plugin - /// - /// The path to the configuration file for the authorization plugin - IAuthorizationProviderFactory GetAuthorizationProviderFactory(string authorizationConfigPath); - } -} + /// + /// Creates an authorization provider factory for the authorization plugin + /// + /// The path to the configuration file for the authorization plugin + IAuthorizationProviderFactory GetAuthorizationProviderFactory(string authorizationConfigPath); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authorization/IAuthorizationProvider.cs b/src/EventStore.Plugins/Authorization/IAuthorizationProvider.cs index 29e4ed3..e15a0b7 100644 --- a/src/EventStore.Plugins/Authorization/IAuthorizationProvider.cs +++ b/src/EventStore.Plugins/Authorization/IAuthorizationProvider.cs @@ -1,12 +1,10 @@ using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -namespace EventStore.Plugins.Authorization { - public interface IAuthorizationProvider : IPlugableComponent { - /// - /// Check whether the provided has the rights to perform the - /// - ValueTask CheckAccessAsync(ClaimsPrincipal cp, Operation operation, CancellationToken ct); - } -} +namespace EventStore.Plugins.Authorization; + +public interface IAuthorizationProvider : IPlugableComponent { + /// + /// Check whether the provided has the rights to perform the + /// + ValueTask CheckAccessAsync(ClaimsPrincipal cp, Operation operation, CancellationToken ct); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authorization/IAuthorizationProviderFactory.cs b/src/EventStore.Plugins/Authorization/IAuthorizationProviderFactory.cs index dd87912..f150bda 100644 --- a/src/EventStore.Plugins/Authorization/IAuthorizationProviderFactory.cs +++ b/src/EventStore.Plugins/Authorization/IAuthorizationProviderFactory.cs @@ -1,8 +1,8 @@ -namespace EventStore.Plugins.Authorization { - public interface IAuthorizationProviderFactory { - /// - /// Build an AuthorizationProvider for the authorization plugin - /// - IAuthorizationProvider Build(); - } -} +namespace EventStore.Plugins.Authorization; + +public interface IAuthorizationProviderFactory { + /// + /// Build an AuthorizationProvider for the authorization plugin + /// + IAuthorizationProvider Build(); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authorization/Operation.cs b/src/EventStore.Plugins/Authorization/Operation.cs index 50ccb61..af90357 100644 --- a/src/EventStore.Plugins/Authorization/Operation.cs +++ b/src/EventStore.Plugins/Authorization/Operation.cs @@ -1,61 +1,51 @@ -using System; -using System.Text; +using System.Text; -namespace EventStore.Plugins.Authorization { - public readonly struct Operation { - public string Resource { get; } - public string Action { get; } - public ReadOnlyMemory Parameters { get; } +namespace EventStore.Plugins.Authorization; - public Operation(OperationDefinition definition) : this(definition.Resource, definition.Action) { - } +public readonly struct Operation { + public string Resource { get; } + public string Action { get; } + public ReadOnlyMemory Parameters { get; } - public Operation(string resource, string action) : this(resource, action, Array.Empty()) { } + public Operation(OperationDefinition definition) : this(definition.Resource, definition.Action) { } - public Operation WithParameter(string name, string value) { - return WithParameters(new Parameter(name, value)); - } + public Operation(string resource, string action) : this(resource, action, Array.Empty()) { } - public Operation WithParameter(Parameter parameter) { - return WithParameters(parameter); - } + public Operation WithParameter(string name, string value) => WithParameters(new Parameter(name, value)); - public Operation WithParameters(ReadOnlyMemory parameters) { - var memory = new Memory(new Parameter[Parameters.Length + parameters.Length]); - if (!Parameters.IsEmpty) Parameters.CopyTo(memory); - parameters.CopyTo(memory.Slice(Parameters.Length)); - return new Operation(Resource, Action, memory); - } + public Operation WithParameter(Parameter parameter) => WithParameters(parameter); - public Operation WithParameters(params Parameter[] parameters) { - return WithParameters(new ReadOnlyMemory(parameters)); - } + public Operation WithParameters(ReadOnlyMemory parameters) { + var memory = new Memory(new Parameter[Parameters.Length + parameters.Length]); + if (!Parameters.IsEmpty) Parameters.CopyTo(memory); + parameters.CopyTo(memory.Slice(Parameters.Length)); + return new(Resource, Action, memory); + } - public Operation(string resource, string action, Memory parameters) { - Resource = resource; - Action = action; - Parameters = parameters; - } + public Operation WithParameters(params Parameter[] parameters) => WithParameters(new ReadOnlyMemory(parameters)); - public static implicit operator OperationDefinition(Operation operation) { - return new OperationDefinition(operation.Resource, operation.Action); - } + public Operation(string resource, string action, Memory parameters) { + Resource = resource; + Action = action; + Parameters = parameters; + } - public override string ToString() { - var sb = new StringBuilder(); - sb.Append($"{Resource} : {Action}"); - var parameters = Parameters.Span; - if (!parameters.IsEmpty) { - sb.Append(" p: {"); - while (!parameters.IsEmpty) { - sb.Append($"{parameters[0].Name} : {parameters[0].Value}"); - parameters = parameters.Slice(1); - } - - sb.Append("}"); + public static implicit operator OperationDefinition(Operation operation) => new(operation.Resource, operation.Action); + + public override string ToString() { + var sb = new StringBuilder(); + sb.Append($"{Resource} : {Action}"); + var parameters = Parameters.Span; + if (!parameters.IsEmpty) { + sb.Append(" p: {"); + while (!parameters.IsEmpty) { + sb.Append($"{parameters[0].Name} : {parameters[0].Value}"); + parameters = parameters.Slice(1); } - return sb.ToString(); + sb.Append("}"); } + + return sb.ToString(); } -} +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authorization/OperationDefinition.cs b/src/EventStore.Plugins/Authorization/OperationDefinition.cs index 3e352af..044c734 100644 --- a/src/EventStore.Plugins/Authorization/OperationDefinition.cs +++ b/src/EventStore.Plugins/Authorization/OperationDefinition.cs @@ -1,35 +1,3 @@ -using System; +namespace EventStore.Plugins.Authorization; -namespace EventStore.Plugins.Authorization { - public readonly struct OperationDefinition : IEquatable { - public string Resource { get; } - public string Action { get; } - - public OperationDefinition(string resource, string action) { - Resource = resource; - Action = action; - } - - public bool Equals(OperationDefinition other) { - return Resource == other.Resource && Action == other.Action; - } - - public override bool Equals(object obj) { - return obj is OperationDefinition other && Equals(other); - } - - public override int GetHashCode() { - unchecked { - return (Resource.GetHashCode() * 397) ^ Action.GetHashCode(); - } - } - - public static bool operator ==(OperationDefinition left, OperationDefinition right) { - return left.Equals(right); - } - - public static bool operator !=(OperationDefinition left, OperationDefinition right) { - return !left.Equals(right); - } - } -} +public readonly record struct OperationDefinition(string Resource, string Action); \ No newline at end of file diff --git a/src/EventStore.Plugins/Authorization/Operations.cs b/src/EventStore.Plugins/Authorization/Operations.cs index ebb6c0f..a8edfca 100644 --- a/src/EventStore.Plugins/Authorization/Operations.cs +++ b/src/EventStore.Plugins/Authorization/Operations.cs @@ -1,197 +1,177 @@ -namespace EventStore.Plugins.Authorization { - public static class Operations { - public static class Node { - private const string Resource = "node"; - public static readonly OperationDefinition Redirect = new Operation(Resource, "redirect"); - public static readonly OperationDefinition Options = new Operation(Resource, "options"); - public static readonly OperationDefinition Ping = new Operation(Resource, "ping"); - - public static readonly OperationDefinition StaticContent = - new OperationDefinition(Resource + "/content", "read"); - - public static readonly OperationDefinition Shutdown = new OperationDefinition(Resource, "shutdown"); - public static readonly OperationDefinition ReloadConfiguration = new OperationDefinition(Resource, "reloadConfiguration"); - public static readonly OperationDefinition MergeIndexes = new OperationDefinition(Resource, "mergeIndexes"); - public static readonly OperationDefinition SetPriority = new OperationDefinition(Resource, "setPriority"); - public static readonly OperationDefinition Resign = new OperationDefinition(Resource, "resign"); - - public static readonly OperationDefinition Login = new Operation(Resource, "login"); - - public static class Scavenge { - private const string Resource = Node.Resource + "/scavenge"; - public static readonly OperationDefinition Start = new OperationDefinition(Resource, "start"); - public static readonly OperationDefinition Stop = new OperationDefinition(Resource, "stop"); - public static readonly OperationDefinition Read = new OperationDefinition(Resource, "read"); - } - - public static class Redaction { - private const string Resource = Node.Resource + "/redaction"; - public static readonly OperationDefinition SwitchChunk = new OperationDefinition(Resource, "switchChunk"); - } - - public static class Information { - private const string Resource = Node.Resource + "/info"; - - public static readonly OperationDefinition Subsystems = - new OperationDefinition(Resource + "/subsystems", "read"); - - public static readonly OperationDefinition Histogram = - new OperationDefinition(Resource + "/histograms", "read"); - - public static readonly OperationDefinition Read = new OperationDefinition(Resource, "read"); - - public static readonly OperationDefinition Options = - new OperationDefinition(Resource + "/options", "read"); - } - - public static class Statistics { - private const string Resource = Node.Resource + "/stats"; - public static readonly OperationDefinition Read = new OperationDefinition(Resource, "read"); - - public static readonly OperationDefinition Replication = - new OperationDefinition(Resource + "/replication", "read"); - - public static readonly OperationDefinition Tcp = new OperationDefinition(Resource + "/tcp", "read"); - - public static readonly OperationDefinition Custom = - new OperationDefinition(Resource + "/custom", "read"); - } - - public static class Elections { - private const string Resource = Node.Resource + "/elections"; - public static readonly OperationDefinition ViewChange = new OperationDefinition(Resource, "viewchange"); - - public static readonly OperationDefinition ViewChangeProof = - new OperationDefinition(Resource, "viewchangeproof"); - - public static readonly OperationDefinition Prepare = new OperationDefinition(Resource, "prepare"); - public static readonly OperationDefinition PrepareOk = new OperationDefinition(Resource, "prepareOk"); - public static readonly OperationDefinition Proposal = new OperationDefinition(Resource, "proposal"); - public static readonly OperationDefinition Accept = new OperationDefinition(Resource, "accept"); - - public static readonly OperationDefinition LeaderIsResigning = - new OperationDefinition(Resource, "leaderisresigning"); - - public static readonly OperationDefinition LeaderIsResigningOk = - new OperationDefinition(Resource, "leaderisresigningok"); - } - - public static class Gossip { - private const string Resource = Node.Resource + "/gossip"; - public static readonly OperationDefinition Read = new OperationDefinition(Resource, "read"); - public static readonly OperationDefinition Update = new OperationDefinition(Resource, "update"); - public static readonly OperationDefinition ClientRead = new OperationDefinition(Resource + "/client", "read"); - } +namespace EventStore.Plugins.Authorization; + +public static class Operations { + public static class Node { + const string Resource = "node"; + public static readonly OperationDefinition Redirect = new Operation(Resource, "redirect"); + public static readonly OperationDefinition Options = new Operation(Resource, "options"); + public static readonly OperationDefinition Ping = new Operation(Resource, "ping"); + + public static readonly OperationDefinition StaticContent = new($"{Resource}/content", "read"); + + public static readonly OperationDefinition Shutdown = new(Resource, "shutdown"); + public static readonly OperationDefinition ReloadConfiguration = new(Resource, "reloadConfiguration"); + public static readonly OperationDefinition MergeIndexes = new(Resource, "mergeIndexes"); + public static readonly OperationDefinition SetPriority = new(Resource, "setPriority"); + public static readonly OperationDefinition Resign = new(Resource, "resign"); + + public static readonly OperationDefinition Login = new Operation(Resource, "login"); + + public static class Scavenge { + const string Resource = $"{Node.Resource}/scavenge"; + public static readonly OperationDefinition Start = new(Resource, "start"); + public static readonly OperationDefinition Stop = new(Resource, "stop"); + public static readonly OperationDefinition Read = new(Resource, "read"); } - public static class Streams { - private const string Resource = "streams"; - public static readonly OperationDefinition Read = new OperationDefinition(Resource, "read"); - public static readonly OperationDefinition Write = new OperationDefinition(Resource, "write"); - public static readonly OperationDefinition Delete = new OperationDefinition(Resource, "delete"); - public static readonly OperationDefinition MetadataRead = new OperationDefinition(Resource, "metadataRead"); + public static class Redaction { + const string Resource = $"{Node.Resource}/redaction"; + public static readonly OperationDefinition SwitchChunk = new(Resource, "switchChunk"); + } + + public static class Information { + const string Resource = $"{Node.Resource}/info"; + + public static readonly OperationDefinition Subsystems = new($"{Resource}/subsystems", "read"); + + public static readonly OperationDefinition Histogram = new($"{Resource}/histograms", "read"); + + public static readonly OperationDefinition Read = new(Resource, "read"); + + public static readonly OperationDefinition Options = new($"{Resource}/options", "read"); + } + + public static class Statistics { + const string Resource = $"{Node.Resource}/stats"; + public static readonly OperationDefinition Read = new(Resource, "read"); + + public static readonly OperationDefinition Replication = new($"{Resource}/replication", "read"); + + public static readonly OperationDefinition Tcp = new($"{Resource}/tcp", "read"); + + public static readonly OperationDefinition Custom = new($"{Resource}/custom", "read"); + } + + public static class Elections { + const string Resource = $"{Node.Resource}/elections"; + public static readonly OperationDefinition ViewChange = new(Resource, "viewchange"); + + public static readonly OperationDefinition ViewChangeProof = new(Resource, "viewchangeproof"); + + public static readonly OperationDefinition Prepare = new(Resource, "prepare"); + public static readonly OperationDefinition PrepareOk = new(Resource, "prepareOk"); + public static readonly OperationDefinition Proposal = new(Resource, "proposal"); + public static readonly OperationDefinition Accept = new(Resource, "accept"); - public static readonly OperationDefinition MetadataWrite = - new OperationDefinition(Resource, "metadataWrite"); + public static readonly OperationDefinition LeaderIsResigning = new(Resource, "leaderisresigning"); - public static class Parameters { - public static Parameter StreamId(string streamId) => new Parameter("streamId", streamId); + public static readonly OperationDefinition LeaderIsResigningOk = new(Resource, "leaderisresigningok"); + } - public static Parameter TransactionId(long transactionId) => - new Parameter("transactionId", transactionId.ToString("D")); - } + public static class Gossip { + const string Resource = $"{Node.Resource}/gossip"; + public static readonly OperationDefinition Read = new(Resource, "read"); + public static readonly OperationDefinition Update = new(Resource, "update"); + public static readonly OperationDefinition ClientRead = new($"{Resource}/client", "read"); } + } - public static class Subscriptions { - private const string Resource = "subscriptions"; - public static readonly OperationDefinition Statistics = new OperationDefinition(Resource, "statistics"); - public static readonly OperationDefinition Create = new OperationDefinition(Resource, "create"); - public static readonly OperationDefinition Update = new OperationDefinition(Resource, "update"); - public static readonly OperationDefinition Delete = new OperationDefinition(Resource, "delete"); - public static readonly OperationDefinition ReplayParked = new OperationDefinition(Resource, "replay"); - public static readonly OperationDefinition Restart = new OperationDefinition(Resource, "restart"); + public static class Streams { + const string Resource = "streams"; + public static readonly OperationDefinition Read = new(Resource, "read"); + public static readonly OperationDefinition Write = new(Resource, "write"); + public static readonly OperationDefinition Delete = new(Resource, "delete"); + public static readonly OperationDefinition MetadataRead = new(Resource, "metadataRead"); - public static readonly OperationDefinition ProcessMessages = new OperationDefinition(Resource, "process"); + public static readonly OperationDefinition MetadataWrite = new(Resource, "metadataWrite"); + public static class Parameters { + public static Parameter StreamId(string streamId) => new("streamId", streamId); - public static class Parameters { - public static Parameter SubscriptionId(string id) => new Parameter("subscriptionId", id); - public static Parameter StreamId(string streamId) => new Parameter("streamId", streamId); - } + public static Parameter TransactionId(long transactionId) => new("transactionId", transactionId.ToString("D")); } + } + + public static class Subscriptions { + const string Resource = "subscriptions"; + public static readonly OperationDefinition Statistics = new(Resource, "statistics"); + public static readonly OperationDefinition Create = new(Resource, "create"); + public static readonly OperationDefinition Update = new(Resource, "update"); + public static readonly OperationDefinition Delete = new(Resource, "delete"); + public static readonly OperationDefinition ReplayParked = new(Resource, "replay"); + public static readonly OperationDefinition Restart = new(Resource, "restart"); - public static class Users { - private const string Resource = "users"; - public static readonly OperationDefinition Create = new OperationDefinition(Resource, "create"); - public static readonly OperationDefinition Update = new OperationDefinition(Resource, "update"); - public static readonly OperationDefinition Delete = new OperationDefinition(Resource, "delete"); - public static readonly OperationDefinition List = new OperationDefinition(Resource, "list"); - public static readonly OperationDefinition Read = new OperationDefinition(Resource, "read"); - public static readonly OperationDefinition CurrentUser = new OperationDefinition(Resource, "self"); - public static readonly OperationDefinition Enable = new OperationDefinition(Resource, "enable"); - public static readonly OperationDefinition Disable = new OperationDefinition(Resource, "disable"); - - public static readonly OperationDefinition ResetPassword = - new OperationDefinition(Resource, "resetPassword"); - - public static readonly OperationDefinition ChangePassword = - new OperationDefinition(Resource, "updatePassword"); - - public static class Parameters { - public const string UserParameterName = "user"; - - public static Parameter User(string userId) { - return new Parameter(UserParameterName, userId); - } - } + public static readonly OperationDefinition ProcessMessages = new(Resource, "process"); + + + public static class Parameters { + public static Parameter SubscriptionId(string id) => new("subscriptionId", id); + + public static Parameter StreamId(string streamId) => new("streamId", streamId); } + } + + public static class Users { + const string Resource = "users"; + public static readonly OperationDefinition Create = new(Resource, "create"); + public static readonly OperationDefinition Update = new(Resource, "update"); + public static readonly OperationDefinition Delete = new(Resource, "delete"); + public static readonly OperationDefinition List = new(Resource, "list"); + public static readonly OperationDefinition Read = new(Resource, "read"); + public static readonly OperationDefinition CurrentUser = new(Resource, "self"); + public static readonly OperationDefinition Enable = new(Resource, "enable"); + public static readonly OperationDefinition Disable = new(Resource, "disable"); + + public static readonly OperationDefinition ResetPassword = new(Resource, "resetPassword"); + + public static readonly OperationDefinition ChangePassword = new(Resource, "updatePassword"); + + public static class Parameters { + public const string UserParameterName = "user"; + + public static Parameter User(string userId) => new(UserParameterName, userId); + } + } - public static class Projections { - private const string Resource = "projections"; + public static class Projections { + const string Resource = "projections"; - public static readonly OperationDefinition Create = new OperationDefinition(Resource, "create"); - public static readonly OperationDefinition Update = new OperationDefinition(Resource, "update"); - public static readonly OperationDefinition Read = new OperationDefinition(Resource, "read"); + public static readonly OperationDefinition Create = new(Resource, "create"); + public static readonly OperationDefinition Update = new(Resource, "update"); + public static readonly OperationDefinition Read = new(Resource, "read"); - public static readonly OperationDefinition Abort = new OperationDefinition(Resource, "abort"); + public static readonly OperationDefinition Abort = new(Resource, "abort"); - public static readonly OperationDefinition List = new OperationDefinition(Resource, "list"); - public static readonly OperationDefinition Restart = new OperationDefinition(Resource, "restart"); - public static readonly OperationDefinition Delete = new OperationDefinition(Resource, "delete"); - public static readonly OperationDefinition Enable = new OperationDefinition(Resource, "enable"); - public static readonly OperationDefinition Disable = new OperationDefinition(Resource, "disable"); - public static readonly OperationDefinition Reset = new OperationDefinition(Resource, "reset"); + public static readonly OperationDefinition List = new(Resource, "list"); + public static readonly OperationDefinition Restart = new(Resource, "restart"); + public static readonly OperationDefinition Delete = new(Resource, "delete"); + public static readonly OperationDefinition Enable = new(Resource, "enable"); + public static readonly OperationDefinition Disable = new(Resource, "disable"); + public static readonly OperationDefinition Reset = new(Resource, "reset"); - public static readonly OperationDefinition ReadConfiguration = - new OperationDefinition(Resource + "/configuration", "read"); + public static readonly OperationDefinition ReadConfiguration = new($"{Resource}/configuration", "read"); - public static readonly OperationDefinition UpdateConfiguration = - new OperationDefinition(Resource + "/configuration", "update"); + public static readonly OperationDefinition UpdateConfiguration = new($"{Resource}/configuration", "update"); - public static readonly OperationDefinition Status = new OperationDefinition(Resource, "status"); + public static readonly OperationDefinition Status = new(Resource, "status"); - //can be Partition based - public static readonly OperationDefinition State = new OperationDefinition(Resource, "state"); - public static readonly OperationDefinition Result = new OperationDefinition(Resource, "state"); + //can be Partition based + public static readonly OperationDefinition State = new(Resource, "state"); + public static readonly OperationDefinition Result = new(Resource, "state"); - public static readonly OperationDefinition Statistics = new OperationDefinition(Resource, "statistics"); + public static readonly OperationDefinition Statistics = new(Resource, "statistics"); - //This one is a bit weird - public static readonly OperationDefinition DebugProjection = new OperationDefinition(Resource, "debug"); + //This one is a bit weird + public static readonly OperationDefinition DebugProjection = new(Resource, "debug"); - public static class Parameters { - public static readonly Parameter Query = new Parameter("projectionType", "query"); - public static readonly Parameter OneTime = new Parameter("projectionType", "onetime"); - public static readonly Parameter Continuous = new Parameter("projectionType", "continuous"); + public static class Parameters { + public static readonly Parameter Query = new("projectionType", "query"); + public static readonly Parameter OneTime = new("projectionType", "onetime"); + public static readonly Parameter Continuous = new("projectionType", "continuous"); - public static Parameter Projection(string name) { - return new Parameter("projection", name); - } + public static Parameter Projection(string name) => new("projection", name); - public static Parameter RunAs(string name) { - return new Parameter("runas", name); - } - } + public static Parameter RunAs(string name) => new("runas", name); } } -} +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Authorization/Parameter.cs b/src/EventStore.Plugins/Authorization/Parameter.cs index 120e626..4abf66f 100644 --- a/src/EventStore.Plugins/Authorization/Parameter.cs +++ b/src/EventStore.Plugins/Authorization/Parameter.cs @@ -1,39 +1,5 @@ -using System; +namespace EventStore.Plugins.Authorization; -namespace EventStore.Plugins.Authorization { - public readonly struct Parameter : IEquatable { - public Parameter(string name, string value) { - Name = name ?? throw new ArgumentNullException(nameof(name)); - Value = value ?? throw new ArgumentNullException(nameof(value)); - } - - public string Name { get; } - public string Value { get; } - - public bool Equals(Parameter other) { - return Name == other.Name && Value == other.Value; - } - - public override bool Equals(object obj) { - return obj is Parameter other && Equals(other); - } - - public override int GetHashCode() { - unchecked { - return (Name.GetHashCode() * 397) ^ Value.GetHashCode(); - } - } - - public static bool operator ==(Parameter left, Parameter right) { - return left.Equals(right); - } - - public static bool operator !=(Parameter left, Parameter right) { - return !left.Equals(right); - } - - public override string ToString() { - return $"{Name} : {Value}"; - } - } -} +public readonly record struct Parameter(string Name, string Value) { + public override string ToString() => $"{Name} : {Value}"; +} \ No newline at end of file diff --git a/src/EventStore.Plugins/ConfigParser.cs b/src/EventStore.Plugins/ConfigParser.cs index cbccc59..8d3f299 100644 --- a/src/EventStore.Plugins/ConfigParser.cs +++ b/src/EventStore.Plugins/ConfigParser.cs @@ -1,53 +1,47 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Serilog; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; -namespace EventStore.Plugins { - public static class ConfigParser { - /// - /// Deserializes a section of configuration from a given config file into the provided settings type - /// - /// The path to the configuration file - /// The section to deserialize - /// The type of settings object to create from the configuration - public static T ReadConfiguration(string configPath, string sectionName) where T : class { - try { - var yamlStream = new YamlStream(); - var stringReader = new StringReader(File.ReadAllText(configPath)); - try { - yamlStream.Load(stringReader); - } catch (Exception ex) { - throw new Exception( - $"An invalid configuration file has been specified. {Environment.NewLine}{ex.Message}"); - } - - var yamlNode = (YamlMappingNode)yamlStream.Documents[0].RootNode; - if (!string.IsNullOrEmpty(sectionName)) { - Func, bool> predicate = x => - x.Key.ToString() == sectionName && x.Value is YamlMappingNode; - - var nodeExists = yamlNode.Children.Any(predicate); - if (nodeExists) yamlNode = (YamlMappingNode)yamlNode.Children.First(predicate).Value; - } - - if (yamlNode == null) return default; - - using (var stream = new MemoryStream()) - using (var writer = new StreamWriter(stream)) - using (var reader = new StreamReader(stream)) { - new YamlStream(new YamlDocument(yamlNode)).Save(writer); - writer.Flush(); - stream.Position = 0; - return new Deserializer().Deserialize(reader); - } - } catch (FileNotFoundException ex) { - Log.Error(ex, "Cannot find the specified config file {0}.", configPath); - throw; - } - } - } -} +namespace EventStore.Plugins; + +// move this to ComercialHA +public static class ConfigParser { + /// + /// Deserializes a section of configuration from a given config file into the provided settings type + /// + /// The path to the configuration file + /// The section to deserialize + /// The type of settings object to create from the configuration + public static T? ReadConfiguration(string configPath, string sectionName) where T : class { + var yamlStream = new YamlStream(); + var stringReader = new StringReader(File.ReadAllText(configPath)); + + try { + yamlStream.Load(stringReader); + } + catch (Exception ex) { + throw new( + $"An invalid configuration file has been specified. {Environment.NewLine}{ex.Message}"); + } + + var yamlNode = (YamlMappingNode)yamlStream.Documents[0].RootNode; + if (!string.IsNullOrEmpty(sectionName)) { + Func, bool> predicate = x => + x.Key.ToString() == sectionName && x.Value is YamlMappingNode; + + var nodeExists = yamlNode.Children.Any(predicate); + if (nodeExists) yamlNode = (YamlMappingNode)yamlNode.Children.First(predicate).Value; + } + + if (yamlNode is null) return default; + + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + using var reader = new StreamReader(stream); + + new YamlStream(new YamlDocument(yamlNode)).Save(writer); + writer.Flush(); + stream.Position = 0; + + return new Deserializer().Deserialize(reader); + } +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Diagnostics/PluginDiagnosticsData.cs b/src/EventStore.Plugins/Diagnostics/PluginDiagnosticsData.cs new file mode 100644 index 0000000..4f625ca --- /dev/null +++ b/src/EventStore.Plugins/Diagnostics/PluginDiagnosticsData.cs @@ -0,0 +1,17 @@ +namespace EventStore.Plugins.Diagnostics; + +/// +/// Represents diagnostic data of a plugin. +/// +/// The source of the event that matches the DiagnosticsName +/// The name of the event. The default is PluginDiagnosticsData. +/// The data associated with the event in the form of a dictionary. +/// When the event occurred. +/// Whether the event is a snapshot and should override previously collected data, by event name. Default value is true. +public readonly record struct PluginDiagnosticsData( + string Source, + string EventName, + Dictionary Data, + DateTimeOffset Timestamp, + bool IsSnapshot = true +); \ No newline at end of file diff --git a/src/EventStore.Plugins/Diagnostics/PluginDiagnosticsDataCollector.cs b/src/EventStore.Plugins/Diagnostics/PluginDiagnosticsDataCollector.cs new file mode 100644 index 0000000..541bd22 --- /dev/null +++ b/src/EventStore.Plugins/Diagnostics/PluginDiagnosticsDataCollector.cs @@ -0,0 +1,111 @@ +using System.Collections.Concurrent; +using System.Diagnostics; + +namespace EventStore.Plugins.Diagnostics; + +/// +/// A delegate to handle events. +/// +public delegate void OnEventCollected(PluginDiagnosticsData diagnosticsData); + +/// +/// Component to collect diagnostics data from plugins. More specifically events. +/// +[PublicAPI] +public class PluginDiagnosticsDataCollector : IObserver, IObserver>, IDisposable { + /// + /// Creates a new instance of . + /// + /// + /// A delegate to handle events. + /// + /// + /// The plugin diagnostic names to collect diagnostics data from. + /// + public PluginDiagnosticsDataCollector(OnEventCollected onEventCollected, params string[] sources) { + OnEventCollected = onEventCollected; + Sources = [..sources]; + + if (sources.Length > 0) + DiagnosticListener.AllListeners.Subscribe(this); + } + + /// + /// Creates a new instance of . + /// + /// + /// The plugin diagnostic names to collect diagnostics data from. + /// + public PluginDiagnosticsDataCollector(params string[] sources) : this(static _ => { }, sources) { } + + ConcurrentDictionary CollectedEventsByPlugin { get; } = new(); + OnEventCollected OnEventCollected { get; } + List Sources { get; } + List Subscriptions { get; } = []; + + /// + /// The collected events. + /// + public ICollection CollectedEvents => CollectedEventsByPlugin.Values.ToArray(); + + void IObserver.OnNext(DiagnosticListener value) { + // if (Sources.Contains(value.Name) && value.IsEnabled(value.Name)) + if (Sources.Contains(value.Name)) + Subscriptions.Add(value.Subscribe(this)); + } + + void IObserver>.OnNext(KeyValuePair value) { + if (value.Key != nameof(PluginDiagnosticsData) || value.Value is not PluginDiagnosticsData pluginEvent) return; + + CollectedEventsByPlugin.AddOrUpdate( + pluginEvent.Source, + static (_, pluginEvent) => pluginEvent, + static (_, _, pluginEvent) => pluginEvent, + pluginEvent + ); + + try { + OnEventCollected(pluginEvent); + } + catch (Exception) { + // stay on target + } + } + + void IObserver.OnCompleted() { } + + void IObserver>.OnCompleted() { } + + void IObserver.OnError(Exception error) { } + + void IObserver>.OnError(Exception error) { } + + /// + public void Dispose() { + foreach (var subscription in Subscriptions) + subscription.Dispose(); + } + + /// + /// Starts the with the specified delegate and sources. + /// This method is a convenient way to create a new instance of the and start collecting data immediately. + /// + /// + /// A delegate to handle events. + /// + /// + /// The plugin diagnostic names to collect diagnostics data from. + /// + public static PluginDiagnosticsDataCollector Start(OnEventCollected onEventCollected, params string[] sources) => + new(onEventCollected, sources); + + /// + /// Starts the with the specified sources. + /// This method is a convenient way to create a new instance of the and start collecting data immediately. + /// + /// + /// The plugin diagnostic names to collect diagnostics data from. + /// + public static PluginDiagnosticsDataCollector Start(params string[] sources) => + new(sources); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/EventStore.Plugins.csproj b/src/EventStore.Plugins/EventStore.Plugins.csproj index 5fe3836..67a6634 100644 --- a/src/EventStore.Plugins/EventStore.Plugins.csproj +++ b/src/EventStore.Plugins/EventStore.Plugins.csproj @@ -1,13 +1,14 @@ - - net8.0 + net8.0 + latest true - AnyCPU - disable - enable true + enable + enable + true + EventStore.Plugins Event Store Ltd @@ -20,16 +21,28 @@ Copyright 2012-2024 Event Store Ltd eventstore plugins + - - - + + + + + + + + + + + + + + diff --git a/src/EventStore.Plugins/IPlugableComponent.cs b/src/EventStore.Plugins/IPlugableComponent.cs index 14ea1f8..c27f16f 100644 --- a/src/EventStore.Plugins/IPlugableComponent.cs +++ b/src/EventStore.Plugins/IPlugableComponent.cs @@ -1,15 +1,50 @@ -using System.Text.Json.Nodes; -using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace EventStore.Plugins; -// Component that can be plugged in to the main server. -// Plugins are libraries that result in IPlugableComponents being produced and plugged in. +/// +/// Component that can be plugged into the main server. +/// public interface IPlugableComponent { - IApplicationBuilder Configure(IApplicationBuilder builder) => builder; - IServiceCollection ConfigureServices(IServiceCollection services, IConfiguration configuration) => services; - void CollectTelemetry(Action reply) { } -} + /// + /// The name of the component. + /// + string Name { get; } + + /// + /// The name used for diagnostics. + /// + string DiagnosticsName { get; } + + /// + /// The tags used for diagnostics. + /// + KeyValuePair[] DiagnosticsTags { get; } + + /// + /// The version of the component. + /// + string Version { get; } + + /// + /// Indicates whether the component is enabled. + /// + bool Enabled { get; } + + /// + /// Configures the services using the provided IServiceCollection and IConfiguration. + /// + /// The IServiceCollection to use for configuration. + /// The IConfiguration to use for configuration. + /// The configured IServiceCollection. + IServiceCollection ConfigureServices(IServiceCollection services, IConfiguration configuration); + + /// + /// Configures the application using the provided WebHostBuilderContext and IApplicationBuilder. + /// + /// The IApplicationBuilder to use for configuration. + /// The configured IApplicationBuilder. + IApplicationBuilder Configure(IApplicationBuilder builder); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/InflectorExtensions.cs b/src/EventStore.Plugins/InflectorExtensions.cs new file mode 100644 index 0000000..91e0031 --- /dev/null +++ b/src/EventStore.Plugins/InflectorExtensions.cs @@ -0,0 +1,45 @@ +//The Inflector extensions were partially cloned from Inflector (https://github.com/srkirkland/Inflector) + +//The MIT License (MIT) + +//Copyright (c) 2013 Scott Kirkland + +//Permission is hereby granted, free of charge, to any person obtaining a copy of +//this software and associated documentation files (the "Software"), to deal in +//the Software without restriction, including without limitation the rights to +//use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +//the Software, and to permit persons to whom the Software is furnished to do so, +//subject to the following conditions: + +//The above copyright notice and this permission notice shall be included in all +//copies or substantial portions of the Software. + +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +//FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +//COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +//IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +//CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Text.RegularExpressions; + +namespace EventStore.Plugins; + +static class InflectorExtensions { + /// + /// Separates the input words with underscore + /// + public static string Underscore(this string input) => + Regex.Replace(Regex.Replace(Regex.Replace(input, @"([\p{Lu}]+)([\p{Lu}][\p{Ll}])", "$1_$2"), @"([\p{Ll}\d])([\p{Lu}])", "$1_$2"), @"[-\s]", "_") + .ToLowerInvariant(); + + /// + /// Replaces underscores with dashes in the string + /// + public static string Dasherize(this string underscoredWord) => underscoredWord.Replace('_', '-'); + + /// + /// Separates the input words with hyphens and all the words are converted to lowercase + /// + public static string Kebaberize(this string input) => Underscore(input).Dasherize(); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Licensing/License.cs b/src/EventStore.Plugins/Licensing/License.cs index e4d6683..fbef7cf 100644 --- a/src/EventStore.Plugins/Licensing/License.cs +++ b/src/EventStore.Plugins/Licensing/License.cs @@ -1,9 +1,7 @@ using System.Security.Cryptography; -using System.Threading.Tasks; -using System; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; -using System.Collections.Generic; +using static System.Convert; namespace EventStore.Plugins.Licensing; @@ -13,50 +11,59 @@ public async Task IsValidAsync(string publicKey) { return result.IsValid; } + public bool IsValid(string publicKey) => + IsValidAsync(publicKey).GetAwaiter().GetResult(); + public static async Task CreateAsync( string publicKey, string privateKey, IDictionary claims) { - using var rsa = RSA.Create(); - rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateKey), out _); + rsa.ImportRSAPrivateKey(FromBase64String(privateKey), out _); var tokenHandler = new JsonWebTokenHandler(); var token = tokenHandler.CreateToken(new SecurityTokenDescriptor { Audience = "esdb", Issuer = "esdb", Expires = DateTime.UtcNow + TimeSpan.FromHours(1), Claims = claims, - SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256), + SigningCredentials = new(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256) }); var result = await ValidateTokenAsync(publicKey, token); if (!result.IsValid) - throw new Exception("Token could not be validated"); + throw new("Token could not be validated"); if (result.SecurityToken is not JsonWebToken jwt) - throw new Exception("Token is not a JWT"); + throw new("Token is not a JWT"); - return new License(jwt); + return new(jwt); } - private static async Task ValidateTokenAsync(string publicKey, string token) { + public static License Create(string publicKey, string privateKey, IDictionary? claims = null) => + CreateAsync(publicKey, privateKey, claims ?? new Dictionary()).GetAwaiter().GetResult(); + + public static License Create(byte[] publicKey, byte[] privateKey, IDictionary? claims = null) => + CreateAsync(ToBase64String(publicKey), ToBase64String(privateKey), claims ?? new Dictionary()).GetAwaiter().GetResult(); + + static async Task ValidateTokenAsync(string publicKey, string token) { // not very satisfactory https://github.com/dotnet/runtime/issues/43087 CryptoProviderFactory.Default.CacheSignatureProviders = false; using var rsa = RSA.Create(); - rsa.ImportRSAPublicKey(Convert.FromBase64String(publicKey), out _); + rsa.ImportRSAPublicKey(FromBase64String(publicKey), out _); var result = await new JsonWebTokenHandler().ValidateTokenAsync( token, - new TokenValidationParameters { + new() { ValidIssuer = "esdb", ValidAudience = "esdb", IssuerSigningKey = new RsaSecurityKey(rsa), ValidateAudience = true, ValidateIssuerSigningKey = true, ValidateIssuer = true, - ValidateLifetime = true, + ValidateLifetime = true }); + return result; } -} +} \ No newline at end of file diff --git a/src/EventStore.Plugins/MD5/IMD5Plugin.cs b/src/EventStore.Plugins/MD5/IMD5Plugin.cs index c82f9b5..955d1c3 100644 --- a/src/EventStore.Plugins/MD5/IMD5Plugin.cs +++ b/src/EventStore.Plugins/MD5/IMD5Plugin.cs @@ -9,4 +9,4 @@ public interface IMD5Plugin { /// Creates an MD5 provider factory for the MD5 plugin /// IMD5ProviderFactory GetMD5ProviderFactory(); -} +} \ No newline at end of file diff --git a/src/EventStore.Plugins/MD5/IMD5Provider.cs b/src/EventStore.Plugins/MD5/IMD5Provider.cs index 810f788..507d6ca 100644 --- a/src/EventStore.Plugins/MD5/IMD5Provider.cs +++ b/src/EventStore.Plugins/MD5/IMD5Provider.cs @@ -7,4 +7,4 @@ public interface IMD5Provider : IPlugableComponent { /// Creates an instance of the MD5 hash algorithm implementation /// HashAlgorithm Create(); -} +} \ No newline at end of file diff --git a/src/EventStore.Plugins/MD5/IMD5ProviderFactory.cs b/src/EventStore.Plugins/MD5/IMD5ProviderFactory.cs index 0b0a36b..1919355 100644 --- a/src/EventStore.Plugins/MD5/IMD5ProviderFactory.cs +++ b/src/EventStore.Plugins/MD5/IMD5ProviderFactory.cs @@ -5,4 +5,4 @@ public interface IMD5ProviderFactory { /// Builds an MD5 provider for the MD5 plugin /// IMD5Provider Build(); -} +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Plugin.cs b/src/EventStore.Plugins/Plugin.cs new file mode 100644 index 0000000..f3dab70 --- /dev/null +++ b/src/EventStore.Plugins/Plugin.cs @@ -0,0 +1,146 @@ +using System.Diagnostics; +using EventStore.Plugins.Diagnostics; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using static System.StringComparison; +using License = EventStore.Plugins.Licensing.License; + +namespace EventStore.Plugins; + +public record PluginOptions { + public string? Name { get; init; } + public string? Version { get; init; } + public string? LicensePublicKey { get; init; } + public string? DiagnosticsName { get; init; } + public KeyValuePair[] DiagnosticsTags { get; init; } = []; +} + +[PublicAPI] +public abstract class Plugin : IPlugableComponent, IDisposable { + protected Plugin( + string? name = null, + string? version = null, + string? licensePublicKey = null, + string? diagnosticsName = null, + params KeyValuePair[] diagnosticsTags) { + var pluginType = GetType(); + + Name = name ?? pluginType.Name + .Replace("Plugin", "", OrdinalIgnoreCase) + .Replace("Component", "", OrdinalIgnoreCase) + .Replace("Subsystems", "", OrdinalIgnoreCase) + .Replace("Subsystem", "", OrdinalIgnoreCase); + + Version = version + ?? pluginType.Assembly.GetName().Version?.ToString() + ?? "1.0.0.0-preview"; + + LicensePublicKey = licensePublicKey; + + DiagnosticsName = diagnosticsName ?? Name; + DiagnosticsTags = diagnosticsTags; + + DiagnosticListener = new(DiagnosticsName); + + IsEnabledResult = (false, ""); + Configuration = null!; + } + + protected Plugin(PluginOptions options) : this( + options.Name, + options.Version, + options.LicensePublicKey, + options.DiagnosticsName, + options.DiagnosticsTags) { } + + string? LicensePublicKey { get; } + + DiagnosticListener DiagnosticListener { get; } + + (bool Enabled, string EnableInstructions) IsEnabledResult { get; set; } + + IConfiguration Configuration { get; set; } + + /// + public string Name { get; } + + /// + public string Version { get; } + + /// + public string DiagnosticsName { get; } + + /// + public KeyValuePair[] DiagnosticsTags { get; } + + /// + public bool Enabled => IsEnabledResult.Enabled; + + protected virtual void ConfigureServices(IServiceCollection services, IConfiguration configuration) { } + + protected virtual void ConfigureApplication(IApplicationBuilder app, IConfiguration configuration) { } + + protected virtual (bool Enabled, string EnableInstructions) IsEnabled(IConfiguration configuration) => (true, ""); + + IServiceCollection IPlugableComponent.ConfigureServices(IServiceCollection services, IConfiguration configuration) { + Configuration = configuration; + IsEnabledResult = IsEnabled(configuration); + + if (Enabled) + ConfigureServices(services, configuration); + + return services; + } + + IApplicationBuilder IPlugableComponent.Configure(IApplicationBuilder app) { + PublishDiagnostics(new() { ["enabled"] = Enabled }); + + var logger = app.ApplicationServices.GetRequiredService().CreateLogger(GetType()); + + var license = app.ApplicationServices.GetService(); + if (Enabled && LicensePublicKey is not null && (license is null || !license.IsValid(LicensePublicKey))) + throw new PluginLicenseException(Name); + + if (!Enabled) { + logger.LogInformation( + "{Version} plugin disabled. {EnableInstructions}", + Version, IsEnabledResult.EnableInstructions + ); + + return app; + } + + logger.LogInformation("{Version} plugin enabled.", Version); + + ConfigureApplication(app, Configuration); + + PublishDiagnostics(new() { ["enabled"] = Enabled }); + + return app; + } + + protected internal void PublishDiagnostics(string eventName, Dictionary eventData) { + DiagnosticListener.Write( + nameof(PluginDiagnosticsData), + new PluginDiagnosticsData( + DiagnosticsName, + eventName, + eventData, + DateTimeOffset.UtcNow + ) + ); + } + + protected internal void PublishDiagnostics(Dictionary eventData) => + PublishDiagnostics(nameof(PluginDiagnosticsData), eventData); + + protected internal void PublishDiagnosticsEvent(T pluginEvent) => + DiagnosticListener.Write(typeof(T).Name, pluginEvent); + + /// + public void Dispose() { + DiagnosticListener.Dispose(); + } +} \ No newline at end of file diff --git a/src/EventStore.Plugins/PluginLicenseException.cs b/src/EventStore.Plugins/PluginLicenseException.cs new file mode 100644 index 0000000..b59b40e --- /dev/null +++ b/src/EventStore.Plugins/PluginLicenseException.cs @@ -0,0 +1,8 @@ +namespace EventStore.Plugins; + +public class PluginLicenseException(string pluginName) : Exception( + $"A license is required to use the {pluginName} plugin, but was not found. " + + "Please obtain a license or disable the plugin." +) { + public string PluginName { get; } = pluginName; +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Subsystems/ISubsystem.cs b/src/EventStore.Plugins/Subsystems/ISubsystem.cs index 8790ffa..06d405f 100644 --- a/src/EventStore.Plugins/Subsystems/ISubsystem.cs +++ b/src/EventStore.Plugins/Subsystems/ISubsystem.cs @@ -1,9 +1,6 @@ -using System.Threading.Tasks; - -namespace EventStore.Plugins.Subsystems; +namespace EventStore.Plugins.Subsystems; public interface ISubsystem : IPlugableComponent { - string Name { get; } Task Start(); Task Stop(); -} +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Subsystems/ISubsystemsPlugin.cs b/src/EventStore.Plugins/Subsystems/ISubsystemsPlugin.cs index a77a20c..022536f 100644 --- a/src/EventStore.Plugins/Subsystems/ISubsystemsPlugin.cs +++ b/src/EventStore.Plugins/Subsystems/ISubsystemsPlugin.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +namespace EventStore.Plugins.Subsystems; -namespace EventStore.Plugins.Subsystems; - -// A plugin that can create multiple subsystems. +/// +/// A plugin that can create multiple subsystems. +/// public interface ISubsystemsPlugin { string Name { get; } string Version { get; } string CommandLineName { get; } + IReadOnlyList GetSubsystems(); -} +} \ No newline at end of file diff --git a/src/EventStore.Plugins/SubsystemsPlugin.cs b/src/EventStore.Plugins/SubsystemsPlugin.cs new file mode 100644 index 0000000..1fdf2c5 --- /dev/null +++ b/src/EventStore.Plugins/SubsystemsPlugin.cs @@ -0,0 +1,43 @@ +using EventStore.Plugins.Subsystems; +using static System.StringComparison; + +namespace EventStore.Plugins; + +public record SubsystemsPluginOptions : PluginOptions { + public string? CommandLineName { get; init; } +} + +[PublicAPI] +public abstract class SubsystemsPlugin : Plugin, ISubsystem, ISubsystemsPlugin { + protected SubsystemsPlugin(SubsystemsPluginOptions options) : base(options) { + CommandLineName = options.CommandLineName ?? options.Name ?? GetType().Name + .Replace("Plugin", "", OrdinalIgnoreCase) + .Replace("Component", "", OrdinalIgnoreCase) + .Replace("Subsystems", "", OrdinalIgnoreCase) + .Replace("Subsystem", "", OrdinalIgnoreCase) + .Kebaberize(); + } + + protected SubsystemsPlugin( + string? name = null, string? version = null, + string? licensePublicKey = null, + string? commandLineName = null, + string? diagnosticsName = null, + params KeyValuePair[] diagnosticsTags + ) : this(new() { + Name = name, + Version = version, + LicensePublicKey = licensePublicKey, + DiagnosticsName = diagnosticsName, + DiagnosticsTags = diagnosticsTags, + CommandLineName = commandLineName + }) { } + + public string CommandLineName { get; } + + public virtual Task Start() => Task.CompletedTask; + + public virtual Task Stop() => Task.CompletedTask; + + public virtual IReadOnlyList GetSubsystems() => [this]; +} \ No newline at end of file diff --git a/test/EventStore.Plugins.Tests/ConfigurationReaderTests/LdapsSettings.cs b/test/EventStore.Plugins.Tests/ConfigurationReaderTests/LdapsSettings.cs new file mode 100644 index 0000000..d2489cc --- /dev/null +++ b/test/EventStore.Plugins.Tests/ConfigurationReaderTests/LdapsSettings.cs @@ -0,0 +1,24 @@ +namespace EventStore.Plugins.Tests.ConfigurationReaderTests; + +public class LdapsSettings { + public string Host { get; set; } = null!; + public int Port { get; set; } = 636; + public bool ValidateServerCertificate { get; set; } + public bool UseSSL { get; set; } = true; + + public bool AnonymousBind { get; set; } + public string BindUser { get; set; } = null!; + public string BindPassword { get; set; } = null!; + + public string BaseDn { get; set; } = null!; + public string ObjectClass { get; set; } = "organizationalPerson"; + public string Filter { get; set; } = "sAMAccountName"; + public string GroupMembershipAttribute { get; set; } = "memberOf"; + + public bool RequireGroupMembership { get; set; } + public string RequiredGroupDn { get; set; } = null!; + + public int PrincipalCacheDurationSec { get; set; } = 60; + + public Dictionary LdapGroupRoles { get; set; } = null!; +} \ No newline at end of file diff --git a/src/EventStore.Plugins.Tests/ConfigurationReaderTests/valid_node_config.yaml b/test/EventStore.Plugins.Tests/ConfigurationReaderTests/valid_node_config.yaml similarity index 100% rename from src/EventStore.Plugins.Tests/ConfigurationReaderTests/valid_node_config.yaml rename to test/EventStore.Plugins.Tests/ConfigurationReaderTests/valid_node_config.yaml diff --git a/test/EventStore.Plugins.Tests/ConfigurationReaderTests/when_reading_valid_configuration.cs b/test/EventStore.Plugins.Tests/ConfigurationReaderTests/when_reading_valid_configuration.cs new file mode 100644 index 0000000..b31e60d --- /dev/null +++ b/test/EventStore.Plugins.Tests/ConfigurationReaderTests/when_reading_valid_configuration.cs @@ -0,0 +1,30 @@ +namespace EventStore.Plugins.Tests.ConfigurationReaderTests; + +public class when_reading_valid_configuration { + [Fact] + public void should_return_correct_options() { + var settings = ConfigParser.ReadConfiguration( + Path.Combine("ConfigurationReaderTests", "valid_node_config.yaml"), + "LdapsAuth" + ); + + settings!.Host.Should().Be("13.64.104.29"); + settings.Port.Should().Be(389); + settings.ValidateServerCertificate.Should().BeFalse(); + settings.UseSSL.Should().BeFalse(); + settings.AnonymousBind.Should().BeFalse(); + settings.BindUser.Should().Be("mycompany\\binder"); + settings.BindPassword.Should().Be("p@ssw0rd!"); + settings.BaseDn.Should().Be("ou=Lab,dc=mycompany,dc=local"); + settings.ObjectClass.Should().Be("organizationalPerson"); + settings.GroupMembershipAttribute.Should().Be("memberOf"); + settings.RequireGroupMembership.Should().BeFalse(); + settings.RequiredGroupDn.Should().Be("RequiredGroupDn"); + settings.PrincipalCacheDurationSec.Should().Be(120); + settings.LdapGroupRoles.Should().BeEquivalentTo(new Dictionary { + { "CN=ES-Accounting,CN=Users,DC=mycompany,DC=local", "accounting" }, + { "CN=ES-Operations,CN=Users,DC=mycompany,DC=local", "it" }, + { "CN=ES-Admins,CN=Users,DC=mycompany,DC=local", "$admins" } + }); + } +} \ No newline at end of file diff --git a/test/EventStore.Plugins.Tests/Diagnostics/PluginDiagnosticsDataCollectorTests.cs b/test/EventStore.Plugins.Tests/Diagnostics/PluginDiagnosticsDataCollectorTests.cs new file mode 100644 index 0000000..5af6565 --- /dev/null +++ b/test/EventStore.Plugins.Tests/Diagnostics/PluginDiagnosticsDataCollectorTests.cs @@ -0,0 +1,33 @@ +using EventStore.Plugins.Diagnostics; + +namespace EventStore.Plugins.Tests.Diagnostics; + +public class PluginDiagnosticsDataCollectorTests { + [Fact] + public void can_collect_diagnostics_data_from_plugin() { + using var plugin = new TestPlugin(pluginName: Guid.NewGuid().ToString()); + + using var sut = PluginDiagnosticsDataCollector.Start(plugin.DiagnosticsName); + + plugin.PublishDiagnostics(new() { ["enabled"] = plugin.Enabled }); + + sut.CollectedEvents.Should().ContainSingle().Which + .Data["enabled"].Should().Be(plugin.Enabled); + } + + [Fact] + public void can_collect_diagnostics_data_from_subsystems_plugin() { + using var plugin = new TestSubsystemsPlugin(pluginName: Guid.NewGuid().ToString()); + + using var sut = PluginDiagnosticsDataCollector.Start(plugin.DiagnosticsName); + + plugin.PublishDiagnostics(new() { ["enabled"] = plugin.Enabled }); + + sut.CollectedEvents.Should().ContainSingle().Which + .Data["enabled"].Should().Be(plugin.Enabled); + } + + class TestPlugin(string? pluginName = null) : Plugin(pluginName); + + class TestSubsystemsPlugin(string? pluginName = null) : SubsystemsPlugin(pluginName); +} \ No newline at end of file diff --git a/test/EventStore.Plugins.Tests/Diagnostics/PluginMetricsTests.cs b/test/EventStore.Plugins.Tests/Diagnostics/PluginMetricsTests.cs new file mode 100644 index 0000000..b3c7288 --- /dev/null +++ b/test/EventStore.Plugins.Tests/Diagnostics/PluginMetricsTests.cs @@ -0,0 +1,56 @@ +using System.Diagnostics.Metrics; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; + +namespace EventStore.Plugins.Tests.Diagnostics; + +public class PluginMetricsTests { + [Fact] + public void can_receive_metrics_from_plugin() { + // Arrange + IPlugableComponent plugin = new AdamSmasherPlugin(diagnosticsTags: new KeyValuePair("test_name", "can_receive_metrics_from_plugin")); + + var builder = WebApplication.CreateBuilder(); + + plugin.ConfigureServices(builder.Services, builder.Configuration); + + using var app = builder.Build(); + + plugin.Configure(app); + + using var collector = new MetricCollector( + app.Services.GetRequiredService(), + plugin.DiagnosticsName, + ((AdamSmasherPlugin)plugin).TestCounter.Name + ); + + // Act + ((AdamSmasherPlugin)plugin).TestCounter.Add(1, plugin.DiagnosticsTags); // we also need to add then here? ffs... they should propagate from the meter... + + // Assert + var collectedMeasurement = collector.GetMeasurementSnapshot().Should().ContainSingle().Which; + + collectedMeasurement.Value.Should().Be(1); + + collectedMeasurement.Tags.Should().BeEquivalentTo(plugin.DiagnosticsTags); + } + + class AdamSmasherPlugin(params KeyValuePair[] diagnosticsTags) : Plugin(diagnosticsTags: diagnosticsTags) { + public Counter TestCounter { get; private set; } = null!; + + protected override void ConfigureApplication(IApplicationBuilder app, IConfiguration configuration) { + var meterFactory = app.ApplicationServices.GetRequiredService(); + + var meter = meterFactory.Create(DiagnosticsName, Version, DiagnosticsTags); + + TestCounter = meter.CreateCounter( + "plugin_test_calls", + "int", + "just to test the counter", + DiagnosticsTags + ); + } + } +} \ No newline at end of file diff --git a/test/EventStore.Plugins.Tests/EventStore.Plugins.Tests.csproj b/test/EventStore.Plugins.Tests/EventStore.Plugins.Tests.csproj new file mode 100644 index 0000000..581637b --- /dev/null +++ b/test/EventStore.Plugins.Tests/EventStore.Plugins.Tests.csproj @@ -0,0 +1,45 @@ + + + net8.0 + latest + true + enable + enable + true + false + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + Always + + + + + + + + + + + + diff --git a/src/EventStore.Plugins.Tests/Licensing/LicenseTests.cs b/test/EventStore.Plugins.Tests/Licensing/LicenseTests.cs similarity index 51% rename from src/EventStore.Plugins.Tests/Licensing/LicenseTests.cs rename to test/EventStore.Plugins.Tests/Licensing/LicenseTests.cs index c06b782..34321f8 100644 --- a/src/EventStore.Plugins.Tests/Licensing/LicenseTests.cs +++ b/test/EventStore.Plugins.Tests/Licensing/LicenseTests.cs @@ -1,16 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Threading.Tasks; +using System.Security.Cryptography; using EventStore.Plugins.Licensing; -using Xunit; namespace EventStore.Plugins.Tests.Licensing; public class LicenseTests { - public static (string PublicKey, string PrivateKey) CreateKeyPair() { - using var rsa = RSA.Create(512); + static (string PublicKey, string PrivateKey) CreateKeyPair() { + using var rsa = RSA.Create(1024); // was failing with 512?!? var publicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()); var privateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey()); return (publicKey, privateKey); @@ -20,16 +15,16 @@ public static (string PublicKey, string PrivateKey) CreateKeyPair() { public async Task can_create_and_validate_license() { var (publicKey, privateKey) = CreateKeyPair(); - var license = await License.CreateAsync(publicKey, privateKey, new Dictionary() { - { "foo", "bar"}, + var license = await License.CreateAsync(publicKey, privateKey, new Dictionary { + { "foo", "bar" } }); // check repeatedly because of https://github.com/dotnet/runtime/issues/43087 - Assert.True(await license.IsValidAsync(publicKey)); - Assert.True(await license.IsValidAsync(publicKey)); - Assert.True(await license.IsValidAsync(publicKey)); + (await license.IsValidAsync(publicKey)).Should().BeTrue(); + (await license.IsValidAsync(publicKey)).Should().BeTrue(); + (await license.IsValidAsync(publicKey)).Should().BeTrue(); - Assert.Equal("bar", license.Token.Claims.First(c => c.Type == "foo").Value); + license.Token.Claims.First(c => c.Type == "foo").Value.Should().Be("bar"); } [Fact] @@ -37,11 +32,11 @@ public async Task detects_incorrect_public_key() { var (publicKey, privateKey) = CreateKeyPair(); var (publicKey2, _) = CreateKeyPair(); - var license = await License.CreateAsync(publicKey, privateKey, new Dictionary() { - { "foo", "bar"}, + var license = await License.CreateAsync(publicKey, privateKey, new Dictionary { + { "foo", "bar" } }); - Assert.False(await license.IsValidAsync(publicKey2)); + (await license.IsValidAsync(publicKey2)).Should().BeFalse(); } [Fact] @@ -49,11 +44,10 @@ public async Task cannot_create_with_inconsistent_keys() { var (publicKey, _) = CreateKeyPair(); var (_, privateKey) = CreateKeyPair(); - var ex = await Assert.ThrowsAsync(() => - License.CreateAsync(publicKey, privateKey, new Dictionary() { - { "foo", "bar"}, - })); + Func act = () => License.CreateAsync(publicKey, privateKey, new Dictionary { + { "foo", "bar" } + }); - Assert.Equal("Token could not be validated", ex.Message); + await act.Should().ThrowAsync().WithMessage("Token could not be validated"); } -} +} \ No newline at end of file diff --git a/test/EventStore.Plugins.Tests/PluginBaseTests.cs b/test/EventStore.Plugins.Tests/PluginBaseTests.cs new file mode 100644 index 0000000..375d179 --- /dev/null +++ b/test/EventStore.Plugins.Tests/PluginBaseTests.cs @@ -0,0 +1,147 @@ +// ReSharper disable AccessToDisposedClosure + +using System.Security.Cryptography; +using EventStore.Plugins.Licensing; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using static System.Convert; + +namespace EventStore.Plugins.Tests; + +public class PluginBaseTests { + [Fact] + public void plugin_base_sets_defaults_automatically() { + var expectedOptions = new PluginOptions { + Name = "NightCity", + Version = "1.0.0.0", + DiagnosticsName = "NightCity" + }; + + using var plugin = new NightCityPlugin(); + + plugin.Options.Should().BeEquivalentTo(expectedOptions); + } + + [Fact] + public void subsystems_plugin_base_sets_defaults_automatically() { + var expectedOptions = new SubsystemsPluginOptions { + Name = "PhantomLiberty", + Version = "1.0.0.0", + DiagnosticsName = "PhantomLiberty", + CommandLineName = "phantom-liberty" + }; + + using var plugin = new PhantomLibertySubsystemsPlugin(); + + plugin.Options.Should().BeEquivalentTo(expectedOptions); + } + + [Fact] + public void comercial_plugin_is_disabled_when_licence_is_missing() { + // Arrange + IPlugableComponent plugin = new NightCityPlugin(new() { + LicensePublicKey = "valid-public-key" + }); + + var builder = WebApplication.CreateBuilder(); + + plugin.ConfigureServices(builder.Services, EmptyConfiguration); + + using var app = builder.Build(); + + Action configure = () => plugin.Configure(app); + + // Act & Assert + configure.Should().Throw().Which + .PluginName.Should().Be(plugin.Name); + } + + [Fact] + public void comercial_plugin_is_disabled_when_licence_is_invalid() { + // Arrange + var (license, _) = CreateLicense(); + var (_, invalidPublicKey) = CreateLicense(); + + IPlugableComponent plugin = new NightCityPlugin(new() { + LicensePublicKey = invalidPublicKey + }); + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddSingleton(license); + + plugin.ConfigureServices(builder.Services, EmptyConfiguration); + + using var app = builder.Build(); + + Action configure = () => plugin.Configure(app); + + // Act & Assert + configure.Should().Throw().Which + .PluginName.Should().Be(plugin.Name); + } + + [Fact] + public void comercial_plugin_is_enabled_when_licence_is_present() { + // Arrange + var (license, publicKey) = CreateLicense(); + + IPlugableComponent plugin = new NightCityPlugin(new() { + LicensePublicKey = publicKey + }); + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddSingleton(license); + + plugin.ConfigureServices(builder.Services, builder.Configuration); + + using var app = builder.Build(); + + Action configure = () => plugin.Configure(app); + + // Act & Assert + configure.Should().NotThrow(); + } + + static (License License, string PublicKey) CreateLicense(Dictionary? claims = null) { + using var rsa = RSA.Create(1024); + + var publicKey = ToBase64String(rsa.ExportRSAPublicKey()); + var privateKey = ToBase64String(rsa.ExportRSAPrivateKey()); + + return (License.Create(publicKey, privateKey, claims), publicKey); + } + + static readonly IConfiguration EmptyConfiguration = new ConfigurationBuilder().AddInMemoryCollection().Build(); + + class NightCityPlugin : Plugin { + public NightCityPlugin(PluginOptions options) : base(options) { + Options = options with { + Name = Name, + Version = Version, + DiagnosticsName = DiagnosticsName + }; + } + + public NightCityPlugin() : this(new()) { } + + public PluginOptions Options { get; } + } + + class PhantomLibertySubsystemsPlugin : SubsystemsPlugin { + public PhantomLibertySubsystemsPlugin(SubsystemsPluginOptions options) : base(options) { + Options = options with { + Name = Name, + Version = Version, + DiagnosticsName = DiagnosticsName, + CommandLineName = CommandLineName + }; + } + + public PhantomLibertySubsystemsPlugin() : this(new()) { } + + public SubsystemsPluginOptions Options { get; } + } +} \ No newline at end of file