diff --git a/.gitignore b/.gitignore index 3585f2b73ecf1c9ddc9c46a21d91f18dbe79b937..8a5e7f13989c72b4b86131b33e8fe02c662b85ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ *private.env.json +**/*.secret.json +**/encryptionKeys +**/assets/attachment.pdf +**/**notes.md + +private_notes/ ### VisualStudioCode template .vscode/* @@ -10,6 +16,10 @@ # Local History for Visual Studio Code .history/ +.DS_Store + +**.nupkg + ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider @@ -476,3 +486,4 @@ Network Trash Folder Temporary Items .apdisk +deploy.sh diff --git a/.idea/.idea.FitConnect/.idea/codeStyles/codeStyleConfig.xml b/.idea/.idea.FitConnect/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000000000000000000000000000000000000..a55e7a179bde3e4e772c29c0c85e53354aa54618 --- /dev/null +++ b/.idea/.idea.FitConnect/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ +<component name="ProjectCodeStyleConfiguration"> + <state> + <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> + </state> +</component> \ No newline at end of file diff --git a/BasicUnitTest/BasicUnitTest.csproj b/BasicUnitTest/BasicUnitTest.csproj new file mode 100644 index 0000000000000000000000000000000000000000..87a4ad558fb5cab8be7c9ea8ad23e2cdac2ea72e --- /dev/null +++ b/BasicUnitTest/BasicUnitTest.csproj @@ -0,0 +1,27 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <Nullable>enable</Nullable> + + <IsPackable>false</IsPackable> + + <RootNamespace>FluentApiTest</RootNamespace> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="FluentAssertions" Version="6.7.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> + <PackageReference Include="Moq" Version="4.18.1" /> + <PackageReference Include="NUnit" Version="3.13.2" /> + <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> + <PackageReference Include="coverlet.collector" Version="3.1.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> + <ProjectReference Include="..\MockContainer\MockContainer.csproj" /> + </ItemGroup> + +</Project> diff --git a/BasicUnitTest/FluentSenderTests.cs b/BasicUnitTest/FluentSenderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..0951d3b8279130ae2069fe2e2fcee6c7a3ec1c16 --- /dev/null +++ b/BasicUnitTest/FluentSenderTests.cs @@ -0,0 +1,100 @@ +using System; +using Autofac; +using FitConnect; +using FitConnect.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using MockContainer; +using NUnit.Framework; + +namespace FluentApiTest; + +public class FluentSenderTests { + private IContainer _container = null!; + protected string clientId = null!; + protected string clientSecret = null!; + private ILogger? logger; + private string leikaKey = null!; + + [OneTimeSetUp] + public void OneTimeSetup() { + _container = Container.Create(); + logger = _container.Resolve<ILogger>(); + var settings = _container.Resolve<MockSettings>(); + clientId = settings.SenderClientId; + clientSecret = settings.SenderClientSecret; + leikaKey = settings.LeikaKey; + } + + [SetUp] + public void Setup() { + } + + [Test] + public void FluentSender_ShouldNotThrowAnError_ArgumentExceptionLeikaKeyInvalid() { + Assert.Throws<ArgumentException>(() => { + new Sender(FitConnectEnvironment.Testing, + clientId, clientSecret, + Container.Create()) + .WithDestination(Guid.NewGuid().ToString()) + .WithServiceType("", "") + .WithAttachments(Array.Empty<Attachment>()) + .WithData("") + .Submit(); + })!.Message.Should().Be("Invalid leika key"); + } + + [Test] + public void FluentSender_ShouldNotThrowAnError_MockingServicesWithData() { + new Sender(FitConnectEnvironment.Testing, + clientId, clientSecret, + Container.Create()) + .WithDestination(Guid.NewGuid().ToString()) + .WithServiceType("", leikaKey) + .WithAttachments(Array.Empty<Attachment>()) + .WithData("") + .Submit(); + } + + [Test] + public void FluentSender_ShouldNotThrowAnError_MockingServicesWithoutData() { + new Sender(FitConnectEnvironment.Testing, + clientId, clientSecret, + Container.Create()) + .WithDestination(Guid.NewGuid().ToString()) + .WithServiceType("", leikaKey) + .WithAttachments(Array.Empty<Attachment>()) + .Submit(); + } + + [Test] + public void VerifyMetaData_ValidData_Fine() { + // Arrange + var submission = new Submission(); + submission.ServiceType.Identifier = leikaKey; + + // Act + var metadata = Sender.CreateMetadata(submission); + + // Assert + var errors = Subscriber.VerifyMetadata(metadata); + foreach (var error in errors) Console.WriteLine(error.ToString()); + + errors.Count.Should().Be(0); + } + + [Test] + public void VerifyMetaData_MissingLeikaKey_ThorwsAnError() { + // Arrange + var submission = new Submission(); + + // Act + var metadata = Sender.CreateMetadata(submission); + + // Assert + var errors = Subscriber.VerifyMetadata(metadata); + foreach (var error in errors) Console.WriteLine(error.ToString()); + + errors.Count.Should().Be(1); + } +} diff --git a/BasicUnitTest/FluentSubscriberReceiveTests.cs b/BasicUnitTest/FluentSubscriberReceiveTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..6ab27366f830c539631570796c1088c38bc33373 --- /dev/null +++ b/BasicUnitTest/FluentSubscriberReceiveTests.cs @@ -0,0 +1,41 @@ +using Autofac; +using FitConnect; +using FitConnect.Models; +using Microsoft.Extensions.Logging; +using MockContainer; +using NUnit.Framework; + +namespace FluentApiTest; + +public class FluentSubscriberReceiveTests { + private readonly string clientId = "clientId"; + private readonly string clientSecret = "clientSecret"; + private IContainer _container = null!; + private ILogger _logger = null!; + private MockSettings _mockSettings = null!; + + [OneTimeSetUp] + public void OneTimeSetup() { + _container = Container.Create(); + _logger = _container.Resolve<ILogger>(); + _mockSettings = _container.Resolve<MockSettings>(); + } + + [SetUp] + public void SetUp() { + } + + [Test] + [Ignore("Missing encrypted data")] + public void GetSubmission() { + var fluentSubscriber = + new Subscriber(FitConnectEnvironment.Testing, "", "", "", "", "", "", logger: null); + + var submissions = fluentSubscriber + .GetAvailableSubmissions(); + + var attachments = fluentSubscriber + .RequestSubmission("submissionId") + .GetAttachments(); + } +} diff --git a/BasicUnitTest/SecurityEventTokenTests.cs b/BasicUnitTest/SecurityEventTokenTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..a2f752acaf07d45c9442e0f8e04e5e4e60b5e1f7 --- /dev/null +++ b/BasicUnitTest/SecurityEventTokenTests.cs @@ -0,0 +1,149 @@ +using System; +using Autofac; +using FitConnect.Encryption; +using FitConnect.Models; +using FitConnect.Models.v1.Api; +using FluentAssertions; +using NUnit.Framework; +using SecurityEventToken = FitConnect.Models.SecurityEventToken; + +namespace FluentApiTest; + +[TestFixture] +public class SecurityEventTokenTests { + private const string rejectSubmission = + SecurityEventToken.RejectSubmissionSchema; + + private FitEncryption _encryption = null!; + + private const string acceptSubmission = + SecurityEventToken.AcceptSubmissionSchema; + + [SetUp] + public void Setup() { + var container = MockContainer.Container.Create(); + _encryption = new FitEncryption(container.Resolve<KeySet>(), null); + } + + [Test] + public void CreateJwt_AcceptSubmission() { + var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + acceptSubmission, + null + ); + Console.WriteLine(token); + var decoded = new FitConnect.Models.SecurityEventToken(token); + decoded.Event?.Type.Should() + .Be(acceptSubmission); + decoded.EventType.Should().Be(EventType.Accept); + } + + [Test] + public void CreateJwt_Reject_WithEncryptionIssue() { + var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + rejectSubmission, + new[] { Problems.EncryptionIssue } + ); + Console.WriteLine(token); + var decoded = new FitConnect.Models.SecurityEventToken(token); + decoded.Event?.Type.Should() + .Be(rejectSubmission); + decoded.EventType.Should().Be(EventType.Reject); + } + + [Test] + public void CreateJwt_Reject_WithMissingSchema() { + var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + rejectSubmission, + new[] { Problems.MissingSchema } + ); + Console.WriteLine(token); + var decoded = new FitConnect.Models.SecurityEventToken(token); + decoded.Event?.Type.Should() + .Be(rejectSubmission); + + decoded.EventType.Should().Be(EventType.Reject); + } + + [Test] + public void CreateJwt_Reject_WithSchemaViolation() { + var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + rejectSubmission, + new[] { Problems.SchemaViolation } + ); + Console.WriteLine(token); + var decoded = new FitConnect.Models.SecurityEventToken(token); + decoded.Event?.Type.Should() + .Be(rejectSubmission); + decoded.EventType.Should().Be(EventType.Reject); + } + + [Test] + public void CreateJwt_Reject_WithSyntaxViolation() { + var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + rejectSubmission, + new[] { Problems.SyntaxViolation } + ); + Console.WriteLine(token); + + var decoded = new FitConnect.Models.SecurityEventToken(token); + decoded.Event?.Type.Should() + .Be(rejectSubmission); + decoded.EventType.Should().Be(EventType.Reject); + } + + [Test] + public void CreateJwt_Reject_WithUnsupportedSchema() { + var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + rejectSubmission, + new[] { Problems.UnsupportedSchema } + ); + Console.WriteLine(token); + var decoded = new FitConnect.Models.SecurityEventToken(token); + decoded.Event?.Type.Should() + .Be(rejectSubmission); + decoded.EventType.Should().Be(EventType.Reject); + } + + [Test] + public void CreateJwt_Reject_WithIncorrectAuthenticationTag() { + var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + rejectSubmission, + new[] { Problems.IncorrectAuthenticationTag } + ); + Console.WriteLine(token); + var decoded = new FitConnect.Models.SecurityEventToken(token); + decoded.Event?.Type.Should() + .Be(rejectSubmission); + decoded.EventType.Should().Be(EventType.Reject); + } + + [Test] + public void CreateJwt_Reject_WithCustomProblem() { + var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + rejectSubmission, + new[] { new Problems() { Description = "A real big issue" } } + ); + Console.WriteLine(token); + var decoded = new FitConnect.Models.SecurityEventToken(token); + decoded.Event?.Type.Should() + .Be(rejectSubmission); + decoded.EventType.Should().Be(EventType.Reject); + } +} diff --git a/DummyClient/.dockerignore b/DemoRunner/.dockerignore similarity index 100% rename from DummyClient/.dockerignore rename to DemoRunner/.dockerignore diff --git a/DemoRunner/.gitignore b/DemoRunner/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..16bf3adb5b4003c3a628bdea461c41238d3d1b74 --- /dev/null +++ b/DemoRunner/.gitignore @@ -0,0 +1 @@ +appsettings.json diff --git a/DemoRunner/DemoRunner.csproj b/DemoRunner/DemoRunner.csproj new file mode 100644 index 0000000000000000000000000000000000000000..1e670320d5da6e923c6e02273e721b38128fafd4 --- /dev/null +++ b/DemoRunner/DemoRunner.csproj @@ -0,0 +1,48 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net6.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="FitConnect" Version="0.0.1-beta.2"/> + <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0"/> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0"/> + </ItemGroup> + + <ItemGroup> + <None Update="appsettings.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="Test.pdf"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="encryptionKeys\set-public-keys.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="encryptionKeys\privateKey_signing.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="encryptionKeys\publicKey_encryption.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="encryptionKeys\privateKey_decryption.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="encryptionKeys\publicKey_signature_verification.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\FitConnect\FitConnect.csproj"/> + </ItemGroup> + +</Project> diff --git a/DemoRunner/Dockerfile b/DemoRunner/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..69fa8ac7f6fb0b815f0201eda443c5bae1dc0fcb --- /dev/null +++ b/DemoRunner/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["DemoRunner/DemoRunner.csproj", "DemoRunner/"] +RUN dotnet restore "DemoRunner/DemoRunner.csproj" +COPY . . +WORKDIR "/src/DemoRunner" +RUN dotnet build "DemoRunner.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "DemoRunner.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "DemoRunner.dll"] diff --git a/DemoRunner/OutputHelper.cs b/DemoRunner/OutputHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..99de311f3bbd552e1567e15883f025288e8efda8 --- /dev/null +++ b/DemoRunner/OutputHelper.cs @@ -0,0 +1,38 @@ +namespace DemoRunner; + +public static class OutputHelper { + #region Headline Output + +// @formatter:off +public static void PrintFitConnect() { + Console.WriteLine(@" ______ ____ ______ ______ __ "); + Console.WriteLine(@" / ____// _//_ __/ / ____/____ ____ ____ ___ _____ / /_ "); + Console.WriteLine(@" / /_ / / / /______ / / / __ \ / __ \ / __ \ / _ \ / ___// __/ "); + Console.WriteLine(@" / __/ _/ / / //_____// /___ / /_/ // / / // / / // __// /__ / /_ "); + Console.WriteLine(@"/_/ /___/ /_/ \____/ \____//_/ /_//_/ /_/ \___/ \___/ \__/ "); + Console.WriteLine(@""); + Console.WriteLine(@""); + Console.WriteLine("Demo client for the .NET SDK"); + Console.WriteLine(@""); + +} + +public static void PrintSender() { + Console.WriteLine(@" _____ __"); + Console.WriteLine(@" / ___/ ___ ____ ____/ /___ _____"); + Console.WriteLine(@" \__ \ / _ \ / __ \ / __ // _ \ / ___/"); + Console.WriteLine(@" ___/ // __// / / // /_/ // __// / "); + Console.WriteLine(@" /____/ \___//_/ /_/ \__,_/ \___//_/ "); +} + +public static void PrintSubscriber() { + Console.WriteLine(@" _____ __ _ __"); + Console.WriteLine(@" / ___/ __ __ / /_ _____ _____ _____ (_)/ /_ ___ _____"); + Console.WriteLine(@" \__ \ / / / // __ \ / ___// ___// ___// // __ \ / _ \ / ___/"); + Console.WriteLine(@" ___/ // /_/ // /_/ /(__ )/ /__ / / / // /_/ // __// / "); + Console.WriteLine(@" /____/ \__,_//_.___//____/ \___//_/ /_//_.___/ \___//_/ "); +} +// @formatter:on + + #endregion +} diff --git a/DemoRunner/Program.cs b/DemoRunner/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..ad63d7bab9cd3511626c31906ff554bb8aa1b65d --- /dev/null +++ b/DemoRunner/Program.cs @@ -0,0 +1,28 @@ +// See https://aka.ms/new-console-template for more information + +using DemoRunner; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +OutputHelper.PrintFitConnect(); + +#region Preparation + +// Load appsettings.json and create a configuration object +var config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + +// Create a new instance of the logger. +var logger = LoggerFactory.Create( + builder => { + builder.AddSimpleConsole(); + builder.SetMinimumLevel(LogLevel.Trace); + }).CreateLogger("FIT-Connect"); + +#endregion + +logger.LogInformation("Starting FIT-Connect"); + +SenderDemo.Run(config, logger); +SubscriberDemo.Run(config, logger); diff --git a/DemoRunner/SenderDemo.cs b/DemoRunner/SenderDemo.cs new file mode 100644 index 0000000000000000000000000000000000000000..e1735afffd8a09137a66448648db979c766ba405 --- /dev/null +++ b/DemoRunner/SenderDemo.cs @@ -0,0 +1,26 @@ +using FitConnect; +using FitConnect.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace DemoRunner; + +public static class SenderDemo { + public static void Run(IConfigurationRoot config, ILogger logger) { + var clientId = config["FitConnect:Sender:ClientId"]; + var clientSecret = config["FitConnect:Sender:ClientSecret"]; + var destinationId = config["FitConnect:Sender:DestinationId"]; + var leikaKey = config["FitConnect:Sender:LeikaKey"]; + + + OutputHelper.PrintSender(); + var submission = Client + .GetSender(FitConnectEnvironment.Testing, clientId, clientSecret, logger) + .WithDestination(destinationId) + .WithServiceType("FIT Connect Demo", leikaKey) + .WithAttachments(new[] { new Attachment("Test.pdf", "Test Attachment") }) + .WithData("{\"message\":\"Hello World\"}") + .Submit(); + + } +} diff --git a/DemoRunner/SubscriberDemo.cs b/DemoRunner/SubscriberDemo.cs new file mode 100644 index 0000000000000000000000000000000000000000..16047cf3e0a8c34eee0900eedecdf8899580ecd6 --- /dev/null +++ b/DemoRunner/SubscriberDemo.cs @@ -0,0 +1,43 @@ +using FitConnect; +using FitConnect.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace DemoRunner; + +public static class SubscriberDemo { + public static void Run(IConfiguration config, ILogger logger) { + var clientId = config["FitConnect:Subscriber:ClientId"]; + var clientSecret = config["FitConnect:Subscriber:ClientSecret"]; + var destinationId = config["FitConnect:Subscriber:DestinationId"]; + var privateKeyDecryption = config["FitConnect:Subscriber:PrivateKeyDecryption"]; + var privateKeySigning = config["FitConnect:Subscriber:PrivateKeySigning"]; + var publicKeyEncryption = config["FitConnect:Subscriber:PublicKeyEncryption"]; + var publicKeySignatureVerification = + config["FitConnect:Subscriber:PublicKeySignatureVerification"]; + var setPublicKey = config["FitConnect:Subscriber:SetPublicKey"]; + + + OutputHelper.PrintSubscriber(); + + var subscriber = Client.GetSubscriber(FitConnectEnvironment.Testing, clientId, + clientSecret, + File.ReadAllText(privateKeyDecryption), + File.ReadAllText(privateKeySigning), + File.ReadAllText(publicKeyEncryption), + File.ReadAllText(publicKeySignatureVerification), + logger); + + var submissions = subscriber.GetAvailableSubmissions(); + + foreach (var submission in submissions) { + var subscriberWithSubmission = subscriber.RequestSubmission(submission.SubmissionId); + var attachments = subscriberWithSubmission + .GetAttachments(); + + logger.LogInformation("Fachdaten: {Data}", subscriberWithSubmission.GetDataJson()); + subscriberWithSubmission + .AcceptSubmission(); + } + } +} diff --git a/DemoRunner/Test.pdf b/DemoRunner/Test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..50a038977b732f8c86889b164166410d5d60e0de Binary files /dev/null and b/DemoRunner/Test.pdf differ diff --git a/DemoRunner/appsettings.json.template b/DemoRunner/appsettings.json.template new file mode 100644 index 0000000000000000000000000000000000000000..22d1b2e054b134a606deab61659be4b4a24f0f9c --- /dev/null +++ b/DemoRunner/appsettings.json.template @@ -0,0 +1,21 @@ +{ + "FitConnect": { + "Sender": { + "ClientId": "00000000-0000-0000-0000-000000000000", + "ClientSecret": "", + "DestinationId": "00000000-0000-0000-0000-000000000000", + "LeikaKey": "urn:de:fim:leika:leistung:99400048079000" + }, + "Subscriber": { + "ClientId": "00000000-0000-0000-0000-000000000000", + "ClientSecret": "", + "DestinationId": "00000000-0000-0000-0000-000000000000", + "LeikaKey": "urn:de:fim:leika:leistung:00000000000000", + "PrivateKeyDecryption": "./encryptionKeys/privateKey_decryption.json", + "PrivateKeySigning": "./encryptionKeys/privateKey_signing.json", + "PublicKeyEncryption": "./encryptionKeys/publicKey_encryption.json", + "PublicKeySignatureVerification": "./encryptionKeys/publicKey_signature_verification.json", + "SetPublicKey": "./encryptionKeys/set-public-keys.json" + } + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0e4d7c5684d013f5b4dff1b0301cd7a4bf5df961 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /test + +COPY . . +RUN dotnet test EncryptionTests/EncryptionTests.csproj + +RUN dotnet build DummyClient/DummyClient.csproj -c Release -o /test/build + +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base +COPY --from=build /test/build . + +CMD ["dotnet", "DummyClient.dll"] \ No newline at end of file diff --git a/Documentation/documentation.de-DE.md b/Documentation/documentation.de-DE.md new file mode 100644 index 0000000000000000000000000000000000000000..24fb1aab9576d4eff7efe71eb64cb18695d72a76 --- /dev/null +++ b/Documentation/documentation.de-DE.md @@ -0,0 +1,93 @@ +# FIT-Connect .NET SDK User Guide + +## Einleitung + +Das FIT-Connect .NET SDK bietet eine einfache Möglichkeit, sowohl einen Antragsteller (Sender) als auch einen Antragsempfänger (Subscriber) an FIT-Connect anzubinden. + +## Voraussetzungen + +### OSX + +Auf OSX wird das SDK nur dann unterstützt, wenn OpenSSL auf dem System installiert ist. + +```sh +brew install openssl@1.1 +``` + +Die Environment-Variable ```DYLD_LIBRARY_PATH``` muss auf den Pfad zu OpenSSL verweisen. + +_Beispiele:_ + +```sh +export DYLD_LIBRARY_PATH=/usr/local/opt/openssl/lib +export DYLD_LIBRARY_PATH=/usr/local/opt/openssl@1.1/lib +``` + +### Sender + +Um einen Antrag mit dem SDK versenden zu können, werden eine ClientID und ein ClientSecret benötigt. +Diese können im FIT-Connect Self-Service-Portal erzeugt werden. + +[Offizelle Dokumentation von FIT-Connect zum Versenden von Einreichungen (Anträgen)](https://docs.fitko.de/fit-connect/docs/sending/overview) + +### Subscriber + +Der Subscriber benötigt sowohl eine ClientID und das ClientSecret, aber auch die Schlüsselpaare zur +Verschlüsselung wie auch zum Signieren der Daten. +Zu Testzwecken können selbstgenerierte Schlüsselpaare mit dem [hierzu bereitgestellten Tool](https://docs.fitko.de/fit-connect/docs/details/jwk-creation) erzeugt werden. + +In der Produktivumgebung müssen hierzu [Zertifikate der Verwaltungs-PKI zu Einsatz kommen](https://docs.fitko.de/fit-connect/docs/receiving/certificate). + +[Offizielle Dokumentation von FIT-Connect zum Abrufen von Einreichungen](https://docs.fitko.de/fit-connect/docs/receiving/overview) + +# Beispiele + +## Sender + +```csharp +var submission = Client + .GetSender(FitConnectEnvironment.Development, clientId, clientSecret, logger) + .WithDestination(destinationId) + .WithServiceType("FIT Connect Demo", leikaKey) + .WithAttachments(new Attachment("Test.pdf", "Test Attachment")) + .WithData("{\"message\":\"Hello World\"}") + .Submit(); +``` + +## Subscriber + +### Erstellen des Subscribers + +```csharp +var subscriber = Client.GetSubscriber(FitConnectEnvironment.Development, clientId, + clientSecret, + privateKeyDecryption, + privateKeySigning, + publicKeyEncryption, + publicKeySignatureVerification, + logger); +``` + +### Abrufen der Submissions + +```csharp +var submissions = subscriber.GetAvailableSubmissions(); +``` + +### Abrufen der Submissions mit den Anhängen + +```csharp +foreach (var submission in submissions) { + var subscriberWithSubmission = subscriber.RequestSubmission(submission.SubmissionId); + var data = subscriber.GetDataJson(); + var attachments = subscriberWithSubmission + .GetAttachments(); + // Submission accept + subscriberWithSubmission + .AcceptSubmission(); + // or submission reject + subscriberWithSubmission + .RejectSubmission(); + +} +``` diff --git a/Documentation/documentation.en-EN.md b/Documentation/documentation.en-EN.md new file mode 100644 index 0000000000000000000000000000000000000000..f5eaea6d26fdaffa64af48e4ce784a2a4e5e2075 --- /dev/null +++ b/Documentation/documentation.en-EN.md @@ -0,0 +1,82 @@ +# FIT-Connect .NET SDK User Guide + +## Introduction + +The SDK supports a high-level support as well as a low-level support for the +FIT-Connect API and a fluent API. + +## All in one + +For not having to deal with the low-level API, the SDK provides a high-level support. + +### Sender + +```mermaid +flowchart TD + client([Create a new FitConnect client]) + send.submission[Create new Submission] + send.send[Send the submission] + send.return[\Return the result/] + + client-->send.submission-->send.send-->send.return + + + subscribe.request[Request submissions] + + client-->subscribe.request-->ToBeDefined +``` + +Simplified call: + +```csharp +bool SendSubmission(Submission submission){ + var client = new Client(...); + return client.SendSubmissionAsync(submission); +} +``` + +### Subscriber + +### Example + +Visit [All in one](./all_in_one.md) to see the code. + +## Fluent Api + +### Sender + +```mermaid +stateDiagram + [*]-->Authenticate + Authenticate-->CreateSubmission + CreateSubmission-->UploadAttachments + UploadAttachments-->SendSubmission + SendSubmission-->[*] +``` + +### Subscriber + +```mermaid +stateDiagram + [*]-->Authenticate + Authenticate-->GetSubmissions + GetSubmissions-->[*] + + Authenticate-->GetSubmission + GetSubmission-->GetAttachments + GetAttachments-->DecryptAttachments + DecryptAttachments-->DecryptData + DecryptData-->DecryptMetadata + DecryptMetadata-->[*] +``` + +#### Receiving list of submissions + +#### Receiving specific submission + +[Fluent Api Example](./fluent_api.md) + +## Detailed calls + +[Detailed calls](./detailed_calls.md) + diff --git a/DummyClient/Dockerfile b/DummyClient/Dockerfile deleted file mode 100644 index 2e2e405808c74fafa716b3126ca341d547c3dbfb..0000000000000000000000000000000000000000 --- a/DummyClient/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build -WORKDIR /src -COPY ["DummyClient/DummyClient.csproj", "DummyClient/"] -RUN dotnet restore "DummyClient/DummyClient.csproj" -COPY . . -WORKDIR "/src/DummyClient" -RUN dotnet build "DummyClient.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "DummyClient.csproj" -c Release -o /app/publish - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "DummyClient.dll"] diff --git a/DummyClient/DummyClient.csproj b/DummyClient/DummyClient.csproj deleted file mode 100644 index e449bbc26417523e8a0a2dc0ac0878d7f2f50811..0000000000000000000000000000000000000000 --- a/DummyClient/DummyClient.csproj +++ /dev/null @@ -1,15 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net6.0</TargetFramework> - <ImplicitUsings>enable</ImplicitUsings> - <Nullable>enable</Nullable> - <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> - </PropertyGroup> - - <ItemGroup> - <ProjectReference Include="..\FitConnect\FitConnect.csproj"/> - </ItemGroup> - -</Project> diff --git a/DummyClient/Program.cs b/DummyClient/Program.cs deleted file mode 100644 index b5d49cb0fc4ae21f7144eeac2beedb70b2a60202..0000000000000000000000000000000000000000 --- a/DummyClient/Program.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using FitConnect; -using FitConnect.Models; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -Client client; - - -void FluentSenderCall() { - client.Sender - .WithDestination("destinationId") - .WithAttachments(Array.Empty<Attachment>()) - .WithData(@"{""data"":""content""}") - .Submit(); - - client.Sender - .WithDestination("destinationId") - .WithAttachments(Array.Empty<Attachment>()) - .Submit(); -} - -void FluentSubscriberCall() { - var submissions = client.Subscriber - .GetAvailableSubmissions("destinationId"); - - client.Subscriber.RequestSubmission("submissionId") - .GetAttachments((attachments => { - // Check if the attachments are valid - return true; - })); -} - - -ILogger logger = new Logger<AppDomain>(new NullLoggerFactory()); -client = new Client( - FitConnectEnvironments.Create(FitConnectEnvironments.EndpointType.Development), - "clientId", "clientSecret", - logger); - -Console.WriteLine( - "This is a dummy client to demonstrate the usage of the FitConnect SDK for .NET"); - -FluentSenderCall(); -FluentSubscriberCall(); diff --git a/E2ETest/E2ETest.csproj b/E2ETest/E2ETest.csproj new file mode 100644 index 0000000000000000000000000000000000000000..9b68a3a077dc80a2ef5cfae445e642b7cf745aa3 --- /dev/null +++ b/E2ETest/E2ETest.csproj @@ -0,0 +1,24 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> + <PackageReference Include="NUnit" Version="3.13.3" /> + <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" /> + <PackageReference Include="NUnit.Analyzers" Version="3.3.0" /> + <PackageReference Include="coverlet.collector" Version="3.1.2" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> + <ProjectReference Include="..\MockContainer\MockContainer.csproj" /> + </ItemGroup> + +</Project> diff --git a/E2ETest/EndToEndTestBase.cs b/E2ETest/EndToEndTestBase.cs new file mode 100644 index 0000000000000000000000000000000000000000..d6a15f6d892d30fc7d46edd13943ded703cf7cc9 --- /dev/null +++ b/E2ETest/EndToEndTestBase.cs @@ -0,0 +1,39 @@ +using Autofac; +using FitConnect; +using FitConnect.Interfaces.Sender; +using FitConnect.Interfaces.Subscriber; +using FitConnect.Models; +using Microsoft.Extensions.Logging; +using MockContainer; + +namespace E2ETest; + +public abstract class EndToEndTestBase { + private ISender _sender; + private ISubscriber _subscriber; + + [OneTimeSetUp] + public void Setup() { + var container = MockContainer.Container.Create(); + var settings = container.Resolve<MockSettings>(); + + var logger = LoggerFactory.Create( + builder => { + builder.AddSimpleConsole(); + builder.SetMinimumLevel(LogLevel.Trace); + }).CreateLogger("E2E Test"); + + + _sender = Client.GetSender(FitConnectEnvironment.Testing, + settings.SenderClientId, settings.SenderClientSecret, + logger); + _subscriber = Client.GetSubscriber(FitConnectEnvironment.Testing, + settings.SenderClientId, settings.SenderClientSecret, + settings.PrivateKeyDecryption, + settings.PrivateKeySigning, + settings.PublicKeyEncryption, + settings.PublicKeySignatureVerification, + logger + ); + } +} diff --git a/E2ETest/RejectSubmissionTest.cs b/E2ETest/RejectSubmissionTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..7279f8a56631301dba4a6028163c5218fac6fe87 --- /dev/null +++ b/E2ETest/RejectSubmissionTest.cs @@ -0,0 +1,6 @@ +using FitConnect.Encryption; + +namespace E2ETest; + +public class RejectSubmissionTest : EndToEndTestBase { +} diff --git a/E2ETest/StraightForwardTest.cs b/E2ETest/StraightForwardTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..f7b1e3312263ed59927209fb201b57564af5b97d --- /dev/null +++ b/E2ETest/StraightForwardTest.cs @@ -0,0 +1,9 @@ +namespace E2ETest; + +public class StraightForwardTest : EndToEndTestBase { + [Order(10)] + [Test] + public void Test1() { + Assert.Pass(); + } +} diff --git a/E2ETest/Usings.cs b/E2ETest/Usings.cs new file mode 100644 index 0000000000000000000000000000000000000000..324456763afb873e116da92178ad9018cb9e7e50 --- /dev/null +++ b/E2ETest/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; diff --git a/EncryptionTests/Certificates/certificate.cer b/EncryptionTests/Certificates/certificate.cer new file mode 100644 index 0000000000000000000000000000000000000000..73f0387ee1b1ff8bdc9fe430d24b328c0e860b85 --- /dev/null +++ b/EncryptionTests/Certificates/certificate.cer @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEyjCCArKgAwIBAgIIMIw7HOs7698wDQYJKoZIhvcNAQELBQAwJTEWMBQGA1UEAxMNZml0Y29u +bmVjdC5kZTELMAkGA1UEBhMCREUwHhcNMjIwNjA4MTEwNzQ3WhcNMjcwNjA4MTEwNzUyWjAlMRYw +FAYDVQQDEw1maXRjb25uZWN0LmRlMQswCQYDVQQGEwJERTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAJxxbIieIEY9Vvql4R6ED5HqtGMjoJZ2xiwF5Zv/DuLseAZEUR7QuBvPG8CLcYOE +JArXcAS5smLfG2LiNerMgDnrLGvBl3qrYOEq0kRIqGkSr5JM2V/bk2UgMToOJGQDBMh6IbU707Xp +u1gHe2wuNxbJE4kALZV8zJX9fE2bc87o3URsw4yv3EnCpR3TskPI2uazNxKWVWjgpZ5yB6PQMbG0 +R7e0qGdEeIo5PpaxdkzbJUN4LwnEdZCKbXt3bMqr7wUJ4lXrqCUhO1Y57iR5YoNBTkKgJHV9fbcy +EHs6IZAhxQCCyuczby5UDS4VzXvnqpxxnvI/rv12nc2WPPctvmA3AyHOH7hTugg7UZ75UKFFHqfU +TeY6lWkifUpTpWVZ3Krm0+3Ja/VZzufPSybifTR9v5JMxQdwaqp9E53Cqh1csqciLVazDOjU6Lhn +fjLr4GwSri1FveXDqpq/jJhkTbuDL1fAEs+eDauD3vNxb0Bverpb5/7rOcAKTH0GEzZhSHV23fyP +DpKmpi5h0py42SdbBAZat8C1StMQ7m3UxuDdoMg+zRS5d12d6s0ZFf2xwFCjDVNVN6y7BkSGuA2l +2/I5ojXhK/Wm+DAcKT/osZznyNq4OlHL2ck6LOSSgFCJFrdkpku++gROx2xjUIFQjryUkcPZ6y0l +PLXAHJUEGO97AgMBAAEwDQYJKoZIhvcNAQELBQADggIBADXJ6LP4HdnUiFCgQR8PoY+fR8XCnYx8 +FdVB9v9xdsBhE1/B1c8z6kUInQdq2NfsRjyfgA1wIdPElP+b1bu/TKS/GpWrsPHUXDRv67qT13cB +rw8I6rHTDdnF00pEU4mabI32EqGnEAwu8DGlKnDbnh/dY7IQjYK40/ZutcqzybzvBiNFoYu7rKXg +7CThPwc92fnPfFcDJJMU9YlA5C6MaLSxDj0e3z1rl+ew9pab8gclnbzgxGOqFMNPhNRHx0SvFrCF +dPzva9mYDATHgR66hJAo/hov8qJnz6/7xkQgFPy4jHwBk2ubM2SN8+UEyklm7u3v8X6mpvdE7/gC +qWJYJjcpzz7QEBmvJ9yYIALNo8E9UyZNfYgE/hEyBpEccNk7z6y/yHNThcojwbfmYjztl5Ed4uHY +i1ycf5LYcSAxtzD6V5CuIVzmPNkSgB1m8Wu/+5Sy2/uQGIKNK/X6E9f1GjTDBv5KzxOoongc4yri +VinYgmrpoF6Uh6G423IGT6/+SpyQa5oegpSbKKDcSAQKK/fCJQbck03WTbpgKDhc0KDLB2C07yiI +RNgSDRO+eg8BTFjxE3uPVwY8AP+QxK2+Xn5ozuT7aedeS41MQYeeatd6StNy809DvIMb44ZUA6JU +/n8Ok8eHWVY+aShfomcLyH7+EeL9e2LTSp0geUdmsnNI +-----END CERTIFICATE----- \ No newline at end of file diff --git a/EncryptionTests/Certificates/certificate.pfx b/EncryptionTests/Certificates/certificate.pfx new file mode 100644 index 0000000000000000000000000000000000000000..ec4092f2c68e0c8865fe9333be770dd09fb5c6f9 Binary files /dev/null and b/EncryptionTests/Certificates/certificate.pfx differ diff --git a/EncryptionTests/EncryptionTests.csproj b/EncryptionTests/EncryptionTests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..86c48ff27f44f56c75667c55b9446ac99c37e00a --- /dev/null +++ b/EncryptionTests/EncryptionTests.csproj @@ -0,0 +1,40 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <Nullable>enable</Nullable> + + <IsPackable>false</IsPackable> + + <RootNamespace>SenderTest</RootNamespace> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="FluentAssertions" Version="6.7.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0"/> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0"/> + <PackageReference Include="NUnit" Version="3.13.2"/> + <PackageReference Include="NUnit3TestAdapter" Version="4.0.0"/> + <PackageReference Include="coverlet.collector" Version="3.1.0"/> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\FitConnect\FitConnect.csproj"/> + <ProjectReference Include="..\MockContainer\MockContainer.csproj"/> + </ItemGroup> + + <ItemGroup> + <Folder Include="Certificates"/> + </ItemGroup> + + <ItemGroup> + <None Update="assets\attachment.pdf"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="Test.pdf"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + </ItemGroup> + +</Project> diff --git a/EncryptionTests/FileEncryptionTest.cs b/EncryptionTests/FileEncryptionTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..457630b02e541c551ac5f81a6a34c3133a7543ca --- /dev/null +++ b/EncryptionTests/FileEncryptionTest.cs @@ -0,0 +1,46 @@ +using System.IO; +using System.Security.Cryptography; +using Autofac; +using FitConnect.Encryption; +using FitConnect.Models; +using FluentAssertions; +using MockContainer; +using NUnit.Framework; + +namespace SenderTest; + +public class FileEncryptionTest { + private string _encryptedFile; + private FitEncryption _encryption; + private byte[] sourceFile = null!; + + [SetUp] + public void Setup() { + sourceFile = RandomNumberGenerator.GetBytes(4096); + var container = Container.Create(); + var keySet = container.Resolve<KeySet>(); + _encryption = new FitEncryption(keySet, null); + } + + [Test] + [Order(10)] + public void EncryptFile() { + _encryptedFile = _encryption.Encrypt(sourceFile); + } + + [Test] + public void TestSha512FileHash() { + // Arrange + var content = File.ReadAllBytes("Test.pdf"); + + + // Act + var attachment = new Attachment("Test.pdf", "Just an attachment"); + + var hash = attachment.Hash; + // Assert + hash.Should() + .Be( + "8b1042900c2039f65fe6c4cb1bca31e2a7a04b61d3ca7d9ae9fc4077068b82ad5512fa298385b025db70551113b762064444b87737e45e657a71be5b88b06e59"); + } +} diff --git a/EncryptionTests/JweTest.cs b/EncryptionTests/JweTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..207a5689f55a99833542c8673b16220fc5b18dcc --- /dev/null +++ b/EncryptionTests/JweTest.cs @@ -0,0 +1,136 @@ +using System; +using System.Security.Cryptography; +using Autofac; +using FitConnect; +using FitConnect.Encryption; +using FitConnect.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using MockContainer; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace SenderTest; + +public class JweTest { + private IContainer _container; + private ILogger<JweTest> _logger; + private Sender _sender; + private MockSettings _settings; + + + [SetUp] + public void SetUp() { + _logger = LoggerFactory.Create(cfg => cfg.AddConsole()) + .CreateLogger<JweTest>(); + + _sender = new Sender( + FitConnectEnvironment.Testing, + "", "", + _logger); + + _container = Container.Create(); + _settings = _container.Resolve<MockSettings>(); + } + + + [Test] + public void TestJwe() { + // Arrange + var engine = new FitEncryption(_container.Resolve<KeySet>(), _logger) { + PublicKeyEncryption = _settings.PublicKeyEncryption, + PrivateKeyDecryption = _settings.PrivateKeyDecryption + }; + + // Act + var dummyText = JsonConvert.SerializeObject(new { Title = "Value", Content = "content" }); + Console.WriteLine(dummyText); + var cypher = engine.Encrypt(dummyText); + Console.WriteLine(cypher); + var plain = engine.Decrypt(cypher); + Console.WriteLine(plain.plainText); + + // Assert + plain.plainText.Should().Be(dummyText); + } + + [Test] + public void TestJwe_withBytes() { + // Arrange + var engine = new FitEncryption(_container.Resolve<KeySet>(), _logger) { + PublicKeyEncryption = _settings.PublicKeyEncryption, + PrivateKeyDecryption = _settings.PrivateKeyDecryption + }; + + // Act + var plainBytes = RandomNumberGenerator.GetBytes(8192); + var cypher = engine.Encrypt(plainBytes); + var plain = engine.Decrypt(cypher); + + + // Assert + plain.plainBytes.Should().BeEquivalentTo(plainBytes); + } + + [Test] + public void TestMetaData() { + // Arrange + var encryptionEngine = new FitEncryption(_container.Resolve<KeySet>(), _logger) { + PublicKeyEncryption = _settings.PublicKeyEncryption + }; + + var decryptEngine = new FitEncryption(_container.Resolve<KeySet>(), _logger) { + PrivateKeyDecryption = _settings.PrivateKeyDecryption + }; + + + var metaData = + "{\"attachments\":[{\"attachmentId\":\"5d055f43-4ad6-4202-822a-f946c3be29a6\",\"description\":\"Just a test\",\"filename\":\"RandomBytes\",\"hash\":{\"content\":\"8b1042900c2039f65fe6c4cb1bca31e2a7a04b61d3ca7d9ae9fc4077068b82ad5512fa298385b025db70551113b762064444b87737e45e657a71be5b88b06e59\",\"type\":0},\"mimeType\":\"application/pdf\",\"purpose\":0}],\"data\":{\"hash\":null,\"submissionSchema\":{\"mimeType\":0,\"schemaUri\":\"urn:de:fim:leika:leistung:99400048079000\"}}}"; + + // Act + var encrypted = encryptionEngine.Encrypt(metaData); + var plain = decryptEngine.Decrypt(encrypted); + + // Assert + plain.plainText.Should().Be(metaData); + } + + [Test] + public void TestJwe_withBytes2() { + // Arrange + var engine = new FitEncryption(_container.Resolve<KeySet>(), _logger) { + PublicKeyEncryption = _settings.PublicKeyEncryption, + PrivateKeyDecryption = _settings.PrivateKeyDecryption + }; + + // Act + var plainBytes = RandomNumberGenerator.GetBytes(512); + var cypher = engine.Encrypt(Convert.ToBase64String(plainBytes)); + var plain = engine.Decrypt(cypher); + + // Assert + plain.plainText.Should().BeEquivalentTo(Convert.ToBase64String(plainBytes)); + } + + [TestCase( + "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoidnI3Y1dLR2wtZzRXYV9DUkdvd05oQVlXX2dRYi1ha01iaWlneE4wRWtESSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJ6aXAiOiJERUYifQ.R5TmfiDyi7dZhYz9NxduhqZc6GrfAERrDVGNLts6eHhU2SVMTAja1O9Rywrf_thPvbjQslM6ukXB8iI3hI6oWN2EY9SqUjKRto3zL2jdmdcXzfwxJFm1BnObMjYl9Jwy2ogEwLldXQhLAwMxVJWJbf31mZCgnVYC6DbPTEi-GLIkLDEn7Tj5y9iak3TjaE0hIxMk546Dda-q6I9QQ7YlDGV8m0Ijjh21yu2_B9H3Uh9LgIyrTN1Jw2saJRKVJgrtoV6e8WRaj5sDXarLNz6R2LhEtIjYjVioZvjBt1EeXADjcR9m1j3qG8V1f9boVocRTivvvaRvWd2NTH5yFGunDXr40oUmXXWXK1SfKsty85AAjlLOv5ZJsU1vhquA2XDgDVDJ7Nm1qC_9VeW2yCD01Ewh9sGiKscMqP1CbLxyVPfbuViVIe4g0h7Krlb6mZe9L7Sk18cLyFuCre7nYfB5ZToKnSkd8C5-ExMHfrcp9MJ196X6_n7YFKrc-Lzg7tvCRBG-6DFIQ1iqnme6crMN15qwo9VddOJOFXV815vNVyFXLhJAvr85q78aqyaE90qgY2QDNXyZXziTbXLurISL8i_Mdzt0J7cUyrvQLZaK0_pMJJvZOg3LvNalTGdJjUHUFwPxTbe-DRWZuPT81KVNyNa8EKkqox1Ohm_56riMFyU.Od1O-XCSw2Xa8wBN.mHWq8eodcIDXKWjMDDqcNkunVLT0EvjvAWq28gc6aOfKN0zeT0SQczxl0jqcWlziHXE8KmDL7CDYhOZT4rv--lN6wDNj998.YjJfxro1W_ERT7jOvI06yA")] + [TestCase( + "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoidnI3Y1dLR2wtZzRXYV9DUkdvd05oQVlXX2dRYi1ha01iaWlneE4wRWtESSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJ6aXAiOiJERUYifQ.NWLSv3Yq3IK2oNVgbDoVpcJ-i3iunOOWQhoULQRa5Haf4g2E9dJOgq7jPqUl347jRB6pkyj09PL9b2R5PUai5Z6KraW6vCOQM4zeA_Z3SAjONO2XNNDQZpA2TvsxrEXqG_89ck8rMOHnkFJBUrQp4jnwFeIswQQTtErdbsTXTyYdkuJk9j18-yrZM0px9FkbB3u5Fqn8Nu5GGRAVGBw_u73QbNbfgmZINgn-2WHCAdqAW5U776XnGSYxf4qIUcwzA8JjFOUU-0l3KqgvMTsN-f0XcHEGDqvxOWCVsgvibfMZ8rnAowirX6z8tIWrVCvqE-5jOX9gXe-xXMYTAi-a0xU2f1ZrJGcfoMoUL8-kePytbU4JdudNgx_F60WGqVI3wnLAj7V_u0mFumsaR6QjrasInNS7Dqi_Rg3T9wZFpa4_dUOx1j6z_r5Z7_GVV4fypli_prWJeN_34iazEfshYw33QngNMKYjNSvGb9JxYabZyTrOJImJsK503Ad_3sTAi4AENsKAnesz1GUCxkz-hONKEX9QfG3rxqt0PPXmX6Dm5NdbyWdUBSWu47eSv0sSH2xoj0Oov0c_c8JzdvkV82eNQRwyqezjSCq6NwmHtIJL_j4b79tqHWhWauXo24xsYLIA8qNpLIH9UfXEJwG33KQlmiCK-k8IHFWB0-_BzVk.m5zCt2SccAkr4hXF.ROdp1qpWiU_wYDyEjtgZp9XFcqL-Jb7r7u7LPhdWJ7nSSIRJM_UYxBUFiF3XOFL22r_67ml8slap7ylN5TGRQxtbtm-Yq_T2y9Df33SozoFY-2pIZg9mzUxN0hbGil5REbmleYyRfY7NsMDkZjWDXlOoQeI7ZjmnXBEEdtOam2-ccOW94rJE-xy7HdwZMdPgakSz9-KPtDPJQMRyMi1C9wUZ_R1FND4sSmjlUmLgJ-dywdIRK35bQMu5WEjlo9JoWxu8Aw8C-vjF5zXXNSMsn3W2YC1T3O9eGBNcVdA1-R_IgkC8lJqL_uwKMQuk5tB2bSO3wdjweVxszouvy3TzHWot75f6rTWbw1NphtSN3G3YUDnBIbnFG0-kU7gLCf3fvmIsL8mf0vopAN83Cwx9hJA.IU6vJRAhYR30TizHP07neQ")] + [TestCase( + "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoidnI3Y1dLR2wtZzRXYV9DUkdvd05oQVlXX2dRYi1ha01iaWlneE4wRWtESSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJ6aXAiOiJERUYifQ.srb2oytHYxTqcaLZiGNQX4QJqBOiEPZAgO4sZJdBA-koWu6-wliPy7YyqYc8-bCMAbiwp9XOoR8wRKK7pnaIsU66k9zCp1FjVcjkE420URdwagzPqqKoxcyr7wNIcAWjA8iMfXB1KlxVHbt4nryspoGwSvLMj7CxBkzOcAUw9naNV0B-RjNrtdpt7scYyBVlCjl9Rj4Qy1W1BGviU5VHYkLJa7NXQ34amTyXBRoTtxhAP3HIu8DXTnbWhyToU25THWswSOy7rNaLOqfsXLp0ecfpoVKXXjDjMjhcCo7o6QJO1Jc6IBKdj7OGhIHCJOQAZ0jE9XchZXR9oxIDlG4qV-JKKhldRtfkJdmIZ4Vx-jhMweaqu2LQy0ZA3F3JrtvjKhTiJFQrg-JVaUQsY_zTKmTKnZis1KsjkKNwdKi_oc5Ug1DAH0_WE4IU_pe5bfRdYYMWl6m1wAOoXd3TiKp1vfpDVyeWfeg-gohiuK0lszoVfiiA97Y5wtk5NpNpzKNThQecHoyRhAj3_E5iRpztjnqHh0I0BSQJFMVqdBjsQU4zZjRhkxsU1jVH_004jVLNyAgJ9jpB1UQzahMsTJsHPuhqvRu8-tjwK1juuDpZXctUdbSzYO8pDTbHz6ysDazyEniGqQFKWj1AEg2chGm2uVtoeFkkAPEZTkdv2Gsr2sA.4re8HD8VxtnSEjc8.hXDdDN4X1M1M9eB-2Kf4WemzOXlSKFeEAR8L_3KBOgAprGDIZygOxSAyBH0sSISMHk7SvSYwtt3HXbmB1hM12ZbQRLbFknLOv6Pw8Lr8CvVheOl70SKMkocx0HHlYXYp3r4ZrTY3AKlHpX3ePoHoMWXStgOt5EWKG5ciW018sFHKVKQG87hMgSvDt4FoZAN4yyHsMn9Sf5DB3LhNyAXyx-4Oz8q9q-M-X-ckYEcnJfZl_G4SOQPBa-Ly7MDEeNQ9fRHozbdHmZ1TzLFF4xZbda12V15fL_JNWUzwuhUL589l-RR119FQlKTxv6Unes3o0gdSz7-l0hQI3Q2eKdf0Jj_CMw8f-yPO89YSgVxbK452YxHK8JWKMCcweNNHhMBjFUssFcBpuBXcHeekjVbcYA.ADUZnHvl9fM3OmLeaibVuA")] + [TestCase( + "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoidnI3Y1dLR2wtZzRXYV9DUkdvd05oQVlXX2dRYi1ha01iaWlneE4wRWtESSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJ6aXAiOiJERUYifQ.R5TmfiDyi7dZhYz9NxduhqZc6GrfAERrDVGNLts6eHhU2SVMTAja1O9Rywrf_thPvbjQslM6ukXB8iI3hI6oWN2EY9SqUjKRto3zL2jdmdcXzfwxJFm1BnObMjYl9Jwy2ogEwLldXQhLAwMxVJWJbf31mZCgnVYC6DbPTEi-GLIkLDEn7Tj5y9iak3TjaE0hIxMk546Dda-q6I9QQ7YlDGV8m0Ijjh21yu2_B9H3Uh9LgIyrTN1Jw2saJRKVJgrtoV6e8WRaj5sDXarLNz6R2LhEtIjYjVioZvjBt1EeXADjcR9m1j3qG8V1f9boVocRTivvvaRvWd2NTH5yFGunDXr40oUmXXWXK1SfKsty85AAjlLOv5ZJsU1vhquA2XDgDVDJ7Nm1qC_9VeW2yCD01Ewh9sGiKscMqP1CbLxyVPfbuViVIe4g0h7Krlb6mZe9L7Sk18cLyFuCre7nYfB5ZToKnSkd8C5-ExMHfrcp9MJ196X6_n7YFKrc-Lzg7tvCRBG-6DFIQ1iqnme6crMN15qwo9VddOJOFXV815vNVyFXLhJAvr85q78aqyaE90qgY2QDNXyZXziTbXLurISL8i_Mdzt0J7cUyrvQLZaK0_pMJJvZOg3LvNalTGdJjUHUFwPxTbe-DRWZuPT81KVNyNa8EKkqox1Ohm_56riMFyU.Od1O-XCSw2Xa8wBN.mHWq8eodcIDXKWjMDDqcNkunVLT0EvjvAWq28gc6aOfKN0zeT0SQczxl0jqcWlziHXE8KmDL7CDYhOZT4rv--lN6wDNj998.YjJfxro1W_ERT7jOvI06yA")] + [TestCase( + "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoidnI3Y1dLR2wtZzRXYV9DUkdvd05oQVlXX2dRYi1ha01iaWlneE4wRWtESSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJ6aXAiOiJERUYifQ.dNozKGHneskpBgbTZs8-aA3jCgH4jjMJHPmL2J2EPyysIpt0o2DQa1S7Dn5we88OHz8d3OuC5S2bO0rpI6mKTnJB8RAkFIn9P0LlkvIma915Bp3stoDWW8-VL9LDKV9j0-5K8y7EakTBvu0RxyvCw_WxIkum1w3vKcCBfwgnMU6kyB7h2t_ZgMdBud-Rap2xnN5Xu0TNq0LDKtCvJ2pk0AzExyVoumOg8LocIeonMkQcIRf0l_6aVGzSJXpLPdld2I0NWUGGa8OwTeoqBC9u_28fNBQfbNHdabZcYTEZFpqA4zqMpkAHi-tzzC-BUigRBfKSl5qcYeJQROpzxhkBEY9V7oFPFeY7gh5hKZPVxljtlPbD9nJjTQ_YVJEKd164RlCNNexFtFObxKOCwE4nkY9ouHTseeAVSAwA67LaD-Ap668RxwP6wp6U-KYCyVfXL08HUre9dKYJZwD9jl48F1l5ZPhgrMchBpPjG2o77jISoMGE6PhR2sTbgbfxACh6NrIFujIUz4CuGah8a84mFVznJIws-d_vvRJUaeHPwhkR2WcC7waUkS_Uf7KLlGdAWbZgtO7TjYxaTyTnIxswOBeN1JMos3PYpmkbgOF2LLJ4yx6Aq_pOe8SszMK26sCvQzpfXn28b9JYKMAAplKuQITWtXexW_L-VHeKepMvTTI.BscV4pPkGDfSFNXn.GGwY5fFBj7uK_-MY_ovQqVSkkMRZ7wfEeJNktHeN-hB8UYAN-yykvXpOHO3kAP3C377PF4p7BaoEsbUtdDRLRbS0qBm8qx-2dU6SeWU2kBZ-4xcYaF0hZaH6Lv6QHKuA9cJPtJIGnN9ooqlegFTyjAEvJu_H0SH8n3wnDEe-6sMEFwCwuyiL33G3dneH8-ozTzrD86ZC101BIguBaS5C-NHAyt4GreR8UEgV3Mhqh51qxEO9FsRs9TNzkkaYZTuD9I-UTtIAf01XJ2O_hRawmwMEANjgVun_IzZYSNiDk9uuyi3gznwuqXwu4AjsDuhfhc2c_NR6bAoovaBKCQIyyzn0SwHfnXm8iV-uyc1SeE3IVEOVV5fpsUwI_YLGprbRlF3JCp1RKfcNV67oRfc1vg.UlFSOsFR_h1YGobdj3Umeg")] + [TestCase( + "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoidnI3Y1dLR2wtZzRXYV9DUkdvd05oQVlXX2dRYi1ha01iaWlneE4wRWtESSIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24iLCJ6aXAiOiJERUYifQ.sPKcV2qS_MWepdSwq5_tmg5DtnARKeGOu058CSz6aB04g3AgCUOMNUetotxdRlvW0im9qnwOHUe_75Aty1DSnzv2ZUcsVwjf44g5KFSFb30w1kWccoCW8F2-9uAxHnpOPsM2qT_enUXDLWvt6L2JiX30hTjh62PwEXFzo36FmExi1oqwFM_aOPahHamfSQ_qwZ1cJRPgN67amgM_UAu4fTHccobJPEiKyqB6cejRfvmduLwiu7RmvPWiZA-ROLVu-rUvLWD65OpxOXOjVmLbxeig85qdYUBI8u55PnjBPouP-hqWf55ZFFUSWZgJ_7lYzTpkzAYPw967-9t7jZmXirj7RoSbGs5dvorDpVVQjQS3xg6o7PG63OXy3CLSSVmyVPCwULMaloebvUZKCrzRTjSXSR4uaLfBZFl2oEp2dBJn9ly_g3l6bapI494YH5N6m3MgvSy2zloQqIShcXi2aMoE9eKVm5fuO0UgqdLecJQ_M_4kUBcSITj_pwNiySYJkHXwhJUDY0GzjPe9aNKLGYzD6IpXG27OmFk-C2VtnyaiCDwB_D77KvvgLGdZKBwN8aWRuJxv0S6UtimrVF0rxLI8EtAEyPgaxW0vfSKoCFGYwmLh5Yuwg1AE2PFT-EbAWM3XJB-JlpbdoX5Xj7ZY8CFQfFzeLUxAZjtISzpaxyQ.MkNrLEOLpw3-aYGX.F1wksBj0QSHdbzK9vWUGwroo93PgU2GZxA6TiDTWoNXIqYM4IxtOpBJpQM9-aKKHx99_nyYhatgAYKx8lZ7kLrFE7J-KYnI.rlpSHJrYBeiz-tqULrLPRg")] + public void DecryptDataFromServer_None_ShouldWork(string data) { + var engine = new FitEncryption(_container.Resolve<KeySet>(), _logger) { + PublicKeyEncryption = _settings.PublicKeyEncryption, + PrivateKeyDecryption = _settings.PrivateKeyDecryption + }; + + var plain = engine.Decrypt(data); + Console.WriteLine(plain.plainText); + } +} diff --git a/EncryptionTests/Test.pdf b/EncryptionTests/Test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..50a038977b732f8c86889b164166410d5d60e0de Binary files /dev/null and b/EncryptionTests/Test.pdf differ diff --git a/FitConnect.sln b/FitConnect.sln index d5a5bcffde12205d47fcacd2dc4bfc6b02fc6927..6ebc17706ecb9c24786748c73c19456dce4042d0 100644 --- a/FitConnect.sln +++ b/FitConnect.sln @@ -2,7 +2,21 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FitConnect", "FitConnect\FitConnect.csproj", "{DFF6A0D9-5AA1-4640-B26C-4A0A28E42FA1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DummyClient", "DummyClient\DummyClient.csproj", "{DEF51494-6BCD-4441-8D76-6769BBA2C089}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests", "IntegrationTests\IntegrationTests.csproj", "{27115A99-2AE8-42BC-9495-BE2DCEDDF1E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EncryptionTests", "EncryptionTests\EncryptionTests.csproj", "{68F1EC39-B234-4422-9AA8-E55CFA15025D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicUnitTest", "BasicUnitTest\BasicUnitTest.csproj", "{39D5C71C-0723-49B5-8637-BF99346F0060}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MockContainer", "MockContainer\MockContainer.csproj", "{EEF0435D-2865-40C9-8987-3856AA8EBF04}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{180029B5-8DD3-4594-B34E-6C07AF1C52C5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Auxiliary", "Auxiliary", "{2F4278C5-FFC9-4080-A1C6-0CE9B5ED2B51}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoRunner", "DemoRunner\DemoRunner.csproj", "{930259A9-A5A9-4BB6-8DA7-4914FB57E7B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E2ETest", "E2ETest\E2ETest.csproj", "{8428631D-E396-4685-BE34-5E6C3307EE97}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -14,11 +28,41 @@ Global {DFF6A0D9-5AA1-4640-B26C-4A0A28E42FA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {DFF6A0D9-5AA1-4640-B26C-4A0A28E42FA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {DFF6A0D9-5AA1-4640-B26C-4A0A28E42FA1}.Release|Any CPU.Build.0 = Release|Any CPU - {DEF51494-6BCD-4441-8D76-6769BBA2C089}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DEF51494-6BCD-4441-8D76-6769BBA2C089}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DEF51494-6BCD-4441-8D76-6769BBA2C089}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DEF51494-6BCD-4441-8D76-6769BBA2C089}.Release|Any CPU.Build.0 = Release|Any CPU + {73CE5625-4C13-458E-B524-0DAA850F4041}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73CE5625-4C13-458E-B524-0DAA850F4041}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73CE5625-4C13-458E-B524-0DAA850F4041}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73CE5625-4C13-458E-B524-0DAA850F4041}.Release|Any CPU.Build.0 = Release|Any CPU + {27115A99-2AE8-42BC-9495-BE2DCEDDF1E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27115A99-2AE8-42BC-9495-BE2DCEDDF1E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27115A99-2AE8-42BC-9495-BE2DCEDDF1E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27115A99-2AE8-42BC-9495-BE2DCEDDF1E8}.Release|Any CPU.Build.0 = Release|Any CPU + {68F1EC39-B234-4422-9AA8-E55CFA15025D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68F1EC39-B234-4422-9AA8-E55CFA15025D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68F1EC39-B234-4422-9AA8-E55CFA15025D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68F1EC39-B234-4422-9AA8-E55CFA15025D}.Release|Any CPU.Build.0 = Release|Any CPU + {39D5C71C-0723-49B5-8637-BF99346F0060}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39D5C71C-0723-49B5-8637-BF99346F0060}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39D5C71C-0723-49B5-8637-BF99346F0060}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39D5C71C-0723-49B5-8637-BF99346F0060}.Release|Any CPU.Build.0 = Release|Any CPU + {EEF0435D-2865-40C9-8987-3856AA8EBF04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEF0435D-2865-40C9-8987-3856AA8EBF04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEF0435D-2865-40C9-8987-3856AA8EBF04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEF0435D-2865-40C9-8987-3856AA8EBF04}.Release|Any CPU.Build.0 = Release|Any CPU + {930259A9-A5A9-4BB6-8DA7-4914FB57E7B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {930259A9-A5A9-4BB6-8DA7-4914FB57E7B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {930259A9-A5A9-4BB6-8DA7-4914FB57E7B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {930259A9-A5A9-4BB6-8DA7-4914FB57E7B3}.Release|Any CPU.Build.0 = Release|Any CPU + {8428631D-E396-4685-BE34-5E6C3307EE97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8428631D-E396-4685-BE34-5E6C3307EE97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8428631D-E396-4685-BE34-5E6C3307EE97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8428631D-E396-4685-BE34-5E6C3307EE97}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution + {27115A99-2AE8-42BC-9495-BE2DCEDDF1E8} = {180029B5-8DD3-4594-B34E-6C07AF1C52C5} + {68F1EC39-B234-4422-9AA8-E55CFA15025D} = {180029B5-8DD3-4594-B34E-6C07AF1C52C5} + {39D5C71C-0723-49B5-8637-BF99346F0060} = {180029B5-8DD3-4594-B34E-6C07AF1C52C5} + {EEF0435D-2865-40C9-8987-3856AA8EBF04} = {180029B5-8DD3-4594-B34E-6C07AF1C52C5} + {930259A9-A5A9-4BB6-8DA7-4914FB57E7B3} = {2F4278C5-FFC9-4080-A1C6-0CE9B5ED2B51} + {8428631D-E396-4685-BE34-5E6C3307EE97} = {180029B5-8DD3-4594-B34E-6C07AF1C52C5} EndGlobalSection EndGlobal diff --git a/DummyClient/DummyClient.csproj.DotSettings b/FitConnect.sln.DotSettings similarity index 67% rename from DummyClient/DummyClient.csproj.DotSettings rename to FitConnect.sln.DotSettings index 453288bf2b5f62addc7e0fb4b6705154705069ae..f281d4db2dd13bfe3086a7132f4fed01ba09ac88 100644 --- a/DummyClient/DummyClient.csproj.DotSettings +++ b/FitConnect.sln.DotSettings @@ -1,2 +1,2 @@ <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> - <s:String x:Key="/Default/CodeInspection/Highlighting/UsageCheckingInspectionLevel/@EntryValue">Off</s:String></wpf:ResourceDictionary> \ No newline at end of file + <s:Boolean x:Key="/Default/UserDictionary/Words/=leika/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file diff --git a/FitConnect/BaseClasses/FunctionalBaseClass.cs b/FitConnect/BaseClasses/FunctionalBaseClass.cs new file mode 100644 index 0000000000000000000000000000000000000000..f6d5abbe54e1a32b74b6cd735f89b713b74a5138 --- /dev/null +++ b/FitConnect/BaseClasses/FunctionalBaseClass.cs @@ -0,0 +1,46 @@ +using FitConnect.Models; +using FitConnect.Services.Interfaces; +using Microsoft.Extensions.Logging; + +namespace FitConnect.BaseClasses; + +public abstract class FunctionalBaseClass { + protected readonly ILogger? Logger; + // protected readonly FitConnectApiService ApiService; + + protected IOAuthService OAuthService; + protected IRouteService RouteService; + protected ISubmissionService SubmissionService; + + + /// <summary> + /// Constructor for the FunctionalBaseClass + /// </summary> + /// <param name="environments">FitConnect endpoints</param> + /// <param name="logger">ILogger implementation</param> + /// <param name="oAuthService"></param> + /// <param name="submissionService"></param> + /// <param name="routeService"></param> + /// <param name="destinationService"></param> + /// <param name="certificate">The Encryption certificate</param> + /// <example> + /// new Sender(logger, FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development)) + /// </example> + protected FunctionalBaseClass(FitConnectEnvironment? environment, + ILogger? logger, + IOAuthService oAuthService, + ISubmissionService submissionService, + IRouteService routeService, IDestinationService destinationService) { + DestinationService = destinationService; + Environment = environment ?? + FitConnectEnvironment.Testing; + + Logger = logger; + OAuthService = oAuthService; + SubmissionService = submissionService; + RouteService = routeService; + } + + protected IDestinationService DestinationService { get; } + public FitConnectEnvironment Environment { get; } +} diff --git a/FitConnect/Client.cs b/FitConnect/Client.cs index 3146b465a5595f130f35e76764bb7a5669d2467e..5de16d8f385d049e05fef785750f118498a5d934 100644 --- a/FitConnect/Client.cs +++ b/FitConnect/Client.cs @@ -1,36 +1,55 @@ -using System; -using System.Threading.Tasks; +using Autofac; +using FitConnect.Interfaces.Sender; +using FitConnect.Interfaces.Subscriber; +using FitConnect.Models; using Microsoft.Extensions.Logging; namespace FitConnect; -/// <summary> -/// The FitConnect API Client -/// </summary> -// ReSharper disable once UnusedType.Global -public class Client { - internal string? ClientId; - internal string? ClientSecret; - - public IFluentSender Sender { get; } - - public IFluentSubscriber Subscriber { get; } - // private Routing Routing { get; } - +public static class Client { + public static ISender GetSender(FitConnectEnvironment environment, string clientId, + string clientSecret, ILogger? logger = null) { + return new Sender(environment, clientId, clientSecret, logger); + } /// <summary> - /// Constructor for the FitConnect API Client + /// Creates a subscriber client /// </summary> - /// <param name="environments">Choose one endpoint or create your own one</param> - /// <param name="clientId">Your client id</param> - /// <param name="clientSecret">Your client secret</param> - /// <param name="logger">Optional logger</param> - public Client( - FitConnectEnvironments environments, - string clientId, - string clientSecret, + /// <param name="environment">FIT Connect Environment</param> + /// <param name="clientId">Client ID</param> + /// <param name="clientSecret">Client Secret</param> + /// <param name="privateKeyDecryption">Private key for decryption</param> + /// <param name="privateKeySigning">Private key for signing</param> + /// <param name="publicKeyEncryption">Public key for encryption</param> + /// <param name="publicKeySignatureVerification">Public key for signature validation</param> + /// <param name="logger">An instance of an ILogger</param> + /// <returns></returns> + public static ISubscriber GetSubscriber(FitConnectEnvironment environment, + string clientId, string clientSecret, + string privateKeyDecryption, + string privateKeySigning, + string publicKeyEncryption, + string publicKeySignatureVerification, ILogger? logger = null) { - ClientId = clientId; - ClientSecret = clientSecret; + return new Subscriber(environment, clientId, clientSecret, privateKeyDecryption, + privateKeySigning, publicKeyEncryption, publicKeySignatureVerification, logger); + } + +#if DEBUG + public static ISender GetSender(FitConnectEnvironment environment, string clientId, + string clientSecret, IContainer container) { + return new Sender(environment, clientId, clientSecret, container); + } + + public static ISubscriber GetSubscriber(FitConnectEnvironment environment, + string clientId, string clientSecret, + string privateKeyDecryption, + string privateKeySigning, + string publicKeyEncryption, + string publicKeySignatureVerification, + IContainer container) { + return new Subscriber(environment, clientId, clientSecret, privateKeyDecryption, + privateKeySigning, publicKeyEncryption, publicKeySignatureVerification, container); } +#endif } diff --git a/FitConnect/DiContainer.cs b/FitConnect/DiContainer.cs new file mode 100644 index 0000000000000000000000000000000000000000..9d463b01d640c2145e514c2c4dd960d0966a9e34 --- /dev/null +++ b/FitConnect/DiContainer.cs @@ -0,0 +1,48 @@ +using System.Net; +using Autofac; +using FitConnect.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace FitConnect; + +public static class DiContainer { + private static IContainer? _container; + public static IWebProxy? WebProxy { get; set; } + + public static void DisposeContainer() { + _container?.Dispose(); + _container = null; + } + + public static IContainer GetContainer(FitConnectEnvironment environment, + ILogger? logger = null) { + return _container ??= BuildContainer(environment, logger); + } + + private static IContainer + BuildContainer(FitConnectEnvironment environment, + ILogger? logger = null) { + var builder = new ContainerBuilder(); + + if (logger == null) + logger = NullLoggerFactory.Instance.CreateLogger("FitConnect"); + + builder.RegisterInstance(environment).As<FitConnectEnvironment>(); + builder.RegisterInstance(logger).As<ILogger>(); + + builder.Register(c => new OAuthService(environment.TokenUrl)).As<IOAuthService>(); + builder.Register(c => new SubmissionService(environment.SubmissionUrl[0])) + .As<ISubmissionService>(); + builder.Register(c => new DestinationService(environment.SubmissionUrl[0])) + .As<IDestinationService>(); + builder.Register(c => new CasesService(environment.SubmissionUrl[0])) + .As<ICasesService>(); + builder.Register(c => new RouteService(environment.RoutingUrl)).As<IRouteService>(); + + + + + return builder.Build(); + } +} diff --git a/FitConnect/Encryption/FitEncryption.cs b/FitConnect/Encryption/FitEncryption.cs new file mode 100644 index 0000000000000000000000000000000000000000..7b0720f522dcd27a1b1ed1adf493ca3b7283542c --- /dev/null +++ b/FitConnect/Encryption/FitEncryption.cs @@ -0,0 +1,136 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Cryptography; +using System.Text; +using FitConnect.Models.v1.Api; +using IdentityModel; +using Jose; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; + +namespace FitConnect.Encryption; + +public class KeySet { + public string? PrivateKeyDecryption { get; set; } + public string? PrivateKeySigning { get; set; } + public string? PublicKeyEncryption { get; set; } + public string? PublicKeySignatureVerification { get; set; } +} + +public class FitEncryption { + private readonly IEncryptor _encryptor = new JoseEncryptor(); + private readonly ILogger? _logger; + private JwtHeader _jwtHeader; + + internal FitEncryption(ILogger? logger) { + _logger = logger; + } + + public FitEncryption(string? privateKeyDecryption, string? privateKeySigning, + string? publicKeyEncryption, string? publicKeySignatureVerification, ILogger? logger) : + this(logger) { + PrivateKeyDecryption = privateKeyDecryption; + PrivateKeySigning = privateKeySigning; + PublicKeyEncryption = publicKeyEncryption; + PublicKeySignatureVerification = publicKeySignatureVerification; + } + + public FitEncryption( + KeySet keySet, + ILogger? logger) : this(logger) { + PrivateKeyDecryption = keySet.PrivateKeyDecryption; + PrivateKeySigning = keySet.PrivateKeySigning; + PublicKeyEncryption = keySet.PublicKeyEncryption; + PublicKeySignatureVerification = keySet.PublicKeySignatureVerification; + } + + public string? PrivateKeyDecryption { get; set; } + public string? PrivateKeySigning { get; set; } + public string? PublicKeyEncryption { get; set; } + public string? PublicKeySignatureVerification { get; set; } + + public (string plainText, byte[] plainBytes, byte[] tag) + Decrypt(string cypherText, string key) { + return _encryptor.Decrypt(key, cypherText); + } + + public (string plainText, byte[] plainBytes, byte[] tag) Decrypt(string cypherText) { + if (PrivateKeyDecryption == null) + throw new InvalidOperationException("PrivateKey is not provided"); + + return Decrypt(cypherText, PrivateKeyDecryption); + } + + + public string Encrypt(string plainText, string key) { + return _encryptor.Encrypt(key, plainText); + } + + public string Encrypt(string plain) { + if (PublicKeyEncryption == null) + throw new InvalidOperationException("PublicKey is not provided"); + + return Encrypt(plain, PublicKeyEncryption); + } + + public string Encrypt(byte[] plain, string key) { + return _encryptor.Encrypt(key, plain); + } + + public string Encrypt(byte[] plain) { + if (PublicKeyEncryption == null) + throw new InvalidOperationException("PublicKey is not provided"); + + return Encrypt(plain, PublicKeyEncryption); + } + + public string CreateSecurityEventToken(string submissionId, + string caseId, + string destinationId, + string eventName, Problems[]? problemsArray) { + var signingKey = Jwk.FromJson(PrivateKeySigning, new JsonMapper()); + var transactionId = "case:" + caseId; + var subject = "submission:" + submissionId; + + _jwtHeader = + new JwtHeader(new SigningCredentials(new JsonWebKey(PrivateKeySigning), "PS512")) { + //{ "typ", "secevent+jwt" }, + // { "kid", signingKey.KeyId }, + // { "alg", "PS512" } + }; + + object problems = problemsArray == null ? new { } : new { problems = problemsArray }; + + _jwtHeader["typ"] = "secevent+jwt"; + + var token = new JwtSecurityToken( + _jwtHeader, new JwtPayload { + { "sub", subject }, + { "jti", Guid.NewGuid().ToString() }, + { "iat", DateTime.UtcNow.ToEpochTime() }, + { "iss", destinationId }, { + "events", + new Dictionary<string, object> { + { eventName, problems } + } + }, + { "txn", transactionId } + }); + + var handler = new JwtSecurityTokenHandler(); + + + return handler.WriteToken(token); + } + + public static string CalculateHash(string data) { + return ByteToHexString(SHA512.Create().ComputeHash(Encoding.UTF8.GetBytes(data))); + } + + private static string ByteToHexString(IEnumerable<byte> data) { + var sb = new StringBuilder(); + foreach (var b in data) sb.Append(b.ToString("x2")); + + return sb.ToString(); + } +} diff --git a/FitConnect/Encryption/IEncryptor.cs b/FitConnect/Encryption/IEncryptor.cs new file mode 100644 index 0000000000000000000000000000000000000000..3af8f0f375785dd8864060560252fb0bd8ff56f8 --- /dev/null +++ b/FitConnect/Encryption/IEncryptor.cs @@ -0,0 +1,7 @@ +namespace FitConnect.Encryption; + +public interface IEncryptor { + public (string plainText, byte[] plainBytes, byte[] tag) Decrypt(string key, string cipher); + public string Encrypt(string key, string plain); + public string Encrypt(string key, byte[] plain); +} diff --git a/FitConnect/Encryption/JoseEncryptor.cs b/FitConnect/Encryption/JoseEncryptor.cs new file mode 100644 index 0000000000000000000000000000000000000000..91ca13ad3b441165fb71911b374561180ac55d45 --- /dev/null +++ b/FitConnect/Encryption/JoseEncryptor.cs @@ -0,0 +1,72 @@ +using Jose; + +// ReSharper disable RedundantExplicitArrayCreation + +namespace FitConnect.Encryption; + +public class JoseEncryptor : IEncryptor { + private const JweEncryption Encryption = JweEncryption.A256GCM; + private const JweCompression Compression = JweCompression.DEF; + private const JweAlgorithm Algorithm = JweAlgorithm.RSA_OAEP_256; + private const SerializationMode SerializationMode = Jose.SerializationMode.Compact; + + private const string ErrorMessage = + "On macOS add DYLD_LIBRARY_PATH to your environment variables. Look at the README for more information."; + + + public (string plainText, byte[] plainBytes, byte[] tag) + Decrypt(string key, string cipher) { + try { + var jwk = Jwk.FromJson(key, new JsonMapper()); + return Decrypt(jwk, cipher); + } + catch (PlatformNotSupportedException e) { + throw new PlatformNotSupportedException(ErrorMessage, e); + } + } + + public string Encrypt(string key, string plain) { + // ti1Z0KkG1uAXvd2XnWb5wWu8slGylvsvz3nOSe7yuAc + try { + var jwk = Jwk.FromJson(key, new JsonMapper()); + return JWE.Encrypt(plain, + new JweRecipient[] { new(Algorithm, jwk) }, + Encryption, + compression: Compression, + mode: SerializationMode, + extraProtectedHeaders: new Dictionary<string, object> { + { "kid", jwk.KeyId }, + { "cty", "application/json" } + } + ); + } + catch (PlatformNotSupportedException e) { + throw new PlatformNotSupportedException(ErrorMessage, e); + } + } + + public string Encrypt(string key, byte[] plain) { + try { + var jwk = Jwk.FromJson(key, new JsonMapper()); + return JWE.EncryptBytes(plain, + new JweRecipient[] { new(Algorithm, jwk) }, + Encryption, + compression: Compression, + mode: SerializationMode, + extraProtectedHeaders: new Dictionary<string, object> { + { "kid", jwk.KeyId }, + { "cty", "application/json" } + } + ); + } + catch (PlatformNotSupportedException e) { + throw new PlatformNotSupportedException(ErrorMessage, e); + } + } + + private (string plainText, byte[] plainBytes, byte[] tag) Decrypt(Jwk key, string payload) { + var result = JWE.Decrypt(payload, key, Algorithm, Encryption); + + return (result.Plaintext, result.PlaintextBytes, result.AuthTag); + } +} diff --git a/FitConnect/FitConnect.csproj b/FitConnect/FitConnect.csproj index 25b093e1c63712f8f48c97e92133e5d0f37be8f1..3827c420f787bd35cf4c8de00b4f830dda6be88e 100644 --- a/FitConnect/FitConnect.csproj +++ b/FitConnect/FitConnect.csproj @@ -4,27 +4,34 @@ <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> - <AssemblyVersion>0.0.0</AssemblyVersion> - <FileVersion>0.0.0</FileVersion> - <PackageVersion>0.0.0</PackageVersion> + <AssemblyVersion></AssemblyVersion> + <FileVersion></FileVersion> + <PackageVersion>0.1.0-beta.1</PackageVersion> <Title>FitConnect SDK</Title> </PropertyGroup> <ItemGroup> - <PackageReference Include="Autofac" Version="6.4.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" /> - <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> - <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> - <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.20.0" /> + <PackageReference Include="Autofac" Version="6.4.0" /> + <PackageReference Include="IdentityModel" Version="6.0.0" /> + <PackageReference Include="jose-jwt" Version="4.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" /> + <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.21.0" /> + <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.21.0" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> + <PackageReference Include="NJsonSchema" Version="10.7.2" /> + <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.21.0" /> </ItemGroup> <ItemGroup> - <None Remove="metadata.schema.json" /> - <EmbeddedResource Include="metadata.schema.json" /> + <None Remove="metadata.schema.json" /> + <EmbeddedResource Include="metadata.schema.json" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\Services\Services.csproj" /> + <Compile Remove="FunctionalBaseClass.cs" /> + <Compile Remove="Models\OAuthAccessToken.cs" /> + <Compile Remove="DiContainer.cs" /> </ItemGroup> </Project> diff --git a/FitConnect/FitConnect.csproj.DotSettings b/FitConnect/FitConnect.csproj.DotSettings index f82351d60d72e7de02dd6aab4adca153a75297f6..d80459a6f9c67eaa547e53ce6c84c64a060debbc 100644 --- a/FitConnect/FitConnect.csproj.DotSettings +++ b/FitConnect/FitConnect.csproj.DotSettings @@ -1,2 +1,2 @@ <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> - <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=interfaces/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=interfaces/@EntryIndexedValue">False</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file diff --git a/FitConnect/FitConnectClient.cs b/FitConnect/FitConnectClient.cs new file mode 100644 index 0000000000000000000000000000000000000000..8968ff7f4eff54900500801c3638949b3693cca1 --- /dev/null +++ b/FitConnect/FitConnectClient.cs @@ -0,0 +1,136 @@ +using System.Net; +using Autofac; +using FitConnect.Encryption; +using FitConnect.Models; +using FitConnect.Services; +using FitConnect.Services.Interfaces; +using Microsoft.Extensions.Logging; + +namespace FitConnect; + +public abstract class FitConnectClient { + private readonly string? _privateKeyDecryption; + private readonly string? _privateKeySigning; + private readonly string? _publicKeyEncryption; + private readonly string? _publicKeySignatureVerification; + + + protected FitConnectClient(FitConnectEnvironment environment, + string clientId, string clientSecret, + ILogger? logger = null, + string? privateKeyDecryption = null, + string? privateKeySigning = null, + string? publicKeyEncryption = null, + string? publicKeySignatureVerification = null + ) : this() { + _privateKeyDecryption = privateKeyDecryption; + _privateKeySigning = privateKeySigning; + _publicKeyEncryption = publicKeyEncryption; + _publicKeySignatureVerification = publicKeySignatureVerification; + OAuthService = new OAuthService(environment.TokenUrl, "V1", clientId, clientSecret, logger); + SubmissionService = + new SubmissionService(environment.SubmissionUrl[0], OAuthService, logger: logger); + RouteService = new RouteService(environment.RoutingUrl, OAuthService, logger: logger); + CasesService = new CasesService(environment.SubmissionUrl[0], OAuthService, logger: logger); + DestinationService = + new DestinationService(environment.SubmissionUrl[0], OAuthService, logger: logger); + Logger = logger; + } + + protected FitConnectClient(FitConnectEnvironment environment, + string clientId, string clientSecret, + IContainer container, + string? privateKeyDecryption = null, + string? privateKeySigning = null, + string? publicKeyEncryption = null, + string? publicKeySignatureVerification = null + ) : this() { + _privateKeyDecryption = privateKeyDecryption; + _privateKeySigning = privateKeySigning; + _publicKeyEncryption = publicKeyEncryption; + _publicKeySignatureVerification = publicKeySignatureVerification; + OAuthService = container.Resolve<IOAuthService>(); + SubmissionService = container.Resolve<ISubmissionService>(); + RouteService = container.Resolve<IRouteService>(); + CasesService = container.Resolve<ICasesService>(); + DestinationService = container.Resolve<IDestinationService>(); + Logger = container.Resolve<ILogger>(); + } + + + private FitConnectClient() { + Encryption = new FitEncryption(Logger); + } + + public IOAuthService OAuthService { get; } + public ISubmissionService SubmissionService { get; } + public IRouteService RouteService { get; } + public IDestinationService DestinationService { get; } + public ICasesService CasesService { get; } + public ILogger? Logger { get; } + public FitEncryption Encryption { get; set; } + + public void SetProxy(WebProxy proxy) { + OAuthService.Proxy = proxy; + SubmissionService.Proxy = proxy; + DestinationService.Proxy = proxy; + RouteService.Proxy = proxy; + } + + public WebProxy? GetProxy() { + return OAuthService.Proxy; + } + + + /// <summary> + /// Retrieve the events for the submission + /// </summary> + /// <param name="caseId"></param> + /// <returns></returns> + public List<SecurityEventToken> GetStatusForSubmission(string caseId) { + var events = SubmissionService.GetStatusForSubmissionAsync(caseId).Result; + return events.Select(e => new SecurityEventToken(e)).ToList(); + } + + /// <summary> + /// Encrypt attachments (Anhänge) + /// </summary> + /// <param name="publicKey"></param> + /// <param name="attachment"></param> + /// <returns></returns> + public KeyValuePair<string, string> Encrypt(string publicKey, Attachment attachment) { + var content = Encryption.Encrypt(attachment.Content, publicKey); + return new KeyValuePair<string, string>(attachment.Id, content); + } + + /// <summary> + /// Encrypt attachments (Anhänge) + /// </summary> + /// <param name="publicKey">Public key for encryption</param> + /// <param name="attachments">List of attachments to encrypt</param> + /// <returns></returns> + public Dictionary<string, string> Encrypt(string publicKey, + IEnumerable<Attachment> attachments) { + return attachments.Select(a => Encrypt(publicKey, a)) + .ToDictionary(p => p.Key, p => p.Value); + } +} + +public static class FitConnectClientExtensions { + /// <summary> + /// </summary> + /// <param name="host"></param> + /// <param name="port"></param> + /// <param name="username"></param> + /// <param name="password"></param> + /// <returns></returns> + public static T WithProxy<T>(this T caller, string host, int port, string? username = null, + string? password = null) where T : FitConnectClient { + var proxy = new WebProxy(host, port); + if (username != null && password != null) + proxy.Credentials = new NetworkCredential(username, password); + + caller.SetProxy(proxy); + return caller; + } +} diff --git a/FitConnect/FunctionalBaseClass.cs b/FitConnect/FunctionalBaseClass.cs deleted file mode 100644 index e9cb4b2de5a4df239d7e6cb5f2f8cd839808f50e..0000000000000000000000000000000000000000 --- a/FitConnect/FunctionalBaseClass.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Security.Cryptography; -using System.Text; -using FitConnect.Models; -using Microsoft.Extensions.Logging; - -namespace FitConnect; - -public class FitConnectEnvironments { - /// <summary> - /// Default constructor. - /// </summary> - /// <param name="tokenUrl">URL for receiving the OAuth token</param> - /// <param name="submissionApi">URL for the submission API</param> - /// <param name="routingApi">URL for the routing API</param> - public FitConnectEnvironments(string tokenUrl, string[] submissionApi, string routingApi) { - TokenUrl = tokenUrl; - SubmissionApi = submissionApi; - RoutingApi = routingApi; - } - - /// <summary> - /// URL for receiving the OAuth token. - /// </summary> - public string TokenUrl { get; } - - /// <summary> - /// URL for the submission API. - /// </summary> - public string[] SubmissionApi { get; } - - /// <summary> - /// URL for the routing API. - /// </summary> - public string RoutingApi { get; } - - - public enum EndpointType { - Development, - Testing, - Production - } - - /// <summary> - /// Creates the endpoints for the given environment. - /// </summary> - /// <param name="endpointType">Environment to get endpoints for</param> - /// <returns></returns> - /// <exception cref="ArgumentException">Not all environments are ready to use</exception> - public static FitConnectEnvironments Create(EndpointType endpointType) { - return endpointType switch { - EndpointType.Development => DevEnvironments, - EndpointType.Testing => throw new ArgumentException("Not approved for online testing"), - EndpointType.Production => throw new ArgumentException("NOT PRODUCTION READY"), - _ => throw new ArgumentOutOfRangeException(nameof(endpointType), endpointType, null) - }; - } - - private static readonly FitConnectEnvironments DevEnvironments = new( - "https://auth-testing.fit-connect.fitko.dev/token", - new []{"https://submission-api-testing.fit-connect.fitko.dev"}, - "https://routing-api-testing.fit-connect.fitko.dev" - ); - - private static readonly FitConnectEnvironments TestEnvironments = new( - "https://auth-testing.fit-connect.fitko.dev/token", - new []{"https://submission-api-testing.fit-connect.fitko.dev"}, - "https://routing-api-testing.fit-connect.fitko.dev" - ); - - private static readonly FitConnectEnvironments ProductionEnvironments = new( - "https://auth-testing.fit-connect.fitko.dev/token", - new []{"https://submission-api-testing.fit-connect.fitko.dev"}, - "https://routing-api-testing.fit-connect.fitko.dev" - ); -} - -public abstract class FunctionalBaseClass { - protected readonly ILogger? logger; - private RSA _rsa; - public FitConnectEnvironments Environments { get; } - - protected FunctionalBaseClass(ILogger? logger, FitConnectEnvironments? endpoints) { - Environments = endpoints ?? - FitConnectEnvironments.Create(FitConnectEnvironments.EndpointType.Development); - - this.logger = logger; - } - - - /// <summary> - /// Requesting an OAuth token from the FitConnect API. - /// - /// You can get the Client ID and Client Secret from the FitConnect Self Service portal - /// under https://portal.auth-testing.fit-connect.fitko.dev - /// </summary> - /// <param name="clientId">Your client Id</param> - /// <param name="clientSecret">Your client Secret</param> - /// <param name="scope">Scope if needed</param> - /// <returns></returns> - public async Task<OAuthAccessToken?> GetTokenAsync(string clientId, string clientSecret, - string? scope = null) { - var client = new HttpClient(); - client.DefaultRequestHeaders.Accept.Add( - MediaTypeWithQualityHeaderValue.Parse("application/json")); - - var requestContent = new Dictionary<string, string> { - { "grant_type", "client_credentials" }, - { "client_id", clientId }, - { "client_secret", clientSecret } - }; - - if (scope != null) - requestContent["scope"] = scope; - - var content = new FormUrlEncodedContent(requestContent); - - var request = new HttpRequestMessage(HttpMethod.Post, Environments.TokenUrl) { - Content = content, - Method = HttpMethod.Post - }; - - var response = await client.SendAsync(request); - - return await response.Content.ReadFromJsonAsync<OAuthAccessToken>(); - } - - public Task<SecurityEventToken> GetSetDataAsync() { - throw new NotImplementedException(); - } - - public byte[] EncryptDataAsync(byte[] data, - byte[]? publicKey, - string? password, - int numberOfIterations) { - _rsa = RSA.Create(2048); - // _rsa.ImportRSAPublicKey(publicKey, out var read); - - logger?.LogInformation( - "Encrypting data with public key: {}", - Convert.ToBase64String(_rsa.ExportRSAPublicKey())); - - // var keyParams = new PbeParameters( - // PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, numberOfIterations); - // - // var privateKey = - // rsa.ExportEncryptedPkcs8PrivateKey(Encoding.UTF8.GetBytes(password), keyParams); - // - // logger?.LogInformation( - // "Private key: {}", privateKey); - - return _rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256); - } - - public byte[] DecryptDataAsync(byte[] data, byte[] privateKey) { - return _rsa.Decrypt(data, RSAEncryptionPadding.OaepSHA256); - } - - - private async Task<T?> RestCall<T>(Uri uri, HttpMethod method, string body) { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("Accept", "application/json"); - client.DefaultRequestHeaders.Add("Content-Type", "application/json"); - - var request = new HttpRequestMessage(); - request.Method = method; - request.Content = new StringContent(body, Encoding.UTF8, "application/json"); - request.RequestUri = uri; - - var response = await client.SendAsync(request); - - if (response.IsSuccessStatusCode) - return (await response.Content.ReadFromJsonAsync<T>()); - - throw new HttpRequestException("Error calling FitConnect API"); - } -} diff --git a/FitConnect/HttpCalls/OAuthToken.http b/FitConnect/HttpCalls/OAuthToken.http new file mode 100644 index 0000000000000000000000000000000000000000..2f94b0f5eb0d9af3cb0100c270efe7d175242ba5 --- /dev/null +++ b/FitConnect/HttpCalls/OAuthToken.http @@ -0,0 +1,12 @@ +### Getting the data from the database BEARER +POST {{oauth_api_url}}/token +Content-Type: application/x-www-form-urlencoded +Accept: application/json + +grant_type=client_credentials&client_id={{senderId}}&client_secret={{senderSecret}} + +> {% + client.global.set("token", response.body.access_token); + // client.global.scope = response.body['scope'] + %} + diff --git a/FitConnect/HttpCalls/RoutingApi.http b/FitConnect/HttpCalls/RoutingApi.http new file mode 100644 index 0000000000000000000000000000000000000000..ca26f34339125366d1cef16c841c87716c31ba9f --- /dev/null +++ b/FitConnect/HttpCalls/RoutingApi.http @@ -0,0 +1,8 @@ +### Get areas +GET {{routing_api_url}}/v1/areas?areaSearchexpression=93437 +Accept: application/json + +### Get areas +GET {{routing_api_url}}/v1/routes?leikaKey=99012070006000 +Accept: application/json + diff --git a/FitConnect/HttpCalls/SenderCalls.http b/FitConnect/HttpCalls/SenderCalls.http new file mode 100644 index 0000000000000000000000000000000000000000..6834bb4b14fef316283551bb70a4897c2edba7aa --- /dev/null +++ b/FitConnect/HttpCalls/SenderCalls.http @@ -0,0 +1,67 @@ +### Get Destination infos +GET {{submission_api_url}}/v1/destinations/{{destinationId}} +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{token}} + +> {% + client.global.set('keyId', response.body.encryptionKid); + %} + +### Get Destination infos +GET {{submission_api_url}}/v1/destinations/{{destinationId}}/keys/{{keyId}} +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{token}} + + +### Create submission +POST {{submission_api_url}}/v1/submissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "destinationId": "{{destinationId}}", + "announcedAttachments": [ + "12399641-2067-4e8b-b049-91b2a6c90544" + ], + "serviceType": { + "name": "Antrag Name", + "identifier": "urn:de:fim:leika:leistung:99102013104000" + }, + "callback": { + "url": "https://my-onlineservice.example.org/callbacks/fit-connect", + "secret": "insecure_unsafe_qHScgrg_kP-R31jHUwp3GkVkGJolvBchz65b74Lzue0" + } +} + +> {% + client.global.set('caseId', response.body.caseId); + client.global.set('submissionId', response.body.submissionId); + %} + +### GET state of Submission +GET {{submission_api_url}}/v1/cases/{{caseId}}/events +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{token}} + + +### GET state of Submission +GET {{submission_api_url}}/v1/cases/4b491eac-2ca0-4b45-94c0-0206b736f9e6/events +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{token}} + + +### Submit submission +PUT {{submission_api_url}}/v1/submissions/{{submissionId}} +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "encryptedMetadata": "l3--gxZp131zyqCOWmdzxHiNySKGSlyOI63eIBoPDg1-f9v-GPFLCjNANFbW8y96orN0RYLsWQEa4JAV93lvz_caJQXMyFiE8VdRr7SjS4ivBfyizdplnnBQ2V_vZCL7t65GneogIF-xpiXF4zxOH6dY5BJxq9IC-6EkWK76fSa5xfe2k-SXDQXlqP6fWqkR2iECKO16cY5rrneMzkvC9nPNzeMKtlcnirQfKPEklK5Kp_6gLRT8nQrBUma5bDhctpOweSIiVeuv_zgHOzvxvuK6_Xoc1M0WHniR4y_qUA8YzLUI1-FfMu1N88TI0brcngbF4zVarQXv7q8gzGynEgTln11JrL0Fx87U_-Gfc5RUsjxMynEgZ8Qr72UpYF3HcMEg4xRTeivlWiOu4s9hvSyE2RJanp6amB68CPG-TUaS5tKbEPjHc5zmTOedUAgw-mfLOJNAleW_J-7L07pWCYxR0Jr5hVMFBBCuh1mbeMoW1kF1mfmDwT_X6U-cC_srvXQiSVk7kub0NJRpG9mx6FvHapVZRO709Luslj4Fw9d8Jnw6zw_MAxbBPOvNAVVm20Nqm66_aB4wv-qJDWA3SQZFW5dO4Zuh8eubLhOBes0jHTEFQSh6q4r1zVNYC1nSJUYzYpK9eaNa43oXF-HVp4rdXNaTyE6M0Vd5YgZwbOLrVS4QyvvURMgOcD5xRQCi6F89ZrAN09aoCAeaKuB-r5VuDfquYB5J6RMSXNLvXuHOR1iHwcC6EFvNnqzDystsDXJudPLSdiQJBszpOjM28DZgtl-I0hw7-FVKdTWlkB9IpbjTNMOdJcWMUCpoJIkAIiWYlAsmZ0K22mj_fFY08kPKyuEOJNBB_dDf2O_kF92PNY51I1Npa_PE2uXEaBp95772KvSdvn6fDri42iGJGIGS5cADQDHiDTYGrBsluzdLD6PKD4XBrGipM9jkWbTRNG8lY4lkNtUwtKU5rVDcbdstAivwdlTpeLKHVjPE-ITW-5QhM13HCgZ6eJPFpwpT4VpAUT4euikzoRccyJOYqkzKr1Z0JP-7kop-0GnKNZ1zBJEaEq_gf53lYInrV9f3EQM8ynQpp_0wMNwVU6_0xOjEMX5DMt2io_whkNEvuTa56A" +} + diff --git a/FitConnect/HttpCalls/StaticTests.http b/FitConnect/HttpCalls/StaticTests.http new file mode 100644 index 0000000000000000000000000000000000000000..f2996b8a2a3a4bff6684e2cc0d09e9b4b8128824 --- /dev/null +++ b/FitConnect/HttpCalls/StaticTests.http @@ -0,0 +1,24 @@ +### Getting the data from the database BEARER +POST {{oauth_api_url}}/token +Content-Type: application/x-www-form-urlencoded +Accept: application/json + +grant_type=client_credentials&client_id={{senderId}}&client_secret={{senderSecret}} + +> {% + client.global.set("token", response.body.access_token); + // client.global.scope = response.body['scope'] + %} + + + +### Submit submission +PUT {{submission_api_url}}/v1/submissions/d698150c-0792-4be7-aa42-580fb0774f02 +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "encryptedMetadata": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJjdHkiOiJhcHBsaWNhdGlvbi9qc29uIiwiZW5jIjoiQTI1NkdDTSIsImtpZCI6InZyN2NXS0dsLWc0V2FfQ1JHb3dOaEFZV19nUWItYWtNYmlpZ3hOMEVrREkiLCJ6aXAiOiJERUYifQ.MMPBbWa7gnK-bu2Iocp0WVT7IeVqpwdV23cH3t9qGmkARXLF14bVQh8oT0wIaZRhn0DLUEwv9tsQch7oZvOj4KZdyip9F-cvt4Z0re74nzC1pC9FGGVdUmOWOiOeh8KSnpP6iVWUlA5HVotao0yOt6OqeqdnbgSt_MT7ARPURAvqvTCTgZNtY3_dBKhm0AQMyaGlQdMZitogXNgXXiXPbydWpgoLvOEB_Y43cIPdqBN1iK1zhuwHHeXVSfxCtYNOkh_mqSAvDy-lHK9rYschhjhynOBEQYzFv66tiCn7HjJpSoFY2XaOM07PYy45dMUEzQv_bkQlnlNhAn58K_FUvwsT0BhRX12pq27Xpg6X7qa4KYrxByKHsiPSegJtsIMT-24BSU8vU3KEEqIvwlp5XtPgPCi4mrzU_wqGozxlbrAlT59mvcxhd3LUQWBjd5EByIZZXRkq9b2jvsiauZahzUppMX8r5iswrlteB7SkVJUMVA3_uGPFPfxGyIBAykS28DB7i9M-_MIoEEkT07xNMHApU9Q95I02c8q9dnQb146ZwqlQ8oGepparaCQrUVIIjD--6oh67ta5M2e1NesK_iWEgzTEOsh_ymyv7TFNmTO9iMQ7HuK8Mf6tozyqruzAHTqiPcd9DdrO3TJMrTyYIXQSnsFz_uyxJ_09bcVDD4M.WNkZENRbSaFdfaPA.YVVkbO3aHaTiNbRO33F2R99gh0PAMyUV6Az_q_Po7mUFTXfvgdRI__VPjYVlktRkkZXmQs13n8BVLbLGH_OkobOvLD3Ufxdn8z0Lot3f_ydXOGsdc1ID-OqAx3mhxzrQi9JdO4QYSWtCIHXi96iyyljQmVwj27ioukNOLbfiH2WVjXR1m7v92ePc8FLDNUMh-epXYk-lSyV3T5w1xRXljZrS53ictqFXrQd31crDVfbCilXwlndYLJCp7NvvyoprtOZIEXsUpiFLpOT3du5PhNrPoAXPbjpYFAKLaI3F56vaSwqF820NJlJWXyRE18m8MVpELRllPUl1D96w7-YnsUbsMWgB2rqoPS8ArzbkIpEa-CIpgnNVTEptnFLAye3Qnb9kCnkjQNdE8AfMfneOcW2Jdox2hwM178tqR5jxxRN3sucfqQ_vuqSo3CcLgnFnYP1GpYBdSv_ehus4VrMEtjRtp5rjjpnqUYydyP3JX7FSUmvxYigq5_-yO24UZQLozcnwL04_oMMbqPfMsiyY_GOS-jA5xl4Qt6wooixoH1nQgJpcVvIniaRdZ7oz3uUhmNVxB5aSkC8UO9KCtp2Warg.VajPcUlW2bShHc09-7KQ8Q", + "encryptedData": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJjdHkiOiJhcHBsaWNhdGlvbi9qc29uIiwiZW5jIjoiQTI1NkdDTSIsImtpZCI6InZyN2NXS0dsLWc0V2FfQ1JHb3dOaEFZV19nUWItYWtNYmlpZ3hOMEVrREkiLCJ6aXAiOiJERUYifQ.qbMW9T1LYYA1jmcjWZi0AYiUrg_124Fk8Hmfx6Fjt6usv0PKHLzMHt3Z7j_yKr9nSLbosoKB6a5FwjgZvkaq_ffuPdCQt36YIGoEdpxP2y3lkEdx8662S-wVKpjukRJi1vFK5Ymq6W6bk2Tsh2GAgwkXr_7q_WjIzRtBRrVmzmHjoDB8jUrQv9_7yQJy-1lCs0_efhmxZPHeHTmOgkHy8w-xAhjVlhGH2ErgFeUZESGjHGGKVg3kancxeNpLz0hIRAej2C8M7QKMTpcuim_J0M70vOK5VFDcDeYxJSUaaIXGMYv2-foISVAA5Dp0t5BZcQTNUT5MbMGBTGcFnICaBEEAef5jWG6320heQavyDLpdS96ddCuWRGIJHlG0wLORPuJqR47kxfR_9BPc2eYJEG8bEFeyPmDQ83Qqi8JTX5Tz1mLGHyUIA8y_tl3qTV1emc6MAAeG-TWTmrds5msjEWaZ3Q9Ee3m55b0PKVMR8lT5P_i0amhZwQIVUXmZYwRepXqXhezJrF-G2Pltu7ge83nCu7i9a5q740sR25cKv3qv3KLBgi-mliSFswTP1rAOCs_UkjN1GmOOQD0mgtPpuY_-yAhl2XOOBgfkHcDKzGP4abTCsEabAaQlbe3bwIKmh2IJuPfPB-YbT7cdrEbJdRo2YBsJksAqm_mDRbLqCtA.fdb92PBWLFD6m8iw.Yxo8kvGgCEgJL-MVWtu64b5Bc0OxlsNedNmCcwCsQXiCuqkucNWxHacpk7AbOr_3ik_8IxFFRt0gnRYXfiiUuKLH1kvuePgY_eQY.iJ3f1rHtnDxvG2qz_snZ-A" +} diff --git a/FitConnect/HttpCalls/SubscriberCalls.http b/FitConnect/HttpCalls/SubscriberCalls.http new file mode 100644 index 0000000000000000000000000000000000000000..ed3b6888221967d812277576b5f191fe77ec7b4b --- /dev/null +++ b/FitConnect/HttpCalls/SubscriberCalls.http @@ -0,0 +1,39 @@ +### Get Token +POST {{oauth_api_url}}/token +Content-Type: application/x-www-form-urlencoded +Accept: application/json + +grant_type=client_credentials&client_id={{subscriberId}}&client_secret={{subscriberSecret}} + +> {% + client.global.set("subscriber_token", response.body.access_token); + // client.global.scope = response.body['scope'] + %} + + + +### GET Avaliable submissions +GET {{submission_api_url}}/v1/submissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{subscriber_token}} + +### GET Avaliable submissions +GET {{submission_api_url}}/v1/submissions/4d0da617-40ab-4e8b-8c20-2aa7024bb2e7 +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{subscriber_token}} + + +### GET Avaliable submissions +GET {{submission_api_url}}/v1/submissions/7d183fc1-e0e4-48bb-ae1d-c9522dea9591 +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{subscriber_token}} + + +### GET Attachment +GET {{submission_api_url}}/v1/submissions/303c60d3-3f6f-4913-bb51-4e66fe133c7f/attachments/27774bc8-4c37-4df0-866b-5be1f6a3eefb +Accept: application/jose +Content-Type: application/jose +Authorization: Bearer {{subscriber_token}} diff --git a/FitConnect/HttpCalls/http-client.env.json b/FitConnect/HttpCalls/http-client.env.json new file mode 100644 index 0000000000000000000000000000000000000000..d4a228a07c5b955ad9aec3e168b996ae6e767285 --- /dev/null +++ b/FitConnect/HttpCalls/http-client.env.json @@ -0,0 +1,13 @@ +{ + "dev": { + "routing_api_url": "https://routing-api-testing.fit-connect.fitko.dev", + "submission_api_url": "https://submission-api-testing.fit-connect.fitko.dev", + "oauth_api_url": "https://auth-testing.fit-connect.fitko.dev", + "keyId": "", + "destinationId": "aa3704d6-8bd7-4d40-a8af-501851f93934", + "senderId": "73a8ff88-076b-4263-9a80-8ebadac97b0d", + "senderSecret": "rdlXms-4ikO47AbTmmCTdzFoE4cTSt13JmSbcY5Dhsw", + "subscriberId": "20175c2b-c4dd-4a01-99b1-3a08436881a1", + "subscriberSecret": "KV2qd7qc5n-xESB6dvfrTlMDx2BWHJd5hXJ6pKKnbEQ" + } +} \ No newline at end of file diff --git a/FitConnect/Interfaces/IBaseFunctionality.cs b/FitConnect/Interfaces/IBaseFunctionality.cs deleted file mode 100644 index 14665ab9662ba3662f28bca67de41159ec4162c0..0000000000000000000000000000000000000000 --- a/FitConnect/Interfaces/IBaseFunctionality.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Threading.Tasks; -using FitConnect.Models; - -namespace FitConnect; - -public interface IBaseFunctionality { - Task<OAuthAccessToken> GetTokenAsync(string clientId, string clientSecret, - string? scope = null); - - Task<SecurityEventToken> GetSetDataAsync(); - - // Receive SecurityEventToken and check signature -} \ No newline at end of file diff --git a/FitConnect/Interfaces/IFluentApi.cs b/FitConnect/Interfaces/IFluentApi.cs deleted file mode 100644 index 3205566d23fe10946a9741c7ecedf8b7ca5a6d32..0000000000000000000000000000000000000000 --- a/FitConnect/Interfaces/IFluentApi.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using FitConnect.Models; - -namespace FitConnect; - -public interface IFluentSender { - /// <summary> - /// Configures the client for the given destination and loads the public key - /// </summary> - /// <param name="destinationId">unique identifier of the clients destination</param> - /// <returns>the upload step for attachments</returns> - public IFluentSenderWithDestination WithDestination(string destinationId); - - public IFluentSenderWithData WithData(string data); - - /// <summary> - /// Send submission to FIT-Connect API. - /// </summary> - /// <returns>submitted submission</returns> - public Submission Submit(); -} - -public interface IFluentSenderWithDestination { - /// <summary> - /// Sends the submission with a list of attachments - /// </summary> - /// <param name="attachments">that are sent with the submission</param> - /// <returns>the step where additional data can be added to the submission</returns> - public IFluentSenderWithAttachments WithAttachments(IEnumerable<Attachment> attachments); -} - -public interface IFluentSenderWithAttachments : IFluentSenderReady { - /// <summary> - /// Data as string. - /// </summary> - /// <param name="data">json or xml as string</param> - /// <returns>next step to submit the data</returns> - public IFluentSenderWithData WithData(string data); -} - -public interface IFluentSenderWithData : IFluentSenderReady { -} - -public interface IFluentSenderReady { - /// <summary> - /// Send submission to FIT-Connect API. - /// </summary> - /// <returns>submitted submission</returns> - public IFluentSender Submit(); -} - -public interface IFluentSubscriber { - /// <summary> - /// Loads a list of available Submissions that were submitted to the subscriber. - /// </summary> - /// <returns>List of available submissions for pickup</returns> - public IEnumerable<string> GetAvailableSubmissions(string? destinationId = null); - - /// <summary> - /// Loads a single Submission by id. - /// </summary> - /// <param name="submissionId">unique identifier of a <see cref="Submission"/></param> - /// <returns></returns> - public IFluentSubscriberWithSubmission RequestSubmission(string submissionId); -} - -public interface IFluentSubscriberWithSubmission { - public Submission Submission { get; } - - /// <summary> - /// Loads the <see cref="Attachment"/>s for the given <see cref="Submission"/>. - /// </summary> - /// <param name="canSubmitSubmission">Function that returns a boolean if the <see cref="Submission"/> can be confirmed</param> - /// <returns></returns> - public IFluentSenderWithAttachments GetAttachments( - Func<IEnumerable<Attachment>, bool> canSubmitSubmission); -} diff --git a/FitConnect/Interfaces/ISender.cs b/FitConnect/Interfaces/ISender.cs deleted file mode 100644 index ed47036ac5e3501093bb6a180658c06e74e3ffe3..0000000000000000000000000000000000000000 --- a/FitConnect/Interfaces/ISender.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; - -namespace FitConnect; - -public interface ISender : IBaseFunctionality { - // Check public keys - Task<bool> CheckPublicKeyAsync(string publicKey); - - // Encrypt Data (Fachdaten) - byte[] EncryptDataAsync(string data, string publicKey); - - // Encrypt attachments (Anhänge) - Task<string> EncryptAttachmentAsync(string attachment, string publicKey); - - // Create Metadata incl. Hash - Task<string> CreateMetadataAsync(string data, string attachment, string publicKey); -} \ No newline at end of file diff --git a/FitConnect/Interfaces/ISubscriber.cs b/FitConnect/Interfaces/ISubscriber.cs deleted file mode 100644 index 6456dab3cceef9c0b81a6a15d5f0df22fab547b6..0000000000000000000000000000000000000000 --- a/FitConnect/Interfaces/ISubscriber.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading.Tasks; -using FitConnect.Models; - -namespace FitConnect; - -public interface ISubscriber : IBaseFunctionality { - /// <summary> - /// Decrypt Data (Fachdaten) - /// </summary> - /// <param name="data">(Fachdaten)</param> - /// <param name="privateKey">Your private key for decryption</param> - /// <returns></returns> - Task<string> DecryptDataAsync(string data, string privateKey); - - // Decrypt attachments (Anhänge) - Task<string> DecryptAttachmentAsync(string attachment, string privateKey); - - /// <summary> - /// Checks the validity of the given metadata against the schema. - /// </summary> - /// <param name="jsonMetaData">JSON meta data</param> - /// <returns></returns> - Task<bool> CheckMetadataAsync(string jsonMetaData); - - // Check Hash from Metadata - Task<bool> CheckHashAsync(string metadata); - - // Create SecurityEventToken and signature - Task<SecurityEventToken> CreateSecurityEventTokenAsync(string data, string attachment, - string privateKey); -} \ No newline at end of file diff --git a/FitConnect/Interfaces/Sender/ISender.cs b/FitConnect/Interfaces/Sender/ISender.cs new file mode 100644 index 0000000000000000000000000000000000000000..2e6824ef707adb7cf146490925244910d6b656d7 --- /dev/null +++ b/FitConnect/Interfaces/Sender/ISender.cs @@ -0,0 +1,46 @@ +using FitConnect.Interfaces.Subscriber; +using FitConnect.Models; +using FitConnect.Services.Interfaces; + +namespace FitConnect.Interfaces.Sender; + +public interface ISender : IFitConnectClient { + public string PublicKey { get; } + + /// <summary> + /// Determines the destination id and configures the sender. + /// </summary> + /// <param name="leiaKey"></param> + /// <param name="ags"></param> + /// <param name="ars"></param> + /// <param name="areaId"></param> + /// <returns></returns> + public ISenderWithDestination FindDestinationId(string leiaKey, string? ags = null, + string? ars = null, + string? areaId = null); + + /// <summary> + /// Configures the client for the given destination and loads the public key + /// </summary> + /// <param name="destinationId"> + /// unique identifier of the clients destination + /// <para>eg: 00000000-0000-0000-0000-000000000000</para> + /// </param> + /// <returns>the upload step for attachments</returns> + public ISenderWithDestination WithDestination(string destinationId); + + + /// <summary> + /// Finding areas for the filter with paging + /// </summary> + /// <param name="filter">Search string for the area, use * as wildcard</param> + /// <param name="totalCount">out var for the total count</param> + /// <param name="offset"></param> + /// <param name="limit"></param> + /// <example> + /// var areas = GetAreas("Erlang*", out var _, 0, 10) + /// </example> + /// <returns></returns> + public IEnumerable<Area> GetAreas(string filter, out int totalCount, int offset = 0, + int limit = 100); +} diff --git a/FitConnect/Interfaces/Sender/ISenderReady.cs b/FitConnect/Interfaces/Sender/ISenderReady.cs new file mode 100644 index 0000000000000000000000000000000000000000..d51808023ec1b33de2e957d674adb37db27c57d2 --- /dev/null +++ b/FitConnect/Interfaces/Sender/ISenderReady.cs @@ -0,0 +1,10 @@ +using FitConnect.Models; + +namespace FitConnect.Interfaces.Sender; + +public interface ISenderReady { + /// <summary> + /// Send submission to FIT-Connect API. + /// </summary> + public Submission Submit(); +} diff --git a/FitConnect/Interfaces/Sender/ISenderWithAttachments.cs b/FitConnect/Interfaces/Sender/ISenderWithAttachments.cs new file mode 100644 index 0000000000000000000000000000000000000000..073048275129459bf5485962d2759d684ae4e71c --- /dev/null +++ b/FitConnect/Interfaces/Sender/ISenderWithAttachments.cs @@ -0,0 +1,17 @@ +using FitConnect.Models; + +namespace FitConnect.Interfaces.Sender; + +public interface ISenderWithAttachments : ISenderReady { + public Submission Submission { get; } + + /// <summary> + /// Data as string. + /// </summary> + /// <param name="data">json or xml as string</param> + /// <example> + /// .WithData(@"{ ""name"": ""John Doe"" }") + /// </example> + /// <returns>next step to submit the data</returns> + public ISenderWithData WithData(string data); +} diff --git a/FitConnect/Interfaces/Sender/ISenderWithData.cs b/FitConnect/Interfaces/Sender/ISenderWithData.cs new file mode 100644 index 0000000000000000000000000000000000000000..567bf942ded1cbb23a9400aa107934010ba71c44 --- /dev/null +++ b/FitConnect/Interfaces/Sender/ISenderWithData.cs @@ -0,0 +1,4 @@ +namespace FitConnect.Interfaces.Sender; + +public interface ISenderWithData : ISenderReady { +} diff --git a/FitConnect/Interfaces/Sender/ISenderWithDestination.cs b/FitConnect/Interfaces/Sender/ISenderWithDestination.cs new file mode 100644 index 0000000000000000000000000000000000000000..247da7e20f18b05b360ddff2d01e7aca8881ebf8 --- /dev/null +++ b/FitConnect/Interfaces/Sender/ISenderWithDestination.cs @@ -0,0 +1,18 @@ +namespace FitConnect.Interfaces.Sender; + +public interface ISenderWithDestination { + /// <summary> + /// Adds the service description to the sender. + /// </summary> + /// <param name="serviceName">Name of the destination service</param> + /// <param name="leikaKey"> + /// Leika key to specify the purpose of the submission + /// <para> + /// eg: urn:de:fim:leika:leistung:00000000000000 + /// </para> + /// </param> + /// <returns></returns> + ISenderWithService WithServiceType(string serviceName, string leikaKey); + + +} diff --git a/FitConnect/Interfaces/Sender/ISenderWithService.cs b/FitConnect/Interfaces/Sender/ISenderWithService.cs new file mode 100644 index 0000000000000000000000000000000000000000..e86042f50a9534fa7f464d2511b0b01463b33b22 --- /dev/null +++ b/FitConnect/Interfaces/Sender/ISenderWithService.cs @@ -0,0 +1,21 @@ +using FitConnect.Models; + +namespace FitConnect.Interfaces.Sender; + +public interface ISenderWithService { + public string PublicKey { get; set; } + + /// <summary> + /// Sends the submission with a list of attachments + /// </summary> + /// <param name="attachments">that are sent with the submission</param> + /// <returns>the step where additional data can be added to the submission</returns> + public ISenderWithAttachments WithAttachments(IEnumerable<Attachment> attachments); + + /// <summary> + /// Sends the submission with a list of attachments + /// </summary> + /// <param name="attachments">that are sent with the submission</param> + /// <returns>the step where additional data can be added to the submission</returns> + public ISenderWithAttachments WithAttachments(params Attachment[] attachments); +} diff --git a/FitConnect/Interfaces/Subscriber/ISubscriber.cs b/FitConnect/Interfaces/Subscriber/ISubscriber.cs new file mode 100644 index 0000000000000000000000000000000000000000..d782ef47c4b05b780eeb52420a295704f3418c5f --- /dev/null +++ b/FitConnect/Interfaces/Subscriber/ISubscriber.cs @@ -0,0 +1,36 @@ +using FitConnect.Models; +using FitConnect.Services.Models.v1.Submission; + +namespace FitConnect.Interfaces.Subscriber; + +public interface ISubscriber : IFitConnectClient { + /// <summary> + /// Loads a list of available Submissions that were submitted to the subscriber. + /// </summary> + /// <param name="destinationId">unique identifier of the destination</param> + /// <param name="offset">the offset of the list of submissions</param> + /// <param name="limit">the limit of the list of submissions</param> + /// <returns>List of available submissions for pickup</returns> + public IEnumerable<SubmissionForPickupDto> GetAvailableSubmissions(string? destinationId = null, + int offset = 0, int limit = 100); + + /// <summary> + /// Loads a single Submission by id. + /// </summary> + /// <param name="submissionId">unique identifier of a <see cref="Submission" /></param> + /// <param name="skipSchemaTest"></param> + /// <returns>A subscriber object with a submission</returns> + public ISubscriberWithSubmission RequestSubmission(string? submissionId, + bool skipSchemaTest = false); +} + +public interface IFitConnectClient{ + +/// <summary> + /// Receives the SecurityEventTokens for a given case + /// </summary> + /// <param name="caseId">ID of the case</param> + /// <returns>List of the <see cref="SecurityEventToken"/></returns> + public List<SecurityEventToken> GetStatusForSubmission(string caseId); + +} diff --git a/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs b/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs new file mode 100644 index 0000000000000000000000000000000000000000..dccc20a60a95dae09317486f5e4ce854d7d64258 --- /dev/null +++ b/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs @@ -0,0 +1,43 @@ +using FitConnect.Models; +using FitConnect.Models.v1.Api; +using Data = FitConnect.Models.Data; + +namespace FitConnect.Interfaces.Subscriber; + +public interface ISubscriberWithSubmission { + public Submission Submission { get; } + + /// <summary> + /// Returns the data (Fachdaten) of the submission + /// </summary> + /// <returns></returns> + public string? GetDataJson() => Submission.Data; + + /// <summary> + /// Loads the <see cref="Attachment" />s for the given <see cref="Submission" />. + /// </summary> + /// <returns>Enumerable of the attachments</returns> + public IEnumerable<Attachment> GetAttachments(); + + /// <summary> + /// Accept submission and delete it from the server + /// </summary> + public void AcceptSubmission(); + + /// <summary> + /// Rejects the submission + /// </summary> + /// <param name="problems">Reasons for the rejection</param> + public void RejectSubmission(params Problems[] problems); + + /// <summary> + /// Set submission state to forwarded + /// </summary> + public void ForwardSubmission(); + + /// <summary> + /// Set submission state + /// </summary> + /// <param name="status">state the submission has to be set to</param> + public void CompleteSubmission(FinishSubmissionStatus status); +} diff --git a/FitConnect/Models/Area.cs b/FitConnect/Models/Area.cs index 71215cc66b6cdcad6252b9b5a102c0d93d836386..d02e2b530240f66483f1720416af2939cbc79052 100644 --- a/FitConnect/Models/Area.cs +++ b/FitConnect/Models/Area.cs @@ -1,3 +1,29 @@ +using System.Text.Json.Serialization; + namespace FitConnect.Models; -public record Area(string Id, string Name, string Type); +// Root myDeserializedClass = JsonSerializer.Deserialize<Root>(myJsonResponse); +public class Area { + [JsonPropertyName("id")] + public string Id { get; set; } = ""; + + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("type")] + public string Type { get; set; } = ""; +} + +public class AreaList { + [JsonPropertyName("count")] + public int Count { get; set; } + + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("totalCount")] + public int TotalCount { get; set; } + + [JsonPropertyName("areas")] + public List<Area> Areas { get; set; } = new(); +} diff --git a/FitConnect/Models/Attachment.cs b/FitConnect/Models/Attachment.cs index 2c1e5da5761e07a7d46931324df81c8fc778d1f0..c33d21d4867987ef6987e69ec08a5ffc90251066 100644 --- a/FitConnect/Models/Attachment.cs +++ b/FitConnect/Models/Attachment.cs @@ -1,3 +1,69 @@ +using System.Security.Cryptography; +using System.Text; +using FitConnect.Models.Api.Metadata; + namespace FitConnect.Models; -public record Attachment(string Id, byte[] Content, string Hash, string Filename); +public class Attachment { + public Attachment(Api.Metadata.Attachment metadata, byte[] content) { + Filename = metadata.Filename; + Content = content; + MimeType = Path.GetExtension(metadata.Filename) switch { + "pdf" => "application/pdf", + "xml" => "application/xml", + _ => "" + }; + } + + /// <summary> + /// Attachment for a submission. + /// </summary> + /// <param name="filename">File name to load</param> + /// <param name="description">Description of the attachment</param> + public Attachment(string filename, string description) { + var file = new FileInfo(filename); + Filename = filename; + Content = File.ReadAllBytes(filename); + var extension = file.Extension[1..].ToLower(); + MimeType = "application/" + extension; + Description = description; + + + Signature = new AttachmentSignature { + DetachedSignature = true, + SignatureFormat = extension switch { + "pdf" => SignatureFormat.Pdf, + "xml" => SignatureFormat.Xml, + "json" => SignatureFormat.Json, + _ => throw new ArgumentOutOfRangeException() + } + }; + } + + public string Id { get; } = Guid.NewGuid().ToString(); + + public byte[]? Content { get; init; } + + public string? Hash => CalculateHash(); + + public string? Filename { get; init; } + + public string? Purpose { get; init; } = "attachment"; + + public string MimeType { get; init; } + + public string? Description { get; init; } + + public AttachmentSignature? Signature { get; } + + private string CalculateHash() { + return ByteToHexString(SHA512.Create().ComputeHash(Content)); + } + + private static string ByteToHexString(IEnumerable<byte> data) { + var sb = new StringBuilder(); + foreach (var b in data) sb.Append(b.ToString("x2")); + + return sb.ToString(); + } +} diff --git a/FitConnect/Models/Callback.cs b/FitConnect/Models/Callback.cs index cd99183d815dd08497737a0749127d1aee6e2a37..b4d7e94fb7bd1cca3711e9ab5ed288d016b870aa 100644 --- a/FitConnect/Models/Callback.cs +++ b/FitConnect/Models/Callback.cs @@ -1,6 +1,13 @@ - +using FitConnect.Services.Models; namespace FitConnect.Models; public record Callback(string? Url, string? Secret) { - } + public static explicit operator Callback(CallbackDto dto) { + return new Callback(dto?.Url, null); + } + + public static explicit operator CallbackDto(Callback model) { + return new CallbackDto { Url = model.Url }; + } +} diff --git a/FitConnect/Models/Destination.cs b/FitConnect/Models/Destination.cs new file mode 100644 index 0000000000000000000000000000000000000000..7230666d2695cb586c0e39d804a357564872e304 --- /dev/null +++ b/FitConnect/Models/Destination.cs @@ -0,0 +1,14 @@ +namespace FitConnect.Models; + +public class Destination { + public string? LeikaKey { get; set; } + public string? Ags { get; set; } + public string? Ars { get; set; } + public string? AreaId { get; set; } + public string? DestinationId { get; set; } + + public bool Valid => !((string.IsNullOrWhiteSpace(Ags) + && string.IsNullOrWhiteSpace(Ars) + && string.IsNullOrWhiteSpace(AreaId)) || + string.IsNullOrWhiteSpace(LeikaKey)); +} diff --git a/FitConnect/Models/FitConnectEnvironment.cs b/FitConnect/Models/FitConnectEnvironment.cs new file mode 100644 index 0000000000000000000000000000000000000000..6bd051ba110b9b27058a613703653896f203ad1f --- /dev/null +++ b/FitConnect/Models/FitConnectEnvironment.cs @@ -0,0 +1,74 @@ +namespace FitConnect.Models; + +// public enum FitConnectEnvironment { +// Development, +// Testing, +// Production +// } + +public class FitConnectEnvironment { + public static readonly FitConnectEnvironment Testing = new( + "https://auth-testing.fit-connect.fitko.dev/token", + new[] { "https://submission-api-testing.fit-connect.fitko.dev" }, + "https://routing-api-testing.fit-connect.fitko.dev" + ); + + public static readonly FitConnectEnvironment Staging = new( + "https://auth-refz.fit-connect.fitko.dev/token", + new[] { "https://submission-api-refz.fit-connect.fitko.dev" }, + "https://routing-api-refz.fit-connect.fitko.dev" + ); + + public static readonly FitConnectEnvironment Production = new( + "https://auth.fit-connect.fitko.net/token", + new[] { "https://submission-api.fit-connect.fitko.net" }, + "https://routing-api.fit-connect.fitko.net" + ); + + public FitConnectEnvironment() { + } + + /// <summary> + /// Default constructor. + /// </summary> + /// <param name="tokenUrl">URL for receiving the OAuth token</param> + /// <param name="submissionUrl">URL for the submission API</param> + /// <param name="routingUrl">URL for the routing API</param> + public FitConnectEnvironment(string tokenUrl, string[] submissionUrl, string routingUrl) { + TokenUrl = tokenUrl; + SubmissionUrl = submissionUrl; + RoutingUrl = routingUrl; + } + + /// <summary> + /// URL for receiving the OAuth token. + /// </summary> + public string TokenUrl { get; } + + /// <summary> + /// URL for the submission API. + /// </summary> + public string[] SubmissionUrl { get; } + + /// <summary> + /// URL for the routing API. + /// </summary> + public string RoutingUrl { get; } + + /// <summary> + /// Creates the endpoints for the given environment. + /// </summary> + /// <param name="fitConnectEnvironment">Environment to get endpoints for</param> + /// <returns></returns> + /// <exception cref="ArgumentException">Not all environments are ready to use</exception> + // public static FitConnectEnvironment Create(FitConnectEnvironment fitConnectEnvironment) { + // return fitConnectEnvironment switch { + // FitConnectEnvironment.Development => DevEnvironment, + // FitConnectEnvironment.Testing => throw new ArgumentException( + // "Not approved for online testing"), + // FitConnectEnvironment.Production => throw new ArgumentException("NOT PRODUCTION READY"), + // _ => throw new ArgumentOutOfRangeException(nameof(fitConnectEnvironment), + // fitConnectEnvironment, null) + // }; + // } +} diff --git a/FitConnect/Models/FitConnectException.cs b/FitConnect/Models/FitConnectException.cs index fbd8c908b35a8e5fe7bdeb29912b2382866b61ce..91ca6c592ae3525804d42abe2e2e3a576ab3173a 100644 --- a/FitConnect/Models/FitConnectException.cs +++ b/FitConnect/Models/FitConnectException.cs @@ -1,5 +1,3 @@ -using System; - namespace FitConnect.Models; /// <summary> diff --git a/FitConnect/Models/Metadata.cs b/FitConnect/Models/Metadata.cs index e5268ab7532bf0baedcb5ea7832dd4039884641d..796635939ac23c46adb0a63aca332bec770408be 100644 --- a/FitConnect/Models/Metadata.cs +++ b/FitConnect/Models/Metadata.cs @@ -1,9 +1,12 @@ -using System.Security.Cryptography.X509Certificates; +using Newtonsoft.Json; namespace FitConnect.Models; -public class Metadata { +public class Metadata : Api.Metadata.Metadata { } public class Data { + public static Data? FromString(string data) { + return JsonConvert.DeserializeObject<Data>(data); + } } diff --git a/FitConnect/Models/SecurityEventToken.cs b/FitConnect/Models/SecurityEventToken.cs index 88a700524c1c8270c9d06685d8e0552cafb0345d..815c9cd344fbc3afb1229270015366b20d266c62 100644 --- a/FitConnect/Models/SecurityEventToken.cs +++ b/FitConnect/Models/SecurityEventToken.cs @@ -1,3 +1,82 @@ +using System.Security.Claims; +using FitConnect.Models.v1.Api; +using Microsoft.IdentityModel.JsonWebTokens; +using Newtonsoft.Json; + namespace FitConnect.Models; -public record SecurityEventToken; +public enum EventType { + NotSet, + Create, + Submit, + Notify, + Forward, + Reject, + Accept, + Delete +} + +public class SecurityEventToken { + public const string CreateSubmissionSchema = "https://schema.fitko.de/fit-connect/events/create-submission"; + public const string SubmitSubmissionSchema = "https://schema.fitko.de/fit-connect/events/submit-submission"; + public const string NotifySubmissionSchema = "https://schema.fitko.de/fit-connect/events/notify-submission"; + public const string ForwardSubmissionSchema = "https://schema.fitko.de/fit-connect/events/forward-submission"; + public const string RejectSubmissionSchema = "https://schema.fitko.de/fit-connect/events/reject-submission"; + public const string AcceptSubmissionSchema = "https://schema.fitko.de/fit-connect/events/accept-submission"; + public const string DeleteSubmissionSchema = "https://schema.fitko.de/fit-connect/events/delete-submission"; + + public SecurityEventToken(string jwtEncodedString) { + Token = new JsonWebToken(jwtEncodedString); + EventType = DecodeEventType(Token.Claims); + var iat = Token.Claims.FirstOrDefault(c => c.Type == "iat").Value; + if (long.TryParse(iat, out var timeEpoch)) + EventTime = DateTime.UnixEpoch.AddSeconds(timeEpoch); + + // TODO Fill problems + } + + public DateTime EventTime { get; set; } + public EventType EventType { get; set; } + public List<Problems>? Problems { get; set; } + public Events? Event { get; set; } + + public object? Payload { get; set; } + public JsonWebToken Token { get; set; } + + private EventType DecodeEventType(IEnumerable<Claim> claims) { + var eventsClaim = claims.FirstOrDefault(c => c.Type == "events"); + if (eventsClaim == null) return EventType.NotSet; + + var events = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(eventsClaim.Value); + Payload = JsonConvert + .DeserializeObject<dynamic>( + events.Values.FirstOrDefault().ToString()); + + + if (eventsClaim.Value.Contains( + CreateSubmissionSchema)) + return EventType.Create; + + if (eventsClaim.Value.Contains( + SubmitSubmissionSchema)) + return EventType.Submit; + + if (eventsClaim.Value.Contains( + NotifySubmissionSchema)) + return EventType.Notify; + if (eventsClaim.Value.Contains( + ForwardSubmissionSchema)) + return EventType.Forward; + if (eventsClaim.Value.Contains( + RejectSubmissionSchema)) + return EventType.Reject; + if (eventsClaim.Value.Contains( + AcceptSubmissionSchema)) + return EventType.Accept; + if (eventsClaim.Value.Contains( + DeleteSubmissionSchema)) + return EventType.Delete; + + return EventType.NotSet; + } +} diff --git a/FitConnect/Models/ServiceType.cs b/FitConnect/Models/ServiceType.cs index 1de00dd23cfbe49e8c1f9adeebef196b72a394ec..270aac944633df1257cd4889be64e8c5b211b312 100644 --- a/FitConnect/Models/ServiceType.cs +++ b/FitConnect/Models/ServiceType.cs @@ -1,4 +1,4 @@ - +using FitConnect.Services.Models; namespace FitConnect.Models; @@ -8,6 +8,21 @@ public class ServiceType { public string? Description { get; set; } public string? Identifier { get; set; } + public static explicit operator ServiceType(ServiceTypeDto dto) { + return new ServiceType { + Description = dto.Description, + Identifier = dto.Identifier, + Name = dto.Name + }; + } + + public static explicit operator ServiceTypeDto(ServiceType model) { + return new ServiceTypeDto { + Description = model.Description, + Identifier = model.Identifier, + Name = model.Name + }; + } public bool IsValid() { return !string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Identifier); diff --git a/FitConnect/Models/Submission.cs b/FitConnect/Models/Submission.cs index a6f18fcb3aed374406c1df5a7833a71040b8635d..63bc466e4cabc6fbb950150fd37cf6d2afea16e7 100644 --- a/FitConnect/Models/Submission.cs +++ b/FitConnect/Models/Submission.cs @@ -1,26 +1,12 @@ - - -using System.Collections.Generic; +using FitConnect.Services.Models; +using FitConnect.Services.Models.v1.Submission; namespace FitConnect.Models; -public class Destination { - public string LeikaKey { get; set; } - public string? Ags { get; set; } - public string? Ars { get; set; } - public string? AreaId { get; set; } - public string DestinationId { get; set; } - - public bool Valid => !((string.IsNullOrWhiteSpace(Ags) - && string.IsNullOrWhiteSpace(Ars) - && string.IsNullOrWhiteSpace(AreaId)) || - string.IsNullOrWhiteSpace(LeikaKey)); -} - public class Submission { - public string Id { get; set; } + public string? Id { get; set; } public string? CaseId { get; set; } - public Destination Destination { get; set; } = new Destination(); + public Destination? Destination { get; set; } = new(); public string DestinationId { get => Destination.DestinationId; @@ -28,14 +14,15 @@ public class Submission { } public List<Attachment> Attachments { get; set; } = new(); + public List<string> AttachmentIds { get; set; } = new(); - public ServiceType ServiceType { get; init; } + public ServiceType ServiceType { get; set; } = new(); public Callback? Callback { get; set; } - public Metadata? Metadata { get; set; } - public Data? Data { get; set; } - public string EncryptedMetadata { get; set; } - public string EncryptedData { get; set; } + public Api.Metadata.Metadata? Metadata { get; set; } + public string? Data { get; set; } + public string? EncryptedMetadata { get; set; } + public string? EncryptedData { get; set; } public bool IsSubmissionReadyToAdd(out string? error) { var innerError = ""; @@ -56,4 +43,52 @@ public class Submission { return true; } + public static implicit operator SubmissionForPickupDto(Submission sub) { + return new SubmissionForPickupDto { + SubmissionId = sub.Id, + CaseId = sub.CaseId, + DestinationId = sub.DestinationId + }; + } + + public static explicit operator Submission(SubmissionForPickupDto dto) { + return new Submission { + Id = dto.SubmissionId, + Callback = null, + DestinationId = dto.DestinationId, + ServiceType = null + }; + } + + public static explicit operator Submission(SubmissionDto dto) { + return new Submission { + Id = dto.SubmissionId, + Callback = (Callback)dto.Callback, + DestinationId = dto.DestinationId, + ServiceType = (ServiceType)dto.ServiceType, + EncryptedData = dto.EncryptedData, + EncryptedMetadata = dto.EncryptedMetadata, + AttachmentIds = dto.Attachments, + CaseId = dto.CaseId + }; + } + + public static explicit operator SubmitSubmissionDto(Submission dto) { + return new SubmitSubmissionDto { + EncryptedData = dto.EncryptedData, + EncryptedMetadata = dto.EncryptedMetadata + }; + } + + public static explicit operator CreateSubmissionDto(Submission sub) { + var result = new CreateSubmissionDto { + AnnouncedAttachments = sub.Attachments.Select(a => a.Id).ToList(), + DestinationId = sub.DestinationId + }; + if (sub.ServiceType != null) + result.ServiceType = (ServiceTypeDto)sub.ServiceType; + if (sub.Callback != null) result.Callback = (CallbackDto)sub.Callback; + + return result; + } } diff --git a/FitConnect/Security/IEncryption.cs b/FitConnect/Security/IEncryption.cs deleted file mode 100644 index 92bf328814bab3acb703c1c0bc28d018a5d6302a..0000000000000000000000000000000000000000 --- a/FitConnect/Security/IEncryption.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Security.Cryptography.X509Certificates; - -namespace FitConnect.Security; - -public interface IEncryption { - /// <summary> - /// Just for Proof of Concept - /// </summary> - /// <returns></returns> - string GetTestToken(); - - void ImportCertificate(X509Certificate2 cert); - - /// <summary> - /// Import a public key from a PEM file - /// </summary> - /// <param name="certificatePath"></param> - /// <param name="password">Password for the certificate</param> - /// <exception cref="ArgumentException"></exception> - /// <exception cref="Exception"></exception> - void ImportCertificate(string certificatePath, string password); - - byte[] DecryptData(byte[] data); - string DecryptData(string data); - - byte[] ExportPublicKey(); - byte[] ExportPrivateKey(); - byte[] EncryptData(byte[] data); - byte[] EncryptData(byte[] data, byte[] publicKey); -} diff --git a/FitConnect/Security/RsaEncryption.cs b/FitConnect/Security/RsaEncryption.cs deleted file mode 100644 index 570622b28626b12b6e9b7a45e7f817a9265be2cc..0000000000000000000000000000000000000000 --- a/FitConnect/Security/RsaEncryption.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.IdentityModel.Tokens.Jwt; -using System.IO; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; - -namespace FitConnect.Security; - -public class RsaEncryption : IEncryption { - private readonly X509Certificate2? _certificate; - private readonly ILogger? _logger; - private readonly RSA _rsa; - private RSA? _privateKey; - private RSA? _publicKey; - - - public RsaEncryption(X509Certificate2? certificate, ILogger? logger) { - _logger = logger; - _rsa = RSA.Create(4096); - - if (certificate != null) { - _certificate = certificate; - ImportCertificate(certificate); - } - } - - - /// <summary> - /// Just for Proof of Concept - /// </summary> - /// <returns></returns> - public string GetTestToken() { - var handler = new JwtSecurityTokenHandler(); - var token = new SecurityTokenDescriptor { - Issuer = "FitConnect", - Audience = "FitConnect", - EncryptingCredentials = - new X509EncryptingCredentials(_certificate ?? - new X509Certificate2(CreateSelfSignedCertificate())), - Subject = new ClaimsIdentity(new Claim[] { - new("Content", "Unencrypted content") - }) - }; - return handler.CreateEncodedJwt(token); - } - - - public void ImportCertificate(X509Certificate2 cert) { - if (!CheckCertificate(cert)) throw new Exception("Invalid certificate"); - - _publicKey = cert.GetRSAPublicKey(); - - if ((_publicKey?.KeySize ?? 0) == 0) - throw new Exception("Invalid certificate, no public key"); - - if (cert.HasPrivateKey) _privateKey = cert.GetRSAPrivateKey(); - - if (_privateKey != null) - _logger?.LogInformation("Certificate with private key imported"); - else - _logger?.LogInformation("Certificate has no private key"); - } - - /// <summary> - /// Import a public key from a PEM file - /// </summary> - /// <param name="certificatePath"></param> - /// <param name="password">Password for the certificate</param> - /// <exception cref="ArgumentException"></exception> - /// <exception cref="Exception"></exception> - public void ImportCertificate(string certificatePath, string password) { - _logger?.LogInformation("Importing certificate {CertPath}", certificatePath); - var cert = - new X509Certificate2(certificatePath, password, X509KeyStorageFlags.MachineKeySet); - - if (!CheckCertificate(cert)) throw new ArgumentException("Certificate is not valid"); - - var parameters = cert.PublicKey.GetRSAPublicKey()?.ExportParameters(true); - if (parameters == null) throw new Exception("Could not get public key from certificate"); - - _rsa.ImportParameters(parameters.Value); - } - - public byte[] DecryptData(byte[] data) { - return (_privateKey ?? _rsa).Decrypt(data, RSAEncryptionPadding.OaepSHA256); - } - - public string DecryptData(string data) { - return Encoding.UTF8.GetString(DecryptData(Convert.FromBase64String(data))); - } - - public byte[] ExportPublicKey() { - return _rsa.ExportRSAPublicKey(); - } - - public byte[] ExportPrivateKey() { - return _rsa.ExportRSAPrivateKey(); - } - - public byte[] EncryptData(byte[] data) { - return (_publicKey ?? _rsa).Encrypt(data, RSAEncryptionPadding.OaepSHA256); - } - - public byte[] EncryptData(byte[] data, byte[] publicKey) { - _logger?.LogInformation( - "Encrypting data with public key: {}", - Convert.ToBase64String(_rsa.ExportRSAPublicKey())); - - _rsa.ImportRSAPublicKey(publicKey, out var read); - return _rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256); - } - - /// <summary> - /// Creating a self signed certificate - /// </summary> - /// <param name="exportPath">Location for storing the certificate files</param> - /// <returns></returns> - /// <exception cref="Exception"></exception> - public static X509Certificate2 CreateSelfSignedCertificate(string? exportPath = null) { - var rsa = RSA.Create(4096); - - var req = new CertificateRequest("c=DE, cn=fitconnect.de", - rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - var cert = req.CreateSelfSigned(DateTimeOffset.Now.AddSeconds(-5), - DateTimeOffset.Now.AddYears(5)); - - if (cert.GetRSAPublicKey() == null) - throw new Exception("Certificate does not contain a public key"); - - if (cert.GetRSAPrivateKey() == null) - throw new Exception("Certificate does not contain a private key"); - - // Export the certificate to a PEM file, just for - // additional external testing - if (exportPath != null) ExportCertificateToFile(exportPath, cert); - - return cert; - } - - private static void ExportCertificateToFile(string exportPath, X509Certificate cert) { - // Create PFX (PKCS #12) with private key - File.WriteAllBytes($"{exportPath}/certificate.pfx", - cert.Export(X509ContentType.Pfx, "")); - - // Create Base 64 encoded CER (public key only) - File.WriteAllText($"{exportPath}/certificate.cer", - "-----BEGIN CERTIFICATE-----\r\n" - + Convert.ToBase64String(cert.Export(X509ContentType.Cert, ""), - Base64FormattingOptions.InsertLineBreaks) - + "\r\n-----END CERTIFICATE-----"); - } - - - /// <summary> - /// Checking the certificate for validity - /// </summary> - /// <param name="cert">Certificate to check</param> - /// <returns>true for a fully approved certificate</returns> - private bool CheckCertificate(X509Certificate2 cert) { - return true; - } -} diff --git a/FitConnect/Sender.cs b/FitConnect/Sender.cs new file mode 100644 index 0000000000000000000000000000000000000000..f56fe72f6b981dcceda3dbaa364321c85b64086b --- /dev/null +++ b/FitConnect/Sender.cs @@ -0,0 +1,247 @@ +using System.Text.RegularExpressions; +using Autofac; +using FitConnect.Encryption; +using FitConnect.Interfaces.Sender; +using FitConnect.Models; +using FitConnect.Models.Api.Metadata; +using FitConnect.Services.Models.v1.Submission; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Attachment = FitConnect.Models.Attachment; +using Data = FitConnect.Models.Api.Metadata.Data; +using Metadata = FitConnect.Models.Metadata; + +namespace FitConnect; + +/// <summary> +/// The fluent implementation for the <see cref="Sender" /> to encapsulate the FitConnect API. +/// </summary> +/// <example> +/// Reference for FluentSender +/// <code> +/// client.Sender +/// .Authenticate(clientId!, clientSecret!) +/// .CreateSubmission(new Submission { Attachments = new List() }) +/// .UploadAttachments() +/// .WithData(data) +/// .Subm@it(); +/// </code> +/// </example> +public class Sender : FitConnectClient, ISender, ISenderWithDestination, + ISenderWithAttachments, ISenderWithData, ISenderWithService { + public Sender(FitConnectEnvironment environment, string clientId, string clientSecret, + ILogger? logger = null) : base(environment, clientId, clientSecret, logger) { + } + + public Sender(FitConnectEnvironment environment, string clientId, string clientSecret, + IContainer container) : base(environment, clientId, clientSecret, + container) { + } + + public string? PublicKey { get; set; } + + + public ISenderWithDestination FindDestinationId(string leiaKey, string? ags = null, + string? ars = null, + string? areaId = null) { + if (ags == null && ars == null && areaId == null) + throw new ArgumentException("One of the following must be provided: ags, ars, areaId"); + + var destinationId = RouteService.GetDestinationIdAsync(leiaKey, ags, ars, areaId).Result; + Logger?.LogInformation("Received destinations: {Destinations}", + destinationId.Select(d => d.DestinationId).Aggregate((a, b) => a + "," + b)); + return WithDestination(destinationId.First().DestinationId); + } + + public ISenderWithDestination WithDestination(string destinationId) { + if (!Regex.IsMatch(destinationId, + "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")) + throw new ArgumentException("The destination must be a valid GUID"); + + Submission = CreateSubmission(destinationId).Result; + return this; + } + + public ISenderWithData WithData(string data) { + try { + JsonConvert.DeserializeObject(data); + } + catch (Exception e) { + throw new ArgumentException("The data must be valid JSON string", e); + } + + + Submission!.Data = data; + return this; + } + + public Submission? Submission { get; set; } + + Submission ISenderReady.Submit() { + if (Submission == null) { + Logger?.LogCritical("Submission is null on submit"); + throw new InvalidOperationException("Submission is not ready"); + } + + var metadata = CreateMetadata(Submission); + Logger?.LogTrace("MetaData: {metadata}", metadata); + Logger?.LogInformation("Sending submission"); + var encryptedMeta = Encryption.Encrypt(metadata); + Logger?.LogTrace("Encrypted metadata: {encryptedMeta}", encryptedMeta); + Submission.EncryptedMetadata = encryptedMeta; + if (Submission.Data != null) + Submission.EncryptedData = Encryption.Encrypt(Submission.Data); + + var result = SubmissionService + .SubmitSubmission(Submission.Id!, (SubmitSubmissionDto)Submission).Result; + + Logger?.LogInformation("Submission sent"); + return Submission; + } + + + public ISenderWithService WithServiceType(string serviceName, string leikaKey) { + if (string.IsNullOrWhiteSpace(leikaKey) || !Regex.IsMatch(leikaKey, + "^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$")) { + Logger?.LogError("The leikaKey must be a valid URN"); + throw new ArgumentException("Invalid leika key"); + } + + Submission!.ServiceType = new ServiceType { + Name = serviceName, + Identifier = leikaKey + }; + return this; + } + + /// <summary> + /// + /// </summary> + /// <param name="attachments"></param> + /// <returns></returns> + public ISenderWithAttachments WithAttachments(params Attachment[] attachments) => + WithAttachments(attachments.ToList()); + + + /// <summary> + /// </summary> + /// <param name="attachments"></param> + /// <returns></returns> + /// <exception cref="InvalidOperationException"></exception> + /// <exception cref="ArgumentException"></exception> + public ISenderWithAttachments WithAttachments(IEnumerable<Attachment> attachments) { + Submission!.Attachments = new List<Attachment>(); + foreach (var attachment in attachments) Submission!.Attachments.Add(attachment); + + if (Submission.ServiceType == null) { + Logger?.LogError("Submission has no service type"); + throw new ArgumentException("Submission has no service type"); + } + + var created = SubmissionService.CreateSubmission((CreateSubmissionDto)Submission); + Submission.Id = created.SubmissionId; + Submission.CaseId = created.CaseId; + + Logger?.LogInformation("Submission Id {CreatedSubmissionId}, CaseId {SubmissionCaseId}", + created.SubmissionId, Submission.CaseId); + + var encryptedAttachments = Encrypt(PublicKey!, Submission.Attachments); + UploadAttachmentsAsync(Submission.Id!, encryptedAttachments).Wait(); + + return this; + } + + public ISenderWithDestination FindDestinationId(Destination destination) { + throw new NotImplementedException(); + } + + /// <summary> + /// Creates a new <see cref="Submission" /> on the FitConnect server. + /// </summary> + /// <param name="destinationId">The id of the destination the submission has to be sent to</param> + /// <returns></returns> + /// <exception cref="InvalidOperationException">If sender is not authenticated</exception> + /// <exception cref="ArgumentException">If submission is not ready to be sent</exception> + private async Task<Submission> CreateSubmission(string destinationId) { + PublicKey = await GetPublicKeyFromDestination(destinationId); + Encryption = new FitEncryption(Logger) { PublicKeyEncryption = PublicKey }; + + var submission = new Submission { + DestinationId = destinationId + }; + + return submission; + } + + private async Task<string> GetPublicKeyFromDestination(string destinationId) { + var publicKey = await DestinationService.GetPublicKey(destinationId); + return publicKey; + } + + /// <summary> + /// Create Metadata incl. Hash + /// </summary> + /// <param name="submission"></param> + /// <returns></returns> + public static string CreateMetadata(Submission submission) { + var data = new Data { + Hash = new DataHash { + Type = "sha512", + Content = FitEncryption.CalculateHash(submission.Data ?? "") + }, + + SubmissionSchema = new Fachdatenschema { + SchemaUri = submission.ServiceType.Identifier, + MimeType = "application/json" + } + }; + var contentStructure = new ContentStructure { + Data = data, + Attachments = submission.Attachments.Select(a => + new Models.Api.Metadata.Attachment { + Description = a.Description, + AttachmentId = a.Id, + MimeType = a.MimeType, + Filename = a.Filename, + Purpose = "attachment", + Hash = new AttachmentHash { + Type = "sha512", + Content = a.Hash + } + }).ToList() + }; + + var metaData = new Metadata { + ContentStructure = contentStructure + }; + return JsonConvert.SerializeObject(metaData); + } + + public IEnumerable<Area> GetAreas(string filter, out int totalCount, int offset = 0, + int limit = 100) { + var dto = RouteService.GetAreas(filter, offset, limit).Result; + totalCount = dto?.TotalCount ?? 0; + return dto?.Areas ?? new List<Area>(); + } + + /// <summary> + /// Uploading the encrypted data to the server + /// </summary> + /// <param name="submissionId">Submissions ID</param> + /// <param name="encryptedAttachments">Encrypted attachments with id and content</param> + private async Task<bool> UploadAttachmentsAsync(string submissionId, + Dictionary<string, string> encryptedAttachments) { + try { + foreach (var (id, content) in encryptedAttachments) { + Logger?.LogInformation("Uploading attachment {id}", id); + SubmissionService.UploadAttachment(submissionId, id, content); + } + + return true; + } + catch (Exception e) { + Logger?.LogError("Error Uploading attachment {message}", e.Message); + throw; + } + } +} diff --git a/FitConnect/Services/CasesService.cs b/FitConnect/Services/CasesService.cs new file mode 100644 index 0000000000000000000000000000000000000000..5012c6bde657c1faef9e8f8d3481f8d83761bf11 --- /dev/null +++ b/FitConnect/Services/CasesService.cs @@ -0,0 +1,50 @@ +using FitConnect.Services.Interfaces; +using FitConnect.Services.Models.v1.Case; +using Microsoft.Extensions.Logging; + +namespace FitConnect.Services; + +public class CasesService : RestCallService, ICasesService { + private readonly IOAuthService _oAuthService; + + public CasesService(string baseUrl, IOAuthService oAuthService, string version = "v1", + ILogger? logger = null) : base( + $"{baseUrl}/{version}", logger) { + _oAuthService = oAuthService; + } + + public string FinishSubmission(string caseId, string token) { + _oAuthService.EnsureAuthenticated(); + var result = RestCallForString($"/cases/{caseId}/events", HttpMethod.Post, token + , "application/jose" + ) + .Result; + + return result; + } + + + /// <summary> + /// <para> + /// @GetMapping("/cases/{caseId}/events") + /// </para> + /// </summary> + /// <param name="caseId">PathVariable</param> + /// <param name="offset">RequestParam</param> + /// <param name="limit">RequestParam</param> + /// <returns></returns> + public EventLogDto GetEventLog(string caseId, int offset, int limit) { + throw new NotImplementedException(); + } + + // + /// <summary> + /// <para>@PostMapping(value = "/cases/{caseId}/events", consumes = "application/jose")</para> + /// </summary> + /// <param name="caseId">PathVariable</param> + /// <param name="eventToken">RequestBody</param> + /// <returns></returns> + public bool ProcessCaseEvent(string caseId, string eventToken) { + throw new NotImplementedException(); + } +} diff --git a/FitConnect/Services/DestinationService.cs b/FitConnect/Services/DestinationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..dfe7b95357dc910501a82415094da74792b64169 --- /dev/null +++ b/FitConnect/Services/DestinationService.cs @@ -0,0 +1,97 @@ +using FitConnect.Services.Interfaces; +using FitConnect.Services.Models.v1.Destination; +using Microsoft.Extensions.Logging; + +namespace FitConnect.Services; + +public class DestinationService : RestCallService, IDestinationService { + private const string ApiVersion = "v1"; + private readonly IOAuthService _oAuthService; + + public DestinationService(string baseUrl, IOAuthService oAuthService, string version = "v1", + ILogger? logger = null) : base( + $"{baseUrl}/{version}", logger) { + _oAuthService = oAuthService; + } + + /// <summary> + /// <para>@PostMapping("/destinations")</para> + /// </summary> + /// <param name="createDestination">RequestBody</param> + /// <returns></returns> + public PrivateDestinationDto CreateDestination(CreateDestinationDto createDestination) { + throw new NotImplementedException(); + } + + + /// <summary> + /// <para>@GetMapping("/destinations/{destinationId}")</para> + /// </summary> + /// <param name="destinationId">PathVariable</param> + public void GetDestination(string destinationId) { + throw new NotImplementedException(); + } + + + /// <summary> + /// <para>@DeleteMapping("/destinations/{destinationId}")</para> + /// </summary> + /// <param name="destinationId">PathVariable</param> + /// <returns></returns> + public bool DeleteDestination(string destinationId) { + throw new NotImplementedException(); + } + + + /// <summary> + /// <para>@PatchMapping("/destinations/{destinationId}")</para> + /// </summary> + /// <param name="destinationId">PathVariable</param> + /// <param name="patchDestination">RequestBody</param> + /// <returns></returns> + public PrivateDestinationDto PatchDestination(string destinationId, + PatchDestinationDto patchDestination) { + throw new NotImplementedException(); + } + + + /// <summary> + /// <para>@GetMapping("/destinations")</para> + /// </summary> + /// <param name="offset">RequestParam</param> + /// <param name="limit">RequestParam</param> + /// <returns></returns> + public DestinationListDto ListDestinations(int offset, int limit) { + throw new NotImplementedException(); + } + + + /// <summary> + /// <para>@PutMapping("/destinations/{destinationId}")</para> + /// </summary> + /// <param name="destinationId">PathVariable</param> + /// <param name="updateDestination">RequestBody</param> + /// <returns></returns> + public PrivateDestinationDto UpdateDestination(string destinationId, + UpdateDestinationDto updateDestination) { + throw new NotImplementedException(); + } + + public async Task<string> GetPublicKey(string destinationId) { + _oAuthService.EnsureAuthenticated(); + var destinationInfo = + await RestCall<PublicDestinationDto>($"/destinations/{destinationId}", + HttpMethod.Get); + + if (destinationInfo?.EncryptionKid == null) + throw new ArgumentException("EncryptionKid was not received"); + + var keyId = destinationInfo.EncryptionKid; + + // /v1/destinations/{{destinationId}}/keys/{{keyId} + var result = await RestCallForString($"/destinations/{destinationId}/keys/{keyId}", + HttpMethod.Get); + + return result; + } +} diff --git a/FitConnect/Services/InfoService.cs b/FitConnect/Services/InfoService.cs new file mode 100644 index 0000000000000000000000000000000000000000..86dcd83a1d752b2e0993d90cb5b39cdbdcc1e3a0 --- /dev/null +++ b/FitConnect/Services/InfoService.cs @@ -0,0 +1,14 @@ +namespace FitConnect.Services; + +public class InfoService : RestCallService { + public InfoService(string baseUrl) : base(baseUrl) { + } + + /// <summary> + /// @GetMapping("/info") + /// </summary> + /// <returns></returns> + public string GetInfo() { + throw new NotImplementedException(); + } +} diff --git a/FitConnect/Services/Interfaces/IDestinationService.cs b/FitConnect/Services/Interfaces/IDestinationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..44e370d49eff3601f7a2808e92425abe4189bef6 --- /dev/null +++ b/FitConnect/Services/Interfaces/IDestinationService.cs @@ -0,0 +1,58 @@ +using FitConnect.Services.Models.v1.Destination; + +namespace FitConnect.Services.Interfaces; + +public interface IDestinationService : IRestCallService { + /// <summary> + /// <para>@PostMapping("/destinations")</para> + /// </summary> + /// <param name="createDestination">RequestBody</param> + /// <returns></returns> + PrivateDestinationDto CreateDestination(CreateDestinationDto createDestination); + + /// <summary> + /// <para>@GetMapping("/destinations/{destinationId}")</para> + /// </summary> + /// <param name="destinationId">PathVariable</param> + void GetDestination(string destinationId); + + /// <summary> + /// <para>@DeleteMapping("/destinations/{destinationId}")</para> + /// </summary> + /// <param name="destinationId">PathVariable</param> + /// <returns></returns> + bool DeleteDestination(string destinationId); + + /// <summary> + /// <para>@PatchMapping("/destinations/{destinationId}")</para> + /// </summary> + /// <param name="destinationId">PathVariable</param> + /// <param name="patchDestination">RequestBody</param> + /// <returns></returns> + PrivateDestinationDto PatchDestination(string destinationId, + PatchDestinationDto patchDestination); + + /// <summary> + /// <para>@GetMapping("/destinations")</para> + /// </summary> + /// <param name="offset">RequestParam</param> + /// <param name="limit">RequestParam</param> + /// <returns></returns> + DestinationListDto ListDestinations(int offset, int limit); + + /// <summary> + /// <para>@PutMapping("/destinations/{destinationId}")</para> + /// </summary> + /// <param name="destinationId">PathVariable</param> + /// <param name="updateDestination">RequestBody</param> + /// <returns></returns> + PrivateDestinationDto UpdateDestination(string destinationId, + UpdateDestinationDto updateDestination); + + /// <summary> + /// <para>@GetMapping("/v1/destinations/{{destinationId}}/keys/{{keyId}")</para> + /// </summary> + /// <param name="destinationId"></param> + /// <returns></returns> + Task<string> GetPublicKey(string destinationId); +} diff --git a/FitConnect/Services/Interfaces/IOAuthService.cs b/FitConnect/Services/Interfaces/IOAuthService.cs new file mode 100644 index 0000000000000000000000000000000000000000..30da0edc50089aa4778a391d08ac8d4a283ad128 --- /dev/null +++ b/FitConnect/Services/Interfaces/IOAuthService.cs @@ -0,0 +1,23 @@ +namespace FitConnect.Services.Interfaces; + +public interface IOAuthService : IRestCallService { + bool IsAuthenticated { get; } + + + /// <summary> + /// Requesting an OAuth token from the FitConnect API. + /// <para> + /// You can get the Client ID and Client Secret from the FitConnect Self Service portal + /// under <br /> + /// https://portal.auth-testing.fit-connect.fitko.dev + /// </para> + /// </summary> + /// <param name="clientId">Your client Id</param> + /// <param name="clientSecret">Your client Secret</param> + /// <param name="scope">Scope if needed</param> + /// <returns>The received token or null</returns> + Task AuthenticateAsync( + string? scope = null); + + public void EnsureAuthenticated(); +} diff --git a/FitConnect/Services/Interfaces/IRouteService.cs b/FitConnect/Services/Interfaces/IRouteService.cs new file mode 100644 index 0000000000000000000000000000000000000000..42e0c535c2a38f80e255168ab37538174ed8b91e --- /dev/null +++ b/FitConnect/Services/Interfaces/IRouteService.cs @@ -0,0 +1,27 @@ +using FitConnect.Models; +using Route = FitConnect.Services.Models.v1.Routes.Route; + +namespace FitConnect.Services.Interfaces; + +public interface IRouteService : IRestCallService { + /// <summary> + /// Returns the destination id for the given intent. + /// </summary> + /// <param name="leiaKey"></param> + /// <param name="ags"></param> + /// <param name="ars"></param> + /// <param name="areaId"></param> + /// <returns></returns> + Task<List<Route>> GetDestinationIdAsync(string leiaKey, + string? ags = null, + string? ars = null, + string? areaId = null); + + /// <summary> + /// </summary> + /// <param name="filter"></param> + /// <param name="offset"></param> + /// <param name="limit"></param> + /// <returns></returns> + Task<AreaList?> GetAreas(string filter, int offset = 0, int limit = 100); +} diff --git a/FitConnect/Services/Interfaces/ISubmissionService.cs b/FitConnect/Services/Interfaces/ISubmissionService.cs new file mode 100644 index 0000000000000000000000000000000000000000..becef9e08764b194886c1031e3c118d0174d3a84 --- /dev/null +++ b/FitConnect/Services/Interfaces/ISubmissionService.cs @@ -0,0 +1,91 @@ +using FitConnect.Services.Models.v1.Submission; + +namespace FitConnect.Services.Interfaces; + +public interface ISubmissionService : IRestCallService { + /// <summary> + /// <para>@PostMapping("/submissions")</para> + /// </summary> + /// <param name="submissionDto">RequestBody</param> + /// <returns></returns> + SubmissionCreatedDto CreateSubmission(CreateSubmissionDto submissionDto); + + /// <summary> + /// <para> + /// @PutMapping(value = "/submissions/{submissionId}/attachments/{attachmentId}", consumes = + /// "application/jose") + /// </para> + /// </summary> + /// <param name="submissionId">PathVariable</param> + /// <param name="attachmentId">PathVariable</param> + /// <param name="encryptedAttachmentContent">RequestBody</param> + /// <returns></returns> + bool AddSubmissionAttachment(string submissionId, string attachmentId, + string encryptedAttachmentContent); + + /// <summary> + /// <para>@PutMapping(value = "/submissions/{submissionId}", consumes = "application/json") </para> + /// </summary> + /// <param name="submissionId">PathVariable</param> + /// <param name="submitSubmission">RequestBody</param> + /// <returns></returns> + Task<SubmissionReducedDto?> SubmitSubmission(string submissionId, + SubmitSubmissionDto submitSubmission); + + /// <summary> + /// <para>@GetMapping("/submissions")</para> + /// </summary> + /// <param name="destinationId">RequestParam</param> + /// <param name="offset">RequestParam</param> + /// <param name="limit">RequestParam</param> + /// <returns></returns> + Task<SubmissionsForPickupDto> ListSubmissions(string? destinationId, int offset, + int limit); + + /// <summary> + /// <para>@GetMapping("/submissions/{submissionId}")</para> + /// </summary> + /// <param name="submissionId">PathVariable</param> + /// <returns></returns> + SubmissionDto GetSubmission(string submissionId); + + /// <summary> + /// <para> + /// @GetMapping(value = "/submissions/{submissionId}/attachments/{attachmentId}", produces = + /// "application/jose") + /// </para> + /// </summary> + /// <param name="submissionId">PathVariable</param> + /// <param name="attachmentId">PathVariable</param> + /// <returns></returns> + string GetAttachment(string submissionId, string attachmentId); + + /// <summary> + /// Receiving the encryption key from the submission api + /// </summary> + /// <param name="keyId"></param> + /// <returns></returns> + string GetKey(string keyId); + + + /// <summary> + /// Uploads an attachment to the submission api + /// </summary> + /// <param name="submissionId"></param> + /// <param name="attachmentId"></param> + /// <param name="content"></param> + void UploadAttachment(string submissionId, string attachmentId, string content) { + AddSubmissionAttachment(submissionId, attachmentId, content); + } + + /// <summary> + /// Retrieves the events of a submission + /// </summary> + /// <param name="caseId"></param> + /// <returns></returns> + Task<List<string>> GetStatusForSubmissionAsync(string caseId); +} + +public interface ICasesService { + public string FinishSubmission(string caseId, string token); +} diff --git a/FitConnect/Services/Models/ApiInfoDto.cs b/FitConnect/Services/Models/ApiInfoDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..5cc8756064c2fa0b71a55d252659ace554ac430e --- /dev/null +++ b/FitConnect/Services/Models/ApiInfoDto.cs @@ -0,0 +1,4 @@ +namespace FitConnect.Services.Models; + +public class ApiInfoDto { +} diff --git a/FitConnect/Services/Models/CallbackDto.cs b/FitConnect/Services/Models/CallbackDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..0228e1f89d81fb88a94ded3a2d5f45f55f8789f0 --- /dev/null +++ b/FitConnect/Services/Models/CallbackDto.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models; + +public class CallbackDto { + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("secret")] + public string? Secret { get; set; } +} diff --git a/FitConnect/Services/Models/OAuthAccessToken.cs b/FitConnect/Services/Models/OAuthAccessToken.cs new file mode 100644 index 0000000000000000000000000000000000000000..077cb92c3c859057dc72e6d848ad72272a401b70 --- /dev/null +++ b/FitConnect/Services/Models/OAuthAccessToken.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models; + +public class OAuthAccessToken { + private readonly DateTime createdAt = DateTime.Now; + + [JsonPropertyName("access_token")] + public string? AccessToken { get; set; } + + [JsonPropertyName("scope")] + public string? Scope { get; set; } + + [JsonPropertyName("token_type")] + public string? TokenType { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + public bool IsValid => createdAt.AddSeconds(ExpiresIn) > DateTime.Now; +} diff --git a/FitConnect/Services/Models/ServiceTypeDto.cs b/FitConnect/Services/Models/ServiceTypeDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..97b5a604fec11305eb21684cc6675b828f121ddb --- /dev/null +++ b/FitConnect/Services/Models/ServiceTypeDto.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models; + +public class ServiceTypeDto { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("identifier")] + public string? Identifier { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Api/Metadata.cs b/FitConnect/Services/Models/v1/Api/Metadata.cs new file mode 100644 index 0000000000000000000000000000000000000000..c5ccaf433011c00cc3ca3068b80d95a09ac78c45 --- /dev/null +++ b/FitConnect/Services/Models/v1/Api/Metadata.cs @@ -0,0 +1,572 @@ +// <auto-generated /> +// +// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do: +// +// using FitConnect; +// +// var metadata = Metadata.FromJson(jsonString); + +namespace FitConnect.Models.Api.Metadata +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public class Metadata + { + /// <summary> + /// Eine Struktur, um zusätzliche Informationen zu hinterlegen + /// </summary> + [JsonProperty("additionalReferenceInfo", NullValueHandling = NullValueHandling.Ignore)] + public AdditionalReferenceInfo AdditionalReferenceInfo { get; set; } + + /// <summary> + /// Eine Liste aller Identifikationsnachweise der Einreichung. + /// </summary> + [JsonProperty("authenticationInformation", NullValueHandling = NullValueHandling.Ignore)] + public List<AuthenticationInformation> AuthenticationInformation { get; set; } + + /// <summary> + /// Beschreibt die Struktur der zusätzlichen Inhalte der Einreichung, wie Anlagen oder + /// Fachdaten. + /// </summary> + [JsonProperty("contentStructure")] + public ContentStructure ContentStructure { get; set; } + + /// <summary> + /// Dieses Objekt enthält die Informationen vom Bezahldienst. + /// </summary> + [JsonProperty("paymentInformation", NullValueHandling = NullValueHandling.Ignore)] + public PaymentInformation PaymentInformation { get; set; } + + /// <summary> + /// Beschreibung der Art der Verwaltungsleistung. Eine Verwaltungsleistung sollte immer mit + /// einer LeiKa-Id beschrieben werden. Ist für die gegebene Verwaltungsleistung keine + /// LeiKa-Id vorhanden, kann die Verwaltungsleistung übergangsweise über die Angabe einer + /// anderen eindeutigen Schema-URN beschrieben werden. + /// </summary> + [JsonProperty("publicServiceType", NullValueHandling = NullValueHandling.Ignore)] + public Verwaltungsleistung PublicServiceType { get; set; } + + [JsonProperty("replyChannel", NullValueHandling = NullValueHandling.Ignore)] + public ReplyChannel ReplyChannel { get; set; } + } + + /// <summary> + /// Eine Struktur, um zusätzliche Informationen zu hinterlegen + /// </summary> + public partial class AdditionalReferenceInfo + { + /// <summary> + /// Das Datum der Antragstellung. Das Datum muss nicht zwingend identisch mit dem Datum der + /// Einreichung des Antrags über FIT-Connect sein. + /// </summary> + [JsonProperty("applicationDate", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? ApplicationDate { get; set; } + + /// <summary> + /// Eine Referenz zum Vorgang im sendenden System, um bei Problemen und Rückfragen + /// außerhalb von FIT-Connect den Vorgang im dortigen System schneller zu identifizieren. + /// </summary> + [JsonProperty("senderReference", NullValueHandling = NullValueHandling.Ignore)] + public string SenderReference { get; set; } + } + + /// <summary> + /// Eine Struktur, die einen Identifikationsnachweis beschreibt. + /// </summary> + public partial class AuthenticationInformation + { + /// <summary> + /// Der Nachweis wird als Base64Url-kodierte Zeichenkette angegeben. + /// </summary> + [JsonProperty("content")] + public string Content { get; set; } + + /// <summary> + /// Definiert die Art des Identifikationsnachweises. + /// </summary> + [JsonProperty("type")] + public AuthenticationInformationType Type { get; set; } + + /// <summary> + /// semver kompatible Versionsangabe des genutzten Nachweistyps. + /// </summary> + [JsonProperty("version")] + public string Version { get; set; } + } + + /// <summary> + /// Beschreibt die Struktur der zusätzlichen Inhalte der Einreichung, wie Anlagen oder + /// Fachdaten. + /// </summary> + public partial class ContentStructure + { + [JsonProperty("attachments")] + public List<Attachment> Attachments { get; set; } + + /// <summary> + /// Definiert das Schema und die Signatur(-art), die für die Fachdaten verwendet werden. + /// </summary> + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public Data Data { get; set; } + } + + /// <summary> + /// Eine in der Einreichung enthaltene Anlage. + /// </summary> + public partial class Attachment + { + /// <summary> + /// Innerhalb einer Einreichung eindeutige Id der Anlage im Format einer UUIDv4. + /// </summary> + [JsonProperty("attachmentId")] + public string AttachmentId { get; set; } + + /// <summary> + /// Optionale Beschreibung der Anlage + /// </summary> + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } + + /// <summary> + /// Ursprünglicher Dateiname bei Erzeugung oder Upload + /// </summary> + [JsonProperty("filename", NullValueHandling = NullValueHandling.Ignore)] + public string Filename { get; set; } + + /// <summary> + /// Der Hashwert der unverschlüsselten Anlage. Die Angabe des Hashwertes dient der + /// Integritätssicherung des Gesamtantrags und schützt vor einem Austausch der Anlage durch + /// Systeme zwischen Sender und Subscriber (z.B. dem Zustelldienst). + /// </summary> + [JsonProperty("hash")] + public AttachmentHash Hash { get; set; } + + /// <summary> + /// Internet Media Type gemäß RFC 2045, z. B. application/pdf. + /// </summary> + [JsonProperty("mimeType")] + public string MimeType { get; set; } + + /// <summary> + /// Zweck/Art der Anlage + /// - form: Automatisch generierte PDF-Repräsentation des vollständigen Antragsformulars + /// - attachment: Anlage, die von einem Bürger hochgeladen wurde + /// - report: Vom Onlinedienst, nachträglich erzeugte Unterlage + /// </summary> + [JsonProperty("purpose")] + public string Purpose { get; set; } + + [JsonProperty("signature", NullValueHandling = NullValueHandling.Ignore)] + public AttachmentSignature Signature { get; set; } + } + + /// <summary> + /// Der Hashwert der unverschlüsselten Anlage. Die Angabe des Hashwertes dient der + /// Integritätssicherung des Gesamtantrags und schützt vor einem Austausch der Anlage durch + /// Systeme zwischen Sender und Subscriber (z.B. dem Zustelldienst). + /// </summary> + public partial class AttachmentHash + { + /// <summary> + /// Der Hex-kodierte Hashwert gemäß des angegebenen Algorithmus. + /// </summary> + [JsonProperty("content")] + public string Content { get; set; } + + /// <summary> + /// Der verwendete Hash-Algorithmus. Derzeit ist nur `sha512` erlaubt. + /// </summary> + [JsonProperty("type")] + public string Type { get; set; } + } + + /// <summary> + /// Beschreibt das Signaturformt und Profile + /// </summary> + public partial class AttachmentSignature + { + /// <summary> + /// Hier wird die Signatur im Falle einer Detached-Signatur als Base64- oder + /// Base64Url-kodierte Zeichenkette hinterlegt. Eine Base64Url-Kodierung kommt nur bei + /// Einsatz von JSON Web Signatures (JWS / JAdES) zum Einsatz. + /// </summary> + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string Content { get; set; } + + /// <summary> + /// Beschreibt, ob die Signatur als seperate (detached) Signatur (`true`) oder als Teil des + /// Fachdatensatzes bzw. der Anlage (`false`) übertragen wird. Wenn der Wert `true` ist, + /// dann wird die Signatur Base64- oder Base64Url-kodiert im Feld `content` übertragen. + /// </summary> + [JsonProperty("detachedSignature")] + public bool DetachedSignature { get; set; } + + /// <summary> + /// Referenziert ein eindeutiges Profil einer AdES (advanced electronic signature/seal) + /// gemäß eIDAS-Verordnung über eine URI gemäß [ETSI TS 119 + /// 192](https://www.etsi.org/deliver/etsi_ts/119100_119199/119192/01.01.01_60/ts_119192v010101p.pdf). + /// + /// Für die Details zur Verwendung und Validierung von Profilen siehe auch + /// https://ec.europa.eu/cefdigital/DSS/webapp-demo/doc/dss-documentation.html#_signatures_profile_simplification + /// </summary> + [JsonProperty("eidasAdesProfile", NullValueHandling = NullValueHandling.Ignore)] + public EidasAdesProfile? EidasAdesProfile { get; set; } + + /// <summary> + /// Beschreibt, welches Signaturformat die genutzte Signatur / das genutzte Siegel nutzt. + /// Aktuell wird die Hinterlegung folgender Signaturformate unterstützt: CMS = Cryptographic + /// Message Syntax, Asic = Associated Signature Containers, PDF = PDF Signatur, XML = + /// XML-Signature, JSON = JSON Web Signature. + /// </summary> + [JsonProperty("signatureFormat")] + public SignatureFormat SignatureFormat { get; set; } + } + + /// <summary> + /// Definiert das Schema und die Signatur(-art), die für die Fachdaten verwendet werden. + /// </summary> + public partial class Data + { + /// <summary> + /// Der Hashwert der unverschlüsselten Fachdaten. Die Angabe des Hashwertes dient der + /// Integritätssicherung des Gesamtantrags und schützt vor einem Austausch der Fachdaten + /// durch Systeme zwischen Sender und Subscriber (z.B. dem Zustelldienst). + /// </summary> + [JsonProperty("hash")] + public DataHash Hash { get; set; } + + /// <summary> + /// Beschreibt das Signaturformt und Profile + /// </summary> + [JsonProperty("signature", NullValueHandling = NullValueHandling.Ignore)] + public DataSignature Signature { get; set; } + + /// <summary> + /// Referenz auf ein Schema, das die Struktur der Fachdaten einer Einreichung beschreibt. + /// </summary> + [JsonProperty("submissionSchema")] + public Fachdatenschema SubmissionSchema { get; set; } + } + + /// <summary> + /// Der Hashwert der unverschlüsselten Fachdaten. Die Angabe des Hashwertes dient der + /// Integritätssicherung des Gesamtantrags und schützt vor einem Austausch der Fachdaten + /// durch Systeme zwischen Sender und Subscriber (z.B. dem Zustelldienst). + /// </summary> + public partial class DataHash + { + /// <summary> + /// Der Hex-kodierte Hashwert gemäß des angegebenen Algorithmus. + /// </summary> + [JsonProperty("content")] + public string Content { get; set; } + + /// <summary> + /// Der verwendete Hash-Algorithmus. Derzeit ist nur `sha512` erlaubt. + /// </summary> + [JsonProperty("type")] + public string Type { get; set; } + } + + /// <summary> + /// Beschreibt das Signaturformt und Profile + /// </summary> + public partial class DataSignature + { + /// <summary> + /// Hier wird die Signatur im Falle einer Detached-Signatur als Base64- oder + /// Base64Url-kodierte Zeichenkette hinterlegt. Eine Base64Url-Kodierung kommt nur bei + /// Einsatz von JSON Web Signatures (JWS / JAdES) zum Einsatz. + /// </summary> + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string Content { get; set; } + + /// <summary> + /// Beschreibt, ob die Signatur als seperate (detached) Signatur (`true`) oder als Teil des + /// Fachdatensatzes bzw. der Anlage (`false`) übertragen wird. Wenn der Wert `true` ist, + /// dann wird die Signatur Base64- oder Base64Url-kodiert im Feld `content` übertragen. + /// </summary> + [JsonProperty("detachedSignature")] + public bool DetachedSignature { get; set; } + + /// <summary> + /// Referenziert ein eindeutiges Profil einer AdES (advanced electronic signature/seal) + /// gemäß eIDAS-Verordnung über eine URI gemäß [ETSI TS 119 + /// 192](https://www.etsi.org/deliver/etsi_ts/119100_119199/119192/01.01.01_60/ts_119192v010101p.pdf). + /// + /// Für die Details zur Verwendung und Validierung von Profilen siehe auch + /// https://ec.europa.eu/cefdigital/DSS/webapp-demo/doc/dss-documentation.html#_signatures_profile_simplification + /// </summary> + [JsonProperty("eidasAdesProfile", NullValueHandling = NullValueHandling.Ignore)] + public EidasAdesProfile? EidasAdesProfile { get; set; } + + /// <summary> + /// Beschreibt, welches Signaturformat die genutzte Signatur / das genutzte Siegel nutzt. + /// Aktuell wird die Hinterlegung folgender Signaturformate unterstützt: CMS = Cryptographic + /// Message Syntax, Asic = Associated Signature Containers, PDF = PDF Signatur, XML = + /// XML-Signature, JSON = JSON Web Signature. + /// </summary> + [JsonProperty("signatureFormat")] + public SignatureFormat SignatureFormat { get; set; } + } + + /// <summary> + /// Referenz auf ein Schema, das die Struktur der Fachdaten einer Einreichung beschreibt. + /// </summary> + public partial class Fachdatenschema + { + /// <summary> + /// Mimetype (z.B. application/json oder application/xml) des referenzierten Schemas (z.B. + /// XSD- oder JSON-Schema). + /// </summary> + [JsonProperty("mimeType")] + public string MimeType { get; set; } + + /// <summary> + /// URI des Fachschemas. Wird hier eine URL verwendet, sollte das Schema unter der + /// angegebenen URL abrufbar sein. Eine Verfügbarkeit des Schemas unter der angegebenen URL + /// darf jedoch nicht vorausgesetzt werden. + /// </summary> + [JsonProperty("schemaUri")] + public string SchemaUri { get; set; } + } + + /// <summary> + /// Dieses Objekt enthält die Informationen vom Bezahldienst. + /// </summary> + public partial class PaymentInformation + { + /// <summary> + /// Bruttobetrag + /// </summary> + [JsonProperty("grossAmount", NullValueHandling = NullValueHandling.Ignore)] + + public double? GrossAmount { get; set; } + + /// <summary> + /// Die vom Benutzer ausgewählte Zahlart. Das Feld ist nur bei einer erfolgreichen Zahlung + /// vorhanden / befüllt. + /// </summary> + [JsonProperty("paymentMethod")] + public PaymentMethod PaymentMethod { get; set; } + + /// <summary> + /// Weitere Erläuterung zur gewählten Zahlart. + /// </summary> + [JsonProperty("paymentMethodDetail", NullValueHandling = NullValueHandling.Ignore)] + + public string PaymentMethodDetail { get; set; } + + /// <summary> + /// - INITIAL - der Einreichung hat einen Payment-Request ausgelöst und eine + /// Payment-Transaction wurde angelegt. Der Nutzer hat aber im Bezahldienst noch keine + /// Wirkung erzeugt. + /// - BOOKED - der Nutzer hat die Bezahlung im Bezahldienst autorisiert. + /// - FAILED - der Vorgang wurde vom Bezahldienst aufgrund der Nutzereingaben abgebrochen. + /// - CANCELED - der Nutzer hat die Bezahlung im Bezahldienst abgebrochen. + /// </summary> + [JsonProperty("status")] + public Status Status { get; set; } + + /// <summary> + /// Eine vom Bezahldienst vergebene Transaktions-Id. + /// </summary> + [JsonProperty("transactionId")] + + public string TransactionId { get; set; } + + /// <summary> + /// Bezahlreferenz bzw. Verwendungszweck, wie z. B. ein Kassenzeichen. + /// </summary> + [JsonProperty("transactionReference")] + public string TransactionReference { get; set; } + + /// <summary> + /// Zeitstempel der erfolgreichen Durchführung der Bezahlung. + /// </summary> + [JsonProperty("transactionTimestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? TransactionTimestamp { get; set; } + + /// <summary> + /// Die Rest-URL der Payment Transaction für die Statusabfrage. + /// </summary> + [JsonProperty("transactionUrl", NullValueHandling = NullValueHandling.Ignore)] + public Uri TransactionUrl { get; set; } + } + + /// <summary> + /// Beschreibung der Art der Verwaltungsleistung. Eine Verwaltungsleistung sollte immer mit + /// einer LeiKa-Id beschrieben werden. Ist für die gegebene Verwaltungsleistung keine + /// LeiKa-Id vorhanden, kann die Verwaltungsleistung übergangsweise über die Angabe einer + /// anderen eindeutigen Schema-URN beschrieben werden. + /// </summary> + public partial class Verwaltungsleistung + { + /// <summary> + /// (Kurz-)Beschreibung der Verwaltungsleistung + /// </summary> + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } + + /// <summary> + /// URN einer Leistung. Im Falle einer Leistung aus dem Leistungskatalog sollte hier + /// `urn:de:fim:leika:leistung:` vorangestellt werden. + /// </summary> + [JsonProperty("identifier")] + + public string Identifier { get; set; } + + /// <summary> + /// Name/Bezeichnung der Verwaltungsleistung + /// </summary> + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + } + + public partial class ReplyChannel + { + /// <summary> + /// Akkreditierte Anbieter siehe + /// https://www.bsi.bund.de/DE/Themen/Oeffentliche-Verwaltung/Moderner-Staat/De-Mail/Akkreditierte-DMDA/akkreditierte-dmda_node.html + /// </summary> + [JsonProperty("deMail", NullValueHandling = NullValueHandling.Ignore)] + public DeMail DeMail { get; set; } + + /// <summary> + /// Siehe https://www.elster.de/elsterweb/infoseite/elstertransfer_hilfe_schnittstellen + /// </summary> + [JsonProperty("elster", NullValueHandling = NullValueHandling.Ignore)] + public Elster Elster { get; set; } + + [JsonProperty("eMail", NullValueHandling = NullValueHandling.Ignore)] + public EMail EMail { get; set; } + + /// <summary> + /// Postfachadresse in einem interoperablen Servicekonto (FINK.PFISK) + /// </summary> + [JsonProperty("fink", NullValueHandling = NullValueHandling.Ignore)] + public Fink Fink { get; set; } + } + + /// <summary> + /// Akkreditierte Anbieter siehe + /// https://www.bsi.bund.de/DE/Themen/Oeffentliche-Verwaltung/Moderner-Staat/De-Mail/Akkreditierte-DMDA/akkreditierte-dmda_node.html + /// </summary> + public partial class DeMail + { + [JsonProperty("address")] + public string Address { get; set; } + } + + public partial class EMail + { + [JsonProperty("address")] + public string Address { get; set; } + + /// <summary> + /// Hilfe zur Erstellung gibt es in der Dokumentation unter + /// https://docs.fitko.de/fit-connect/details/pgp-export + /// </summary> + [JsonProperty("pgpPublicKey", NullValueHandling = NullValueHandling.Ignore)] + public string PgpPublicKey { get; set; } + } + + /// <summary> + /// Siehe https://www.elster.de/elsterweb/infoseite/elstertransfer_hilfe_schnittstellen + /// </summary> + public partial class Elster + { + [JsonProperty("accountId")] + public string AccountId { get; set; } + + [JsonProperty("geschaeftszeichen", NullValueHandling = NullValueHandling.Ignore)] + + public string Geschaeftszeichen { get; set; } + + [JsonProperty("lieferTicket", NullValueHandling = NullValueHandling.Ignore)] + public string LieferTicket { get; set; } + } + + /// <summary> + /// Postfachadresse in einem interoperablen Servicekonto (FINK.PFISK) + /// </summary> + public partial class Fink + { + /// <summary> + /// FINK Postfachadresse + /// </summary> + [JsonProperty("finkPostfachRef")] + + public string FinkPostfachRef { get; set; } + + /// <summary> + /// URL des Servicekontos, in dem das Ziel-Postfach liegt + /// </summary> + [JsonProperty("host", NullValueHandling = NullValueHandling.Ignore)] + public Uri Host { get; set; } + } + + /// <summary> + /// Definiert die Art des Identifikationsnachweises. + /// </summary> + public enum AuthenticationInformationType { IdentificationReport }; + + /// <summary> + /// Der verwendete Hash-Algorithmus. Derzeit ist nur `sha512` erlaubt. + /// </summary> + public enum HashType { Sha512 }; + + /// <summary> + /// Zweck/Art der Anlage + /// - form: Automatisch generierte PDF-Repräsentation des vollständigen Antragsformulars + /// - attachment: Anlage, die von einem Bürger hochgeladen wurde + /// - report: Vom Onlinedienst, nachträglich erzeugte Unterlage + /// </summary> + public enum Purpose { Attachment, Form, Report }; + + /// <summary> + /// Referenziert ein eindeutiges Profil einer AdES (advanced electronic signature/seal) + /// gemäß eIDAS-Verordnung über eine URI gemäß [ETSI TS 119 + /// 192](https://www.etsi.org/deliver/etsi_ts/119100_119199/119192/01.01.01_60/ts_119192v010101p.pdf). + /// + /// Für die Details zur Verwendung und Validierung von Profilen siehe auch + /// https://ec.europa.eu/cefdigital/DSS/webapp-demo/doc/dss-documentation.html#_signatures_profile_simplification + /// </summary> + public enum EidasAdesProfile { HttpUriEtsiOrgAdes191X2LevelBaselineBB, HttpUriEtsiOrgAdes191X2LevelBaselineBLt, HttpUriEtsiOrgAdes191X2LevelBaselineBLta, HttpUriEtsiOrgAdes191X2LevelBaselineBT }; + + /// <summary> + /// Beschreibt, welches Signaturformat die genutzte Signatur / das genutzte Siegel nutzt. + /// Aktuell wird die Hinterlegung folgender Signaturformate unterstützt: CMS = Cryptographic + /// Message Syntax, Asic = Associated Signature Containers, PDF = PDF Signatur, XML = + /// XML-Signature, JSON = JSON Web Signature. + /// </summary> + public enum SignatureFormat { Asic, Cms, Json, Pdf, Xml }; + + /// <summary> + /// Mimetype (z.B. application/json oder application/xml) des referenzierten Schemas (z.B. + /// XSD- oder JSON-Schema). + /// </summary> + //public enum MimeType { ApplicationJson, ApplicationXml }; + + /// <summary> + /// Die vom Benutzer ausgewählte Zahlart. Das Feld ist nur bei einer erfolgreichen Zahlung + /// vorhanden / befüllt. + /// </summary> + public enum PaymentMethod { Creditcard, Giropay, Invoice, Other, Paydirect, Paypal }; + + /// <summary> + /// - INITIAL - der Einreichung hat einen Payment-Request ausgelöst und eine + /// Payment-Transaction wurde angelegt. Der Nutzer hat aber im Bezahldienst noch keine + /// Wirkung erzeugt. + /// - BOOKED - der Nutzer hat die Bezahlung im Bezahldienst autorisiert. + /// - FAILED - der Vorgang wurde vom Bezahldienst aufgrund der Nutzereingaben abgebrochen. + /// - CANCELED - der Nutzer hat die Bezahlung im Bezahldienst abgebrochen. + /// </summary> + public enum Status { Booked, Canceled, Failed, Initial }; + +} diff --git a/FitConnect/Services/Models/v1/Api/SecEventToken.cs b/FitConnect/Services/Models/v1/Api/SecEventToken.cs new file mode 100644 index 0000000000000000000000000000000000000000..a6d73d83b95a49eab53be6f110bdb4145d6966f9 --- /dev/null +++ b/FitConnect/Services/Models/v1/Api/SecEventToken.cs @@ -0,0 +1,415 @@ +// <auto-generated /> +// +// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do: +// +// using FitConnect.Models.v1.Api; +// +// var welcome = Welcome.FromJson(jsonString); + +namespace FitConnect.Models.v1.Api +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class SecurityEventToken + { + [JsonProperty("$schema")] + public Uri Schema { get; set; } + + [JsonProperty("$id")] + public Uri Id { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + public WelcomeProperties Properties { get; set; } + + [JsonProperty("additionalProperties")] + public bool AdditionalProperties { get; set; } + + [JsonProperty("required")] + public string[] WelcomeRequired { get; set; } + } + + public partial class WelcomeProperties + { + [JsonProperty("$schema")] + public Schema Schema { get; set; } + + [JsonProperty("jti")] + public Jti Jti { get; set; } + + [JsonProperty("iss")] + public Iss Iss { get; set; } + + [JsonProperty("iat")] + public Iat Iat { get; set; } + + [JsonProperty("sub")] + public Sub Sub { get; set; } + + [JsonProperty("txn")] + public Sub Txn { get; set; } + + [JsonProperty("events")] + public Events Events { get; set; } + } + + public partial class Events + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + public EventsProperties Properties { get; set; } + + [JsonProperty("additionalProperties")] + public bool AdditionalProperties { get; set; } + + [JsonProperty("minProperties")] + public long MinProperties { get; set; } + + [JsonProperty("maxProperties")] + public long MaxProperties { get; set; } + } + + public partial class EventsProperties + { + [JsonProperty("https://schema.fitko.de/fit-connect/events/create-submission")] + public Iss HttpsSchemaFitkoDeFitConnectEventsCreateSubmission { get; set; } + + [JsonProperty("https://schema.fitko.de/fit-connect/events/submit-submission")] + public HttpsSchemaFitkoDeFitConnectEventsSubmitSubmission HttpsSchemaFitkoDeFitConnectEventsSubmitSubmission { get; set; } + + [JsonProperty("https://schema.fitko.de/fit-connect/events/notify-submission")] + public HttpsSchemaFitkoDeFitConnectEventsNotifySubmission HttpsSchemaFitkoDeFitConnectEventsNotifySubmission { get; set; } + + [JsonProperty("https://schema.fitko.de/fit-connect/events/forward-submission")] + public Iss HttpsSchemaFitkoDeFitConnectEventsForwardSubmission { get; set; } + + [JsonProperty("https://schema.fitko.de/fit-connect/events/reject-submission")] + public HttpsSchemaFitkoDeFitConnectEventsRejectSubmission HttpsSchemaFitkoDeFitConnectEventsRejectSubmission { get; set; } + + [JsonProperty("https://schema.fitko.de/fit-connect/events/accept-submission")] + public HttpsSchemaFitkoDeFitConnectEventsAcceptSubmission HttpsSchemaFitkoDeFitConnectEventsAcceptSubmission { get; set; } + + [JsonProperty("https://schema.fitko.de/fit-connect/events/delete-submission")] + public Iss HttpsSchemaFitkoDeFitConnectEventsDeleteSubmission { get; set; } + } + + public partial class HttpsSchemaFitkoDeFitConnectEventsAcceptSubmission + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + public HttpsSchemaFitkoDeFitConnectEventsAcceptSubmissionProperties Properties { get; set; } + + [JsonProperty("required")] + public string[] HttpsSchemaFitkoDeFitConnectEventsAcceptSubmissionRequired { get; set; } + } + + public partial class HttpsSchemaFitkoDeFitConnectEventsAcceptSubmissionProperties + { + [JsonProperty("problems")] + public AuthenticationTags Problems { get; set; } + + [JsonProperty("authenticationTags")] + public AuthenticationTags AuthenticationTags { get; set; } + } + + public partial class AuthenticationTags + { + [JsonProperty("$ref")] + public string Ref { get; set; } + } + + public partial class Iss + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + } + + public partial class HttpsSchemaFitkoDeFitConnectEventsNotifySubmission + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + public HttpsSchemaFitkoDeFitConnectEventsNotifySubmissionProperties Properties { get; set; } + + [JsonProperty("required")] + public string[] HttpsSchemaFitkoDeFitConnectEventsNotifySubmissionRequired { get; set; } + } + + public partial class HttpsSchemaFitkoDeFitConnectEventsNotifySubmissionProperties + { + [JsonProperty("notifyType")] + public NotifyType NotifyType { get; set; } + } + + public partial class NotifyType + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("enum")] + public string[] Enum { get; set; } + } + + public partial class HttpsSchemaFitkoDeFitConnectEventsRejectSubmission + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + public HttpsSchemaFitkoDeFitConnectEventsRejectSubmissionProperties Properties { get; set; } + + [JsonProperty("required")] + public string[] HttpsSchemaFitkoDeFitConnectEventsRejectSubmissionRequired { get; set; } + } + + public partial class HttpsSchemaFitkoDeFitConnectEventsRejectSubmissionProperties + { + [JsonProperty("problems")] + public Problems Problems { get; set; } + } + + public partial class Problems { + public static readonly Problems IncorrectAuthenticationTag = new Problems(); + public static readonly Problems EncryptionIssue = new Problems(); + public static readonly Problems SyntaxViolation = new Problems(); + public static readonly Problems MissingSchema = new Problems(); + public static readonly Problems UnsupportedSchema = new Problems(); + public static readonly Problems SchemaViolation = new Problems(); + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("items")] + public Items Items { get; set; } + } + + public partial class Items + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + public ItemsProperties Properties { get; set; } + + [JsonProperty("required")] + public string[] ItemsRequired { get; set; } + } + + public partial class ItemsProperties + { + [JsonProperty("type")] + public Sub Type { get; set; } + + [JsonProperty("title")] + public Iss Title { get; set; } + + [JsonProperty("detail")] + public Iss Detail { get; set; } + + [JsonProperty("instance")] + public Sub Instance { get; set; } + } + + public partial class Sub + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("pattern")] + public string Pattern { get; set; } + } + + public partial class HttpsSchemaFitkoDeFitConnectEventsSubmitSubmission + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + public HttpsSchemaFitkoDeFitConnectEventsSubmitSubmissionProperties Properties { get; set; } + + [JsonProperty("required")] + public string[] HttpsSchemaFitkoDeFitConnectEventsSubmitSubmissionRequired { get; set; } + } + + public partial class HttpsSchemaFitkoDeFitConnectEventsSubmitSubmissionProperties + { + [JsonProperty("authenticationTags")] + public PurpleAuthenticationTags AuthenticationTags { get; set; } + } + + public partial class PurpleAuthenticationTags + { + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + public AuthenticationTagsProperties Properties { get; set; } + + [JsonProperty("additionalProperties")] + public bool AdditionalProperties { get; set; } + + [JsonProperty("required")] + public string[] AuthenticationTagsRequired { get; set; } + } + + public partial class AuthenticationTagsProperties + { + [JsonProperty("metadata")] + public AuthenticationTags Metadata { get; set; } + + [JsonProperty("data")] + public Data Data { get; set; } + + [JsonProperty("attachments")] + public Attachments Attachments { get; set; } + } + + public partial class Attachments + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("patternProperties")] + public PatternProperties PatternProperties { get; set; } + + [JsonProperty("additionalProperties")] + public bool AdditionalProperties { get; set; } + } + + public partial class PatternProperties + { + [JsonProperty("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")] + public AuthenticationTags The09AFAF809AFAF409AFAF409AFAF409AFAF12 { get; set; } + } + + public partial class Data + { + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("pattern")] + public string Pattern { get; set; } + + [JsonProperty("minLength")] + public long MinLength { get; set; } + + [JsonProperty("maxLength")] + public long MaxLength { get; set; } + } + + public partial class Iat + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("minimum")] + public long Minimum { get; set; } + } + + public partial class Jti + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("format")] + public string Format { get; set; } + } + + public partial class Schema + { + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("enum")] + public Uri[] Enum { get; set; } + } + + public partial class SecurityEventToken + { + public static SecurityEventToken FromJson(string json) => JsonConvert.DeserializeObject<SecurityEventToken>(json, FitConnect.Models.v1.Api.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this SecurityEventToken self) => JsonConvert.SerializeObject(self, FitConnect.Models.v1.Api.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + { + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + DateParseHandling = DateParseHandling.None, + Converters = + { + new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } + }, + }; + } +} diff --git a/FitConnect/Services/Models/v1/Api/SecurityEventToken.cs b/FitConnect/Services/Models/v1/Api/SecurityEventToken.cs new file mode 100644 index 0000000000000000000000000000000000000000..9fcbaeb948e6f4ab6f64ae61511502dcdd89e6e8 --- /dev/null +++ b/FitConnect/Services/Models/v1/Api/SecurityEventToken.cs @@ -0,0 +1,1122 @@ +// <auto-generated /> +// +// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do: +// +// using FitConnect; +// +// var metadata = Metadata.FromJson(jsonString); + +namespace FitConnect.Models.Api.Set +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class Metadata + { + /// <summary> + /// Eine Struktur, um zusätzliche Informationen zu hinterlegen + /// </summary> + [JsonProperty("additionalReferenceInfo", NullValueHandling = NullValueHandling.Ignore)] + public AdditionalReferenceInfo AdditionalReferenceInfo { get; set; } + + /// <summary> + /// Eine Liste aller Identifikationsnachweise der Einreichung. + /// </summary> + [JsonProperty("authenticationInformation", NullValueHandling = NullValueHandling.Ignore)] + public List<AuthenticationInformation> AuthenticationInformation { get; set; } + + /// <summary> + /// Beschreibt die Struktur der zusätzlichen Inhalte der Einreichung, wie Anlagen oder + /// Fachdaten. + /// </summary> + [JsonProperty("contentStructure")] + public ContentStructure ContentStructure { get; set; } + + /// <summary> + /// Dieses Objekt enthält die Informationen vom Bezahldienst. + /// </summary> + [JsonProperty("paymentInformation", NullValueHandling = NullValueHandling.Ignore)] + public PaymentInformation PaymentInformation { get; set; } + + /// <summary> + /// Beschreibung der Art der Verwaltungsleistung. Eine Verwaltungsleistung sollte immer mit + /// einer LeiKa-Id beschrieben werden. Ist für die gegebene Verwaltungsleistung keine + /// LeiKa-Id vorhanden, kann die Verwaltungsleistung übergangsweise über die Angabe einer + /// anderen eindeutigen Schema-URN beschrieben werden. + /// </summary> + [JsonProperty("publicServiceType", NullValueHandling = NullValueHandling.Ignore)] + public Verwaltungsleistung PublicServiceType { get; set; } + + [JsonProperty("replyChannel", NullValueHandling = NullValueHandling.Ignore)] + public ReplyChannel ReplyChannel { get; set; } + } + + /// <summary> + /// Eine Struktur, um zusätzliche Informationen zu hinterlegen + /// </summary> + public partial class AdditionalReferenceInfo + { + /// <summary> + /// Das Datum der Antragstellung. Das Datum muss nicht zwingend identisch mit dem Datum der + /// Einreichung des Antrags über FIT-Connect sein. + /// </summary> + [JsonProperty("applicationDate", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? ApplicationDate { get; set; } + + /// <summary> + /// Eine Referenz zum Vorgang im sendenden System, um bei Problemen und Rückfragen + /// außerhalb von FIT-Connect den Vorgang im dortigen System schneller zu identifizieren. + /// </summary> + [JsonProperty("senderReference", NullValueHandling = NullValueHandling.Ignore)] + public string SenderReference { get; set; } + } + + /// <summary> + /// Eine Struktur, die einen Identifikationsnachweis beschreibt. + /// </summary> + public partial class AuthenticationInformation + { + /// <summary> + /// Der Nachweis wird als Base64Url-kodierte Zeichenkette angegeben. + /// </summary> + [JsonProperty("content")] + public string Content { get; set; } + + /// <summary> + /// Definiert die Art des Identifikationsnachweises. + /// </summary> + [JsonProperty("type")] + public AuthenticationInformationType Type { get; set; } + + /// <summary> + /// semver kompatible Versionsangabe des genutzten Nachweistyps. + /// </summary> + [JsonProperty("version")] + public string Version { get; set; } + } + + /// <summary> + /// Beschreibt die Struktur der zusätzlichen Inhalte der Einreichung, wie Anlagen oder + /// Fachdaten. + /// </summary> + public partial class ContentStructure + { + [JsonProperty("attachments")] + public List<Attachment> Attachments { get; set; } + + /// <summary> + /// Definiert das Schema und die Signatur(-art), die für die Fachdaten verwendet werden. + /// </summary> + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public Data Data { get; set; } + } + + /// <summary> + /// Eine in der Einreichung enthaltene Anlage. + /// </summary> + public partial class Attachment + { + /// <summary> + /// Innerhalb einer Einreichung eindeutige Id der Anlage im Format einer UUIDv4. + /// </summary> + [JsonProperty("attachmentId")] + public Guid AttachmentId { get; set; } + + /// <summary> + /// Optionale Beschreibung der Anlage + /// </summary> + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } + + /// <summary> + /// Ursprünglicher Dateiname bei Erzeugung oder Upload + /// </summary> + [JsonProperty("filename", NullValueHandling = NullValueHandling.Ignore)] + public string Filename { get; set; } + + /// <summary> + /// Der Hashwert der unverschlüsselten Anlage. Die Angabe des Hashwertes dient der + /// Integritätssicherung des Gesamtantrags und schützt vor einem Austausch der Anlage durch + /// Systeme zwischen Sender und Subscriber (z.B. dem Zustelldienst). + /// </summary> + [JsonProperty("hash")] + public AttachmentHash Hash { get; set; } + + /// <summary> + /// Internet Media Type gemäß RFC 2045, z. B. application/pdf. + /// </summary> + [JsonProperty("mimeType")] + public string MimeType { get; set; } + + /// <summary> + /// Zweck/Art der Anlage + /// - form: Automatisch generierte PDF-Repräsentation des vollständigen Antragsformulars + /// - attachment: Anlage, die von einem Bürger hochgeladen wurde + /// - report: Vom Onlinedienst, nachträglich erzeugte Unterlage + /// </summary> + [JsonProperty("purpose")] + public Purpose Purpose { get; set; } + + [JsonProperty("signature", NullValueHandling = NullValueHandling.Ignore)] + public AttachmentSignature Signature { get; set; } + } + + /// <summary> + /// Der Hashwert der unverschlüsselten Anlage. Die Angabe des Hashwertes dient der + /// Integritätssicherung des Gesamtantrags und schützt vor einem Austausch der Anlage durch + /// Systeme zwischen Sender und Subscriber (z.B. dem Zustelldienst). + /// </summary> + public partial class AttachmentHash + { + /// <summary> + /// Der Hex-kodierte Hashwert gemäß des angegebenen Algorithmus. + /// </summary> + [JsonProperty("content")] + public string Content { get; set; } + + /// <summary> + /// Der verwendete Hash-Algorithmus. Derzeit ist nur `sha512` erlaubt. + /// </summary> + [JsonProperty("type")] + public HashType Type { get; set; } + } + + /// <summary> + /// Beschreibt das Signaturformt und Profile + /// </summary> + public partial class AttachmentSignature + { + /// <summary> + /// Hier wird die Signatur im Falle einer Detached-Signatur als Base64- oder + /// Base64Url-kodierte Zeichenkette hinterlegt. Eine Base64Url-Kodierung kommt nur bei + /// Einsatz von JSON Web Signatures (JWS / JAdES) zum Einsatz. + /// </summary> + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string Content { get; set; } + + /// <summary> + /// Beschreibt, ob die Signatur als seperate (detached) Signatur (`true`) oder als Teil des + /// Fachdatensatzes bzw. der Anlage (`false`) übertragen wird. Wenn der Wert `true` ist, + /// dann wird die Signatur Base64- oder Base64Url-kodiert im Feld `content` übertragen. + /// </summary> + [JsonProperty("detachedSignature")] + public bool DetachedSignature { get; set; } + + /// <summary> + /// Referenziert ein eindeutiges Profil einer AdES (advanced electronic signature/seal) + /// gemäß eIDAS-Verordnung über eine URI gemäß [ETSI TS 119 + /// 192](https://www.etsi.org/deliver/etsi_ts/119100_119199/119192/01.01.01_60/ts_119192v010101p.pdf). + /// + /// Für die Details zur Verwendung und Validierung von Profilen siehe auch + /// https://ec.europa.eu/cefdigital/DSS/webapp-demo/doc/dss-documentation.html#_signatures_profile_simplification + /// </summary> + [JsonProperty("eidasAdesProfile", NullValueHandling = NullValueHandling.Ignore)] + public EidasAdesProfile? EidasAdesProfile { get; set; } + + /// <summary> + /// Beschreibt, welches Signaturformat die genutzte Signatur / das genutzte Siegel nutzt. + /// Aktuell wird die Hinterlegung folgender Signaturformate unterstützt: CMS = Cryptographic + /// Message Syntax, Asic = Associated Signature Containers, PDF = PDF Signatur, XML = + /// XML-Signature, JSON = JSON Web Signature. + /// </summary> + [JsonProperty("signatureFormat")] + public SignatureFormat SignatureFormat { get; set; } + } + + /// <summary> + /// Definiert das Schema und die Signatur(-art), die für die Fachdaten verwendet werden. + /// </summary> + public partial class Data + { + /// <summary> + /// Der Hashwert der unverschlüsselten Fachdaten. Die Angabe des Hashwertes dient der + /// Integritätssicherung des Gesamtantrags und schützt vor einem Austausch der Fachdaten + /// durch Systeme zwischen Sender und Subscriber (z.B. dem Zustelldienst). + /// </summary> + [JsonProperty("hash")] + public DataHash Hash { get; set; } + + /// <summary> + /// Beschreibt das Signaturformt und Profile + /// </summary> + [JsonProperty("signature", NullValueHandling = NullValueHandling.Ignore)] + public DataSignature Signature { get; set; } + + /// <summary> + /// Referenz auf ein Schema, das die Struktur der Fachdaten einer Einreichung beschreibt. + /// </summary> + [JsonProperty("submissionSchema")] + public Fachdatenschema SubmissionSchema { get; set; } + } + + /// <summary> + /// Der Hashwert der unverschlüsselten Fachdaten. Die Angabe des Hashwertes dient der + /// Integritätssicherung des Gesamtantrags und schützt vor einem Austausch der Fachdaten + /// durch Systeme zwischen Sender und Subscriber (z.B. dem Zustelldienst). + /// </summary> + public partial class DataHash + { + /// <summary> + /// Der Hex-kodierte Hashwert gemäß des angegebenen Algorithmus. + /// </summary> + [JsonProperty("content")] + public string Content { get; set; } + + /// <summary> + /// Der verwendete Hash-Algorithmus. Derzeit ist nur `sha512` erlaubt. + /// </summary> + [JsonProperty("type")] + public HashType Type { get; set; } + } + + /// <summary> + /// Beschreibt das Signaturformt und Profile + /// </summary> + public partial class DataSignature + { + /// <summary> + /// Hier wird die Signatur im Falle einer Detached-Signatur als Base64- oder + /// Base64Url-kodierte Zeichenkette hinterlegt. Eine Base64Url-Kodierung kommt nur bei + /// Einsatz von JSON Web Signatures (JWS / JAdES) zum Einsatz. + /// </summary> + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string Content { get; set; } + + /// <summary> + /// Beschreibt, ob die Signatur als seperate (detached) Signatur (`true`) oder als Teil des + /// Fachdatensatzes bzw. der Anlage (`false`) übertragen wird. Wenn der Wert `true` ist, + /// dann wird die Signatur Base64- oder Base64Url-kodiert im Feld `content` übertragen. + /// </summary> + [JsonProperty("detachedSignature")] + public bool DetachedSignature { get; set; } + + /// <summary> + /// Referenziert ein eindeutiges Profil einer AdES (advanced electronic signature/seal) + /// gemäß eIDAS-Verordnung über eine URI gemäß [ETSI TS 119 + /// 192](https://www.etsi.org/deliver/etsi_ts/119100_119199/119192/01.01.01_60/ts_119192v010101p.pdf). + /// + /// Für die Details zur Verwendung und Validierung von Profilen siehe auch + /// https://ec.europa.eu/cefdigital/DSS/webapp-demo/doc/dss-documentation.html#_signatures_profile_simplification + /// </summary> + [JsonProperty("eidasAdesProfile", NullValueHandling = NullValueHandling.Ignore)] + public EidasAdesProfile? EidasAdesProfile { get; set; } + + /// <summary> + /// Beschreibt, welches Signaturformat die genutzte Signatur / das genutzte Siegel nutzt. + /// Aktuell wird die Hinterlegung folgender Signaturformate unterstützt: CMS = Cryptographic + /// Message Syntax, Asic = Associated Signature Containers, PDF = PDF Signatur, XML = + /// XML-Signature, JSON = JSON Web Signature. + /// </summary> + [JsonProperty("signatureFormat")] + public SignatureFormat SignatureFormat { get; set; } + } + + /// <summary> + /// Referenz auf ein Schema, das die Struktur der Fachdaten einer Einreichung beschreibt. + /// </summary> + public partial class Fachdatenschema + { + /// <summary> + /// Mimetype (z.B. application/json oder application/xml) des referenzierten Schemas (z.B. + /// XSD- oder JSON-Schema). + /// </summary> + [JsonProperty("mimeType")] + public string MimeType { get; set; } + + /// <summary> + /// URI des Fachschemas. Wird hier eine URL verwendet, sollte das Schema unter der + /// angegebenen URL abrufbar sein. Eine Verfügbarkeit des Schemas unter der angegebenen URL + /// darf jedoch nicht vorausgesetzt werden. + /// </summary> + [JsonProperty("schemaUri")] + public Uri SchemaUri { get; set; } + } + + /// <summary> + /// Dieses Objekt enthält die Informationen vom Bezahldienst. + /// </summary> + public partial class PaymentInformation + { + /// <summary> + /// Bruttobetrag + /// </summary> + [JsonProperty("grossAmount", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(MinMaxValueCheckConverter))] + public double? GrossAmount { get; set; } + + /// <summary> + /// Die vom Benutzer ausgewählte Zahlart. Das Feld ist nur bei einer erfolgreichen Zahlung + /// vorhanden / befüllt. + /// </summary> + [JsonProperty("paymentMethod")] + public PaymentMethod PaymentMethod { get; set; } + + /// <summary> + /// Weitere Erläuterung zur gewählten Zahlart. + /// </summary> + [JsonProperty("paymentMethodDetail", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(PurpleMinMaxLengthCheckConverter))] + public string PaymentMethodDetail { get; set; } + + /// <summary> + /// - INITIAL - der Einreichung hat einen Payment-Request ausgelöst und eine + /// Payment-Transaction wurde angelegt. Der Nutzer hat aber im Bezahldienst noch keine + /// Wirkung erzeugt. + /// - BOOKED - der Nutzer hat die Bezahlung im Bezahldienst autorisiert. + /// - FAILED - der Vorgang wurde vom Bezahldienst aufgrund der Nutzereingaben abgebrochen. + /// - CANCELED - der Nutzer hat die Bezahlung im Bezahldienst abgebrochen. + /// </summary> + [JsonProperty("status")] + public Status Status { get; set; } + + /// <summary> + /// Eine vom Bezahldienst vergebene Transaktions-Id. + /// </summary> + [JsonProperty("transactionId")] + [JsonConverter(typeof(PurpleMinMaxLengthCheckConverter))] + public string TransactionId { get; set; } + + /// <summary> + /// Bezahlreferenz bzw. Verwendungszweck, wie z. B. ein Kassenzeichen. + /// </summary> + [JsonProperty("transactionReference")] + public string TransactionReference { get; set; } + + /// <summary> + /// Zeitstempel der erfolgreichen Durchführung der Bezahlung. + /// </summary> + [JsonProperty("transactionTimestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? TransactionTimestamp { get; set; } + + /// <summary> + /// Die Rest-URL der Payment Transaction für die Statusabfrage. + /// </summary> + [JsonProperty("transactionUrl", NullValueHandling = NullValueHandling.Ignore)] + public Uri TransactionUrl { get; set; } + } + + /// <summary> + /// Beschreibung der Art der Verwaltungsleistung. Eine Verwaltungsleistung sollte immer mit + /// einer LeiKa-Id beschrieben werden. Ist für die gegebene Verwaltungsleistung keine + /// LeiKa-Id vorhanden, kann die Verwaltungsleistung übergangsweise über die Angabe einer + /// anderen eindeutigen Schema-URN beschrieben werden. + /// </summary> + public partial class Verwaltungsleistung + { + /// <summary> + /// (Kurz-)Beschreibung der Verwaltungsleistung + /// </summary> + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } + + /// <summary> + /// URN einer Leistung. Im Falle einer Leistung aus dem Leistungskatalog sollte hier + /// `urn:de:fim:leika:leistung:` vorangestellt werden. + /// </summary> + [JsonProperty("identifier")] + [JsonConverter(typeof(FluffyMinMaxLengthCheckConverter))] + public string Identifier { get; set; } + + /// <summary> + /// Name/Bezeichnung der Verwaltungsleistung + /// </summary> + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + } + + public partial class ReplyChannel + { + /// <summary> + /// Akkreditierte Anbieter siehe + /// https://www.bsi.bund.de/DE/Themen/Oeffentliche-Verwaltung/Moderner-Staat/De-Mail/Akkreditierte-DMDA/akkreditierte-dmda_node.html + /// </summary> + [JsonProperty("deMail", NullValueHandling = NullValueHandling.Ignore)] + public DeMail DeMail { get; set; } + + /// <summary> + /// Siehe https://www.elster.de/elsterweb/infoseite/elstertransfer_hilfe_schnittstellen + /// </summary> + [JsonProperty("elster", NullValueHandling = NullValueHandling.Ignore)] + public Elster Elster { get; set; } + + [JsonProperty("eMail", NullValueHandling = NullValueHandling.Ignore)] + public EMail EMail { get; set; } + + /// <summary> + /// Postfachadresse in einem interoperablen Servicekonto (FINK.PFISK) + /// </summary> + [JsonProperty("fink", NullValueHandling = NullValueHandling.Ignore)] + public Fink Fink { get; set; } + } + + /// <summary> + /// Akkreditierte Anbieter siehe + /// https://www.bsi.bund.de/DE/Themen/Oeffentliche-Verwaltung/Moderner-Staat/De-Mail/Akkreditierte-DMDA/akkreditierte-dmda_node.html + /// </summary> + public partial class DeMail + { + [JsonProperty("address")] + public string Address { get; set; } + } + + public partial class EMail + { + [JsonProperty("address")] + public string Address { get; set; } + + /// <summary> + /// Hilfe zur Erstellung gibt es in der Dokumentation unter + /// https://docs.fitko.de/fit-connect/details/pgp-export + /// </summary> + [JsonProperty("pgpPublicKey", NullValueHandling = NullValueHandling.Ignore)] + public string PgpPublicKey { get; set; } + } + + /// <summary> + /// Siehe https://www.elster.de/elsterweb/infoseite/elstertransfer_hilfe_schnittstellen + /// </summary> + public partial class Elster + { + [JsonProperty("accountId")] + public string AccountId { get; set; } + + [JsonProperty("geschaeftszeichen", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(TentacledMinMaxLengthCheckConverter))] + public string Geschaeftszeichen { get; set; } + + [JsonProperty("lieferTicket", NullValueHandling = NullValueHandling.Ignore)] + public string LieferTicket { get; set; } + } + + /// <summary> + /// Postfachadresse in einem interoperablen Servicekonto (FINK.PFISK) + /// </summary> + public partial class Fink + { + /// <summary> + /// FINK Postfachadresse + /// </summary> + [JsonProperty("finkPostfachRef")] + [JsonConverter(typeof(StickyMinMaxLengthCheckConverter))] + public string FinkPostfachRef { get; set; } + + /// <summary> + /// URL des Servicekontos, in dem das Ziel-Postfach liegt + /// </summary> + [JsonProperty("host", NullValueHandling = NullValueHandling.Ignore)] + public Uri Host { get; set; } + } + + /// <summary> + /// Definiert die Art des Identifikationsnachweises. + /// </summary> + public enum AuthenticationInformationType { IdentificationReport }; + + /// <summary> + /// Der verwendete Hash-Algorithmus. Derzeit ist nur `sha512` erlaubt. + /// </summary> + public enum HashType { Sha512 }; + + /// <summary> + /// Zweck/Art der Anlage + /// - form: Automatisch generierte PDF-Repräsentation des vollständigen Antragsformulars + /// - attachment: Anlage, die von einem Bürger hochgeladen wurde + /// - report: Vom Onlinedienst, nachträglich erzeugte Unterlage + /// </summary> + public enum Purpose { Attachment, Form, Report }; + + /// <summary> + /// Referenziert ein eindeutiges Profil einer AdES (advanced electronic signature/seal) + /// gemäß eIDAS-Verordnung über eine URI gemäß [ETSI TS 119 + /// 192](https://www.etsi.org/deliver/etsi_ts/119100_119199/119192/01.01.01_60/ts_119192v010101p.pdf). + /// + /// Für die Details zur Verwendung und Validierung von Profilen siehe auch + /// https://ec.europa.eu/cefdigital/DSS/webapp-demo/doc/dss-documentation.html#_signatures_profile_simplification + /// </summary> + public enum EidasAdesProfile { HttpUriEtsiOrgAdes191X2LevelBaselineBB, HttpUriEtsiOrgAdes191X2LevelBaselineBLt, HttpUriEtsiOrgAdes191X2LevelBaselineBLta, HttpUriEtsiOrgAdes191X2LevelBaselineBT }; + + /// <summary> + /// Beschreibt, welches Signaturformat die genutzte Signatur / das genutzte Siegel nutzt. + /// Aktuell wird die Hinterlegung folgender Signaturformate unterstützt: CMS = Cryptographic + /// Message Syntax, Asic = Associated Signature Containers, PDF = PDF Signatur, XML = + /// XML-Signature, JSON = JSON Web Signature. + /// </summary> + public enum SignatureFormat { Asic, Cms, Json, Pdf, Xml }; + + /// <summary> + /// Mimetype (z.B. application/json oder application/xml) des referenzierten Schemas (z.B. + /// XSD- oder JSON-Schema). + /// </summary> + public enum MimeType { ApplicationJson, ApplicationXml }; + + /// <summary> + /// Die vom Benutzer ausgewählte Zahlart. Das Feld ist nur bei einer erfolgreichen Zahlung + /// vorhanden / befüllt. + /// </summary> + public enum PaymentMethod { Creditcard, Giropay, Invoice, Other, Paydirect, Paypal }; + + /// <summary> + /// - INITIAL - der Einreichung hat einen Payment-Request ausgelöst und eine + /// Payment-Transaction wurde angelegt. Der Nutzer hat aber im Bezahldienst noch keine + /// Wirkung erzeugt. + /// - BOOKED - der Nutzer hat die Bezahlung im Bezahldienst autorisiert. + /// - FAILED - der Vorgang wurde vom Bezahldienst aufgrund der Nutzereingaben abgebrochen. + /// - CANCELED - der Nutzer hat die Bezahlung im Bezahldienst abgebrochen. + /// </summary> + public enum Status { Booked, Canceled, Failed, Initial }; + + public partial class Metadata + { + public static Metadata FromJson(string json) => JsonConvert.DeserializeObject<Metadata>(json, FitConnect.Models.Api.Set.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this Metadata self) => JsonConvert.SerializeObject(self, FitConnect.Models.Api.Set.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + { + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + DateParseHandling = DateParseHandling.None, + Converters = + { + AuthenticationInformationTypeConverter.Singleton, + HashTypeConverter.Singleton, + PurposeConverter.Singleton, + EidasAdesProfileConverter.Singleton, + SignatureFormatConverter.Singleton, + MimeTypeConverter.Singleton, + PaymentMethodConverter.Singleton, + StatusConverter.Singleton, + new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } + }, + }; + } + + internal class AuthenticationInformationTypeConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(AuthenticationInformationType) || t == typeof(AuthenticationInformationType?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize<string>(reader); + if (value == "identificationReport") + { + return AuthenticationInformationType.IdentificationReport; + } + throw new Exception("Cannot unmarshal type AuthenticationInformationType"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (AuthenticationInformationType)untypedValue; + if (value == AuthenticationInformationType.IdentificationReport) + { + serializer.Serialize(writer, "identificationReport"); + return; + } + throw new Exception("Cannot marshal type AuthenticationInformationType"); + } + + public static readonly AuthenticationInformationTypeConverter Singleton = new AuthenticationInformationTypeConverter(); + } + + internal class HashTypeConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(HashType) || t == typeof(HashType?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize<string>(reader); + if (value == "sha512") + { + return HashType.Sha512; + } + throw new Exception("Cannot unmarshal type HashType"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (HashType)untypedValue; + if (value == HashType.Sha512) + { + serializer.Serialize(writer, "sha512"); + return; + } + throw new Exception("Cannot marshal type HashType"); + } + + public static readonly HashTypeConverter Singleton = new HashTypeConverter(); + } + + internal class PurposeConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Purpose) || t == typeof(Purpose?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize<string>(reader); + switch (value) + { + case "attachment": + return Purpose.Attachment; + case "form": + return Purpose.Form; + case "report": + return Purpose.Report; + } + throw new Exception("Cannot unmarshal type Purpose"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (Purpose)untypedValue; + switch (value) + { + case Purpose.Attachment: + serializer.Serialize(writer, "attachment"); + return; + case Purpose.Form: + serializer.Serialize(writer, "form"); + return; + case Purpose.Report: + serializer.Serialize(writer, "report"); + return; + } + throw new Exception("Cannot marshal type Purpose"); + } + + public static readonly PurposeConverter Singleton = new PurposeConverter(); + } + + internal class EidasAdesProfileConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(EidasAdesProfile) || t == typeof(EidasAdesProfile?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize<string>(reader); + switch (value) + { + case "http://uri.etsi.org/ades/191x2/level/baseline/B-B#": + return EidasAdesProfile.HttpUriEtsiOrgAdes191X2LevelBaselineBB; + case "http://uri.etsi.org/ades/191x2/level/baseline/B-LT#": + return EidasAdesProfile.HttpUriEtsiOrgAdes191X2LevelBaselineBLt; + case "http://uri.etsi.org/ades/191x2/level/baseline/B-LTA#": + return EidasAdesProfile.HttpUriEtsiOrgAdes191X2LevelBaselineBLta; + case "http://uri.etsi.org/ades/191x2/level/baseline/B-T#": + return EidasAdesProfile.HttpUriEtsiOrgAdes191X2LevelBaselineBT; + } + throw new Exception("Cannot unmarshal type EidasAdesProfile"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (EidasAdesProfile)untypedValue; + switch (value) + { + case EidasAdesProfile.HttpUriEtsiOrgAdes191X2LevelBaselineBB: + serializer.Serialize(writer, "http://uri.etsi.org/ades/191x2/level/baseline/B-B#"); + return; + case EidasAdesProfile.HttpUriEtsiOrgAdes191X2LevelBaselineBLt: + serializer.Serialize(writer, "http://uri.etsi.org/ades/191x2/level/baseline/B-LT#"); + return; + case EidasAdesProfile.HttpUriEtsiOrgAdes191X2LevelBaselineBLta: + serializer.Serialize(writer, "http://uri.etsi.org/ades/191x2/level/baseline/B-LTA#"); + return; + case EidasAdesProfile.HttpUriEtsiOrgAdes191X2LevelBaselineBT: + serializer.Serialize(writer, "http://uri.etsi.org/ades/191x2/level/baseline/B-T#"); + return; + } + throw new Exception("Cannot marshal type EidasAdesProfile"); + } + + public static readonly EidasAdesProfileConverter Singleton = new EidasAdesProfileConverter(); + } + + internal class SignatureFormatConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(SignatureFormat) || t == typeof(SignatureFormat?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize<string>(reader); + switch (value) + { + case "asic": + return SignatureFormat.Asic; + case "cms": + return SignatureFormat.Cms; + case "json": + return SignatureFormat.Json; + case "pdf": + return SignatureFormat.Pdf; + case "xml": + return SignatureFormat.Xml; + } + throw new Exception("Cannot unmarshal type SignatureFormat"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (SignatureFormat)untypedValue; + switch (value) + { + case SignatureFormat.Asic: + serializer.Serialize(writer, "asic"); + return; + case SignatureFormat.Cms: + serializer.Serialize(writer, "cms"); + return; + case SignatureFormat.Json: + serializer.Serialize(writer, "json"); + return; + case SignatureFormat.Pdf: + serializer.Serialize(writer, "pdf"); + return; + case SignatureFormat.Xml: + serializer.Serialize(writer, "xml"); + return; + } + throw new Exception("Cannot marshal type SignatureFormat"); + } + + public static readonly SignatureFormatConverter Singleton = new SignatureFormatConverter(); + } + + internal class MimeTypeConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(MimeType) || t == typeof(MimeType?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize<string>(reader); + switch (value) + { + case "application/json": + return MimeType.ApplicationJson; + case "application/xml": + return MimeType.ApplicationXml; + } + throw new Exception("Cannot unmarshal type MimeType"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (MimeType)untypedValue; + switch (value) + { + case MimeType.ApplicationJson: + serializer.Serialize(writer, "application/json"); + return; + case MimeType.ApplicationXml: + serializer.Serialize(writer, "application/xml"); + return; + } + throw new Exception("Cannot marshal type MimeType"); + } + + public static readonly MimeTypeConverter Singleton = new MimeTypeConverter(); + } + + internal class MinMaxValueCheckConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(double) || t == typeof(double?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize<double>(reader); + if (value >= 0.01) + { + return value; + } + throw new Exception("Cannot unmarshal type double"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (double)untypedValue; + if (value >= 0.01) + { + serializer.Serialize(writer, value); + return; + } + throw new Exception("Cannot marshal type double"); + } + + public static readonly MinMaxValueCheckConverter Singleton = new MinMaxValueCheckConverter(); + } + + internal class PaymentMethodConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(PaymentMethod) || t == typeof(PaymentMethod?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize<string>(reader); + switch (value) + { + case "CREDITCARD": + return PaymentMethod.Creditcard; + case "GIROPAY": + return PaymentMethod.Giropay; + case "INVOICE": + return PaymentMethod.Invoice; + case "OTHER": + return PaymentMethod.Other; + case "PAYDIRECT": + return PaymentMethod.Paydirect; + case "PAYPAL": + return PaymentMethod.Paypal; + } + throw new Exception("Cannot unmarshal type PaymentMethod"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (PaymentMethod)untypedValue; + switch (value) + { + case PaymentMethod.Creditcard: + serializer.Serialize(writer, "CREDITCARD"); + return; + case PaymentMethod.Giropay: + serializer.Serialize(writer, "GIROPAY"); + return; + case PaymentMethod.Invoice: + serializer.Serialize(writer, "INVOICE"); + return; + case PaymentMethod.Other: + serializer.Serialize(writer, "OTHER"); + return; + case PaymentMethod.Paydirect: + serializer.Serialize(writer, "PAYDIRECT"); + return; + case PaymentMethod.Paypal: + serializer.Serialize(writer, "PAYPAL"); + return; + } + throw new Exception("Cannot marshal type PaymentMethod"); + } + + public static readonly PaymentMethodConverter Singleton = new PaymentMethodConverter(); + } + + internal class PurpleMinMaxLengthCheckConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(string); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + var value = serializer.Deserialize<string>(reader); + if (value.Length >= 1 && value.Length <= 36) + { + return value; + } + throw new Exception("Cannot unmarshal type string"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + var value = (string)untypedValue; + if (value.Length >= 1 && value.Length <= 36) + { + serializer.Serialize(writer, value); + return; + } + throw new Exception("Cannot marshal type string"); + } + + public static readonly PurpleMinMaxLengthCheckConverter Singleton = new PurpleMinMaxLengthCheckConverter(); + } + + internal class StatusConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Status) || t == typeof(Status?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize<string>(reader); + switch (value) + { + case "BOOKED": + return Status.Booked; + case "CANCELED": + return Status.Canceled; + case "FAILED": + return Status.Failed; + case "INITIAL": + return Status.Initial; + } + throw new Exception("Cannot unmarshal type Status"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (Status)untypedValue; + switch (value) + { + case Status.Booked: + serializer.Serialize(writer, "BOOKED"); + return; + case Status.Canceled: + serializer.Serialize(writer, "CANCELED"); + return; + case Status.Failed: + serializer.Serialize(writer, "FAILED"); + return; + case Status.Initial: + serializer.Serialize(writer, "INITIAL"); + return; + } + throw new Exception("Cannot marshal type Status"); + } + + public static readonly StatusConverter Singleton = new StatusConverter(); + } + + internal class FluffyMinMaxLengthCheckConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(string); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + var value = serializer.Deserialize<string>(reader); + if (value.Length >= 7 && value.Length <= 255) + { + return value; + } + throw new Exception("Cannot unmarshal type string"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + var value = (string)untypedValue; + if (value.Length >= 7 && value.Length <= 255) + { + serializer.Serialize(writer, value); + return; + } + throw new Exception("Cannot marshal type string"); + } + + public static readonly FluffyMinMaxLengthCheckConverter Singleton = new FluffyMinMaxLengthCheckConverter(); + } + + internal class TentacledMinMaxLengthCheckConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(string); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + var value = serializer.Deserialize<string>(reader); + if (value.Length <= 10) + { + return value; + } + throw new Exception("Cannot unmarshal type string"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + var value = (string)untypedValue; + if (value.Length <= 10) + { + serializer.Serialize(writer, value); + return; + } + throw new Exception("Cannot marshal type string"); + } + + public static readonly TentacledMinMaxLengthCheckConverter Singleton = new TentacledMinMaxLengthCheckConverter(); + } + + internal class StickyMinMaxLengthCheckConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(string); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + var value = serializer.Deserialize<string>(reader); + if (value.Length <= 150) + { + return value; + } + throw new Exception("Cannot unmarshal type string"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + var value = (string)untypedValue; + if (value.Length <= 150) + { + serializer.Serialize(writer, value); + return; + } + throw new Exception("Cannot marshal type string"); + } + + public static readonly StickyMinMaxLengthCheckConverter Singleton = new StickyMinMaxLengthCheckConverter(); + } +} diff --git a/FitConnect/Services/Models/v1/Case/EventLogDto.cs b/FitConnect/Services/Models/v1/Case/EventLogDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..df39168a2c5f9566476a35c78e095409418e3d34 --- /dev/null +++ b/FitConnect/Services/Models/v1/Case/EventLogDto.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Case; + +public class EventLogDto { + [JsonPropertyName("count")] + public int Count { get; set; } + + [JsonPropertyName("eventLog")] + public List<string>? EventLog { get; set; } + + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("totalCount")] + public long TotalCount { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Destination/ContactInformationDto.cs b/FitConnect/Services/Models/v1/Destination/ContactInformationDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..f017503cf6981a04ec603c6a30b4ed6c0d92d017 --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/ContactInformationDto.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Destination; + +public class ContactInformationDto { + [JsonPropertyName("address")] + public string? Address { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } + + [JsonPropertyName("legalName")] + public string? LegalName { get; set; } + + [JsonPropertyName("phone")] + public string? Phone { get; set; } + + [JsonPropertyName("unit")] + public string? Unit { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Destination/CreateDestinationDto.cs b/FitConnect/Services/Models/v1/Destination/CreateDestinationDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..a4fce8a96ccc7363d4c8da5c3925dde9bdfffda3 --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/CreateDestinationDto.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Destination; + +public class CreateDestinationDto { + [JsonPropertyName("callback")] + public CallbackDto? Callback { get; set; } + + [JsonPropertyName("contactInformation")] + public ContactInformationDto? ContactInformation { get; set; } + + + [JsonPropertyName("encryptionKid")] + public string? EncryptionKid { get; set; } + + + [JsonPropertyName("encryptionPublicKey")] + public string? EncryptionPublicKey { get; set; } + + + [JsonPropertyName("metadataVersions")] + public List<string>? MetadataVersions { get; set; } + + + [JsonPropertyName("replyChannels")] + public DestinationReplyChannelsDto? ReplyChannels { get; set; } + + + [JsonPropertyName("services")] + public List<DestinationServiceDto>? Services { get; set; } + + + [JsonPropertyName("signingPublicKey")] + public string? SigningPublicKey { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Destination/DeMailDto.cs b/FitConnect/Services/Models/v1/Destination/DeMailDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..8990552d438fba85a5e529703404718fc0886044 --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/DeMailDto.cs @@ -0,0 +1,4 @@ +namespace FitConnect.Services.Models.v1.Destination; + +public class DeMailDto { +} diff --git a/FitConnect/Services/Models/v1/Destination/DestinationListDto.cs b/FitConnect/Services/Models/v1/Destination/DestinationListDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..9acf39e097ce0e50c90b4b70930068ddacc06710 --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/DestinationListDto.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Destination; + +public class DestinationListDto { + [JsonPropertyName("count")] + public int Count { get; set; } + + + [JsonPropertyName("destinations")] + public List<PrivateDestinationDto>? Destinations { get; set; } + + + [JsonPropertyName("offset")] + public int Offset { get; set; } + + + [JsonPropertyName("totalCount")] + public long TotalCount { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Destination/DestinationReplyChannelsDto.cs b/FitConnect/Services/Models/v1/Destination/DestinationReplyChannelsDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..b869115be7c55a1b957c475cd7452149561209d4 --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/DestinationReplyChannelsDto.cs @@ -0,0 +1,28 @@ +namespace FitConnect.Services.Models.v1.Destination; + +public class DestinationReplyChannelsDto { + public DestinationReplyChannelsDto( + EmailDto eMail, + DeMailDto deMail, + FinkDto fink, + ElsterDto elster) { + EMail = EMail; + DeMail = DeMail; + Fink = Fink; + Elster = Elster; + } + + public DeMailDto? DeMail { get; set; } + + + public ElsterDto? Elster { get; set; } + public EmailDto? EMail { get; set; } + + + public FinkDto? Fink { get; set; } + + + public bool IsEmpty() { + return EMail == null && DeMail == null && Fink == null && Elster == null; + } +} diff --git a/FitConnect/Services/Models/v1/Destination/DestinationServiceDto.cs b/FitConnect/Services/Models/v1/Destination/DestinationServiceDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..cc100b7fbfe68cede5635dac205a4b25243436bb --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/DestinationServiceDto.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Destination; + +public class DestinationServiceDto { + [JsonPropertyName("identifier")] + public string? Identifier { get; set; } + + + [JsonPropertyName("regions")] + public List<string>? Regions { get; set; } + + [JsonPropertyName("submissionSchemas")] + public List<SubmissionSchemaDto>? SubmissionSchemas { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Destination/ElsterDto.cs b/FitConnect/Services/Models/v1/Destination/ElsterDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..fbad21ebb00fdfe0c151a9b0d294e3af886075b8 --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/ElsterDto.cs @@ -0,0 +1,4 @@ +namespace FitConnect.Services.Models.v1.Destination; + +public class ElsterDto { +} diff --git a/FitConnect/Services/Models/v1/Destination/EmailDto.cs b/FitConnect/Services/Models/v1/Destination/EmailDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..019230e9f8435dbfb2bad88cdef0e56f7ed7238b --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/EmailDto.cs @@ -0,0 +1,4 @@ +namespace FitConnect.Services.Models.v1.Destination; + +public class EmailDto { +} diff --git a/FitConnect/Services/Models/v1/Destination/FinkDto.cs b/FitConnect/Services/Models/v1/Destination/FinkDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..ff8c80fc72bfff4b67ffff34d4a8936b6a06c387 --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/FinkDto.cs @@ -0,0 +1,4 @@ +namespace FitConnect.Services.Models.v1.Destination; + +public class FinkDto { +} diff --git a/FitConnect/Services/Models/v1/Destination/PatchDestinationDto.cs b/FitConnect/Services/Models/v1/Destination/PatchDestinationDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..8ae416957d56358e743dc3cb8f82fd8b686c677a --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/PatchDestinationDto.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Destination; + +public class PatchDestinationDto { + [JsonPropertyName("callback")] + public CallbackDto? Callback { get; set; } + + + [JsonPropertyName("contactInformation")] + public ContactInformationDto? ContactInformation { get; set; } + + + [JsonPropertyName("encryptionKid")] + public string? EncryptionKid { get; set; } + + + [JsonPropertyName("metadataVersions")] + public List<string>? metadataVersions { get; set; } + + + [JsonPropertyName("replyChannels")] + public DestinationReplyChannelsDto? ReplyChannels { get; set; } + + + [JsonPropertyName("services")] + public List<DestinationServiceDto>? Services { get; set; } + + [JsonPropertyName("status")] + public string? Status { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Destination/PrivateDestinationDto.cs b/FitConnect/Services/Models/v1/Destination/PrivateDestinationDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..b6d9098af49e0de7597a6b667dc7827e7b3e39ae --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/PrivateDestinationDto.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Destination; + +public class PrivateDestinationDto { + [JsonPropertyName("callback")] + public CallbackDto? Callback { get; set; } + + + [JsonPropertyName("contactInformation")] + public ContactInformationDto? ContactInformation { get; set; } + + [JsonPropertyName("destinationId")] + public string? DestinationId { get; set; } + + + [JsonPropertyName("encryptionKid")] + public string? EncryptionKid { get; set; } + + + [JsonPropertyName("metadataVersions")] + public List<string>? MetadataVersions { get; set; } + + + [JsonPropertyName("replyChannels")] + public DestinationReplyChannelsDto? ReplyChannels { get; set; } + + + [JsonPropertyName("services")] + public List<DestinationServiceDto>? Services { get; set; } + + + [JsonPropertyName("status")] + public string? Status { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Destination/PublicDestinationDto.cs b/FitConnect/Services/Models/v1/Destination/PublicDestinationDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..b188491e560a2bd5b3e9b7d071773e5cfb33cc02 --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/PublicDestinationDto.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Destination; + +// ReSharper disable once ClassNeverInstantiated.Global +public class PublicDestinationDto { + [JsonPropertyName("destinationId")] + public string? DestinationId { get; set; } + + + [JsonPropertyName("encryptionKid")] + public string? EncryptionKid { get; set; } + + + [JsonPropertyName("metadataVersions")] + public List<string>? MetadataVersions { get; set; } + + + [JsonPropertyName("replyChannels")] + public DestinationReplyChannelsDto? ReplyChannels { get; set; } + + + [JsonPropertyName("services")] + public List<DestinationServiceDto>? Services { get; set; } + + + [JsonPropertyName("status")] + public string? Status { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Destination/SubmissionSchemaDto.cs b/FitConnect/Services/Models/v1/Destination/SubmissionSchemaDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..5beadb55742385a881f1c09d3120c097636e32bd --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/SubmissionSchemaDto.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Destination; + +public class SubmissionSchemaDto { + [JsonPropertyName("mimeType")] + // private SubmissionSchemaMimeTypeDto mimeType; + public string mimeType; + + [JsonPropertyName("schemaUri")] + public string? SchemaUri { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Destination/UpdateDestinationDto.cs b/FitConnect/Services/Models/v1/Destination/UpdateDestinationDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..18a7bd98092524ffc2c6725d394dfcdcce103fe0 --- /dev/null +++ b/FitConnect/Services/Models/v1/Destination/UpdateDestinationDto.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Destination; + +public class UpdateDestinationDto { + [JsonPropertyName("callback")] + public CallbackDto? Callback { get; set; } + + + [JsonPropertyName("contactInformation")] + public ContactInformationDto? ContactInformation { get; set; } + + + [JsonPropertyName("encryptionKid")] + public string? EncryptionKid { get; set; } + + + [JsonPropertyName("metadataVersions")] + public List<string>? metadataVersions { get; set; } + + + [JsonPropertyName("replyChannels")] + public DestinationReplyChannelsDto? ReplyChannels { get; set; } + + + [JsonPropertyName("services")] + public List<DestinationServiceDto>? Services { get; set; } + + [JsonPropertyName("status")] + public string? Status { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Routes/Routes.cs b/FitConnect/Services/Models/v1/Routes/Routes.cs new file mode 100644 index 0000000000000000000000000000000000000000..286dbccfa43d8d03238c8c96fb9ef8c5ac1bd8bf --- /dev/null +++ b/FitConnect/Services/Models/v1/Routes/Routes.cs @@ -0,0 +1,110 @@ +// Root myDeserializedClass = JsonSerializer.Deserialize<Root>(myJsonResponse); + +using System.Text.Json.Serialization; +using Newtonsoft.Json; + +namespace FitConnect.Services.Models.v1.Routes; + +public class DestinationParameters { + [JsonPropertyName("encryptionKid")] + public string EncryptionKid { get; set; } + + [JsonPropertyName("metadataVersions")] + public List<string> MetadataVersions { get; set; } + + [JsonPropertyName("publicKeys")] + public PublicKeys PublicKeys { get; set; } + + [JsonPropertyName("replyChannels")] + public ReplyChannels ReplyChannels { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("submissionSchemas")] + public List<SubmissionSchema> SubmissionSchemas { get; set; } + + [JsonPropertyName("submissionUrl")] + public string SubmissionUrl { get; set; } +} + +public class EMail { + [JsonPropertyName("usePgp")] + public bool UsePgp { get; set; } +} + +public class Key { + [JsonPropertyName("kty")] + public string Kty { get; set; } + + [JsonPropertyName("key_ops")] + public List<string> KeyOps { get; set; } + + [JsonPropertyName("alg")] + public string Alg { get; set; } + + [JsonPropertyName("x5c")] + public List<string> X5c { get; set; } + + [JsonPropertyName("kid")] + public string Kid { get; set; } + + [JsonPropertyName("n")] + public string N { get; set; } + + [JsonPropertyName("e")] + public string E { get; set; } + + public string ToString() { + return JsonConvert.SerializeObject(this); + } +} + +public class PublicKeys { + [JsonPropertyName("keys")] + public List<Key> Keys { get; set; } +} + +public class ReplyChannels { + [JsonPropertyName("eMail")] + public EMail EMail { get; set; } +} + +public class RoutesListDto { + [JsonPropertyName("count")] + public int Count { get; set; } + + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("totalCount")] + public int TotalCount { get; set; } + + [JsonPropertyName("routes")] + public List<Route> Routes { get; set; } +} + +public class Route { + [JsonPropertyName("destinationId")] + public string DestinationId { get; set; } + + [JsonPropertyName("destinationSignature")] + public string DestinationSignature { get; set; } + + [JsonPropertyName("destinationParameters")] + public DestinationParameters DestinationParameters { get; set; } + + [JsonPropertyName("destinationParametersSignature")] + public string DestinationParametersSignature { get; set; } + + [JsonPropertyName("destinationName")] + public string DestinationName { get; set; } +} + +public class SubmissionSchema { + [JsonPropertyName("schemaUri")] + public string SchemaUri { get; set; } + + [JsonPropertyName("mimeType")] + public string MimeType { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Submission/CreateSubmissionDto.cs b/FitConnect/Services/Models/v1/Submission/CreateSubmissionDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..bf340a6f82255a75febc5c8ce53ed6f2dcbafad9 --- /dev/null +++ b/FitConnect/Services/Models/v1/Submission/CreateSubmissionDto.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Submission; + +public class CreateSubmissionDto { + [JsonPropertyName("callback")] + public CallbackDto? Callback { get; set; } + + + [JsonPropertyName("serviceType")] + public ServiceTypeDto? ServiceType { get; set; } + + + [JsonPropertyName("announcedAttachments")] + public List<string>? AnnouncedAttachments { get; set; } + + [JsonPropertyName("caseId")] + public string? CaseId { get; set; } + + [JsonPropertyName("destinationId")] + public string? DestinationId { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..307ff526a51be2db21cb5906542e951759def4ff --- /dev/null +++ b/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Submission; + +public class SubmissionCreatedDto { + [JsonPropertyName("destinationId")] + public string? DestinationId { get; set; } + + [JsonPropertyName("submissionId")] + public string? SubmissionId { get; set; } + + [JsonPropertyName("caseId")] + public string? CaseId { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..fc2203507fe54203974b3096e5ee91241983d8bb --- /dev/null +++ b/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Submission; + +public class SubmissionDto { + [JsonPropertyName("attachments")] + public List<string>? Attachments { get; set; } + + + [JsonPropertyName("callback")] + public CallbackDto? Callback { get; set; } + + + [JsonPropertyName("caseId")] + public string? CaseId { get; set; } + + [JsonPropertyName("destinationId")] + public string? DestinationId { get; set; } + + + [JsonPropertyName("encryptedData")] + public string? EncryptedData { get; set; } + + + [JsonPropertyName("encryptedMetadata")] + public string? EncryptedMetadata { get; set; } + + + [JsonPropertyName("serviceType")] + public ServiceTypeDto? ServiceType { get; set; } + + + [JsonPropertyName("submissionId")] + public string? SubmissionId { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..b01e4ac60bbbf8d2e82b9dff39abacc0ad9d0299 --- /dev/null +++ b/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Submission; + +public class SubmissionForPickupDto { + [JsonPropertyName("caseId")] + public string? CaseId { get; set; } + + [JsonPropertyName("destinationId")] + public string? DestinationId { get; set; } + + [JsonPropertyName("submissionId")] + public string? SubmissionId { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionReducedDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionReducedDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..e98939b1b72de6f74467e26116e9ec0e978842c7 --- /dev/null +++ b/FitConnect/Services/Models/v1/Submission/SubmissionReducedDto.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Submission; + +public class SubmissionReducedDto { + [JsonPropertyName("serviceType")] + private ServiceTypeDto _serviceTypeDto { get; set; } + + + [JsonPropertyName("attachments")] + private List<string>? Attachments { get; set; } + + + [JsonPropertyName("callback")] + private CallbackDto? Callback { get; set; } + + + [JsonPropertyName("caseId")] + private string? CaseId { get; set; } + + [JsonPropertyName("destinationId")] + private string? DestinationId { get; set; } + + + [JsonPropertyName("submissionId")] + private string? SubmissionId { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionsForPickupDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionsForPickupDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..a6e98a2320c872a4f5f98e924697eefd2778a0a8 --- /dev/null +++ b/FitConnect/Services/Models/v1/Submission/SubmissionsForPickupDto.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Submission; + +public class SubmissionsForPickupDto { + [JsonPropertyName("count")] + public int Count { get; set; } + + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("submissions")] + public List<SubmissionForPickupDto>? Submissions { get; set; } + + [JsonPropertyName("totalCount")] + public long TotalCount { get; set; } +} diff --git a/FitConnect/Services/Models/v1/Submission/SubmitSubmissionDto.cs b/FitConnect/Services/Models/v1/Submission/SubmitSubmissionDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..b5e206ca846215e34fb4c12c3490793f7f905c7f --- /dev/null +++ b/FitConnect/Services/Models/v1/Submission/SubmitSubmissionDto.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Services.Models.v1.Submission; + +public class SubmitSubmissionDto { + [JsonPropertyName("encryptedData")] + public string? EncryptedData { get; set; } + + [JsonPropertyName("encryptedMetadata")] + public string? EncryptedMetadata { get; set; } +} diff --git a/FitConnect/Services/OAuthService.cs b/FitConnect/Services/OAuthService.cs new file mode 100644 index 0000000000000000000000000000000000000000..bef055daa97122340cd26fe332fa9315d4af4abc --- /dev/null +++ b/FitConnect/Services/OAuthService.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Http.Json; +using System.Security.Authentication; +using FitConnect.Services.Interfaces; +using FitConnect.Services.Models; +using Microsoft.Extensions.Logging; + +namespace FitConnect.Services; + +public class OAuthService : RestCallService, IOAuthService { + private readonly string _clientId; + private readonly string _clientSecret; + private readonly ILogger? _logger; + private readonly string _tokenUrl; + private OAuthAccessToken? _token; + + public OAuthService(string tokenUrl, string version, string clientId, string clientSecret, + ILogger? logger) : base($"{tokenUrl}/{version}", logger) { + _tokenUrl = tokenUrl; + _clientId = clientId; + _clientSecret = clientSecret; + _logger = logger; + } + + public OAuthAccessToken? Token { + get => _token; + private set { + _token = value; + AccessToken = value?.AccessToken; + } + } + + /// <summary> + /// Requesting an OAuth token from the FitConnect API. + /// <para> + /// You can get the Client ID and Client Secret from the FitConnect Self Service portal + /// under <br /> + /// https://portal.auth-testing.fit-connect.fitko.dev + /// </para> + /// </summary> + /// <param name="clientId">Your client Id</param> + /// <param name="clientSecret">Your client Secret</param> + /// <param name="scope">Scope if needed</param> + /// <returns>The received token or null</returns> + public async Task AuthenticateAsync( + string? scope = null) { + var client = CreateClient(); + + var requestContent = new Dictionary<string, string> { + { "grant_type", "client_credentials" }, + { "client_id", _clientId }, + { "client_secret", _clientSecret } + }; + + if (scope != null) + requestContent["scope"] = scope; + + var content = new FormUrlEncodedContent(requestContent); + + var request = new HttpRequestMessage(HttpMethod.Post, _tokenUrl) { + Content = content, + Method = HttpMethod.Post + }; + + var response = await client.SendAsync(request); + if (response.IsSuccessStatusCode) { + var result = await response.Content.ReadFromJsonAsync<OAuthAccessToken>(); + if (result == null) + throw new AuthenticationException("Failed to authenticate"); + Token = result; + } + + if (response.StatusCode == HttpStatusCode.Unauthorized) + throw new InvalidCredentialException(await response.Content.ReadAsStringAsync()); + } + + public bool IsAuthenticated => Token != null; + + public void EnsureAuthenticated() { + if (!IsAuthenticated) AuthenticateAsync().Wait(); + } +} diff --git a/FitConnect/Services/RestCallService.cs b/FitConnect/Services/RestCallService.cs new file mode 100644 index 0000000000000000000000000000000000000000..90a572003c24d2b07f6069df412a41d8dddecdcc --- /dev/null +++ b/FitConnect/Services/RestCallService.cs @@ -0,0 +1,98 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace FitConnect.Services; + +public interface IRestCallService { + public WebProxy? Proxy { get; set; } +} + +public abstract class RestCallService : IRestCallService { + private readonly string _baseUrl; + private readonly ILogger? _logger; + + + protected RestCallService(string baseUrl, ILogger? logger = null) { + _baseUrl = baseUrl; + _logger = logger; + } + + protected static string? AccessToken { get; set; } + public WebProxy? Proxy { get; set; } + + protected HttpClient CreateClient() { + var clientHandler = new HttpClientHandler { + Proxy = Proxy + }; + var client = new HttpClient(clientHandler); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + if (AccessToken != null) + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {AccessToken}"); + return client; + } + + protected async Task<string> RestCallForString(string endpoint, HttpMethod method, + string? body = null, string contentType = "application/json", + string accept = "application/json") { + var client = CreateClient(); + + var request = new HttpRequestMessage(); + + if (accept != "application/json") { + request.Headers.Accept.Clear(); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(accept)); + } + + + request.Method = method; + + request.RequestUri = new Uri($"{_baseUrl}{endpoint}"); + + if (body != null) { + request.Content = new StringContent(body, Encoding.UTF8, contentType); + _logger?.LogTrace("Body: {Body}", body); + } + + var response = await client.SendAsync(request); + _logger?.LogDebug("Server call: {Method} {Uri} - {StatusCode}", method, request.RequestUri, + response.StatusCode); + + if (response.IsSuccessStatusCode) { + var content = await response.Content.ReadAsStringAsync(); + _logger?.LogTrace("Response: {Content}", content); + return content; + } + + _logger?.LogError("Error calling {Method} - {Endpoint} with {Body} => {Status}", method, + endpoint, + body, response.StatusCode); + throw new HttpRequestException("Error calling FitConnect API", + new HttpRequestException( + $"{method} - {request.RequestUri.ToString()} - {response.StatusCode}\n" + + await response.Content.ReadAsStringAsync())); + } + + protected async Task<T?> RestCall<T>(string endpoint, HttpMethod method, string? body = null, + string contentType = "application/json", string accept = "application/json") + where T : class { + var content = await RestCallForString(endpoint, method, body, contentType, accept); + try { + var result = JsonSerializer.Deserialize<T>(content); + + if (result != null) + return result; + } + catch (JsonException e) { + _logger?.LogError("Error deserializing content from {Uri}\r\n{Content}", + $"{_baseUrl}{endpoint}", + content); + throw new HttpRequestException("Error deserializing FitConnect API response", + e); + } + + throw new ArgumentException("Can not deserialize response to type " + typeof(T).Name); + } +} diff --git a/FitConnect/Services/RouteService.cs b/FitConnect/Services/RouteService.cs new file mode 100644 index 0000000000000000000000000000000000000000..e1673d0cffa2208cdeec420a431a8cf4e2e146f3 --- /dev/null +++ b/FitConnect/Services/RouteService.cs @@ -0,0 +1,47 @@ +using FitConnect.Models; +using FitConnect.Services.Interfaces; +using FitConnect.Services.Models.v1.Routes; +using Microsoft.Extensions.Logging; +using Route = FitConnect.Services.Models.v1.Routes.Route; + +namespace FitConnect.Services; + +public class RouteService : RestCallService, IRouteService { + private readonly IOAuthService _oAuthService; + + public RouteService(string baseUrl, IOAuthService oAuthService, string version = "v1", + ILogger? logger = null) : base($"{baseUrl}/{version}", logger) { + _oAuthService = oAuthService; + } + + /// <summary> + /// Returns the destination id for the given intent. + /// </summary> + /// <param name="leikaKey"></param> + /// <param name="ags"></param> + /// <param name="ars"></param> + /// <param name="areaId"></param> + /// <returns></returns> + public async Task<List<Route>> GetDestinationIdAsync(string leikaKey, + string? ags, string? ars, + string? areaId) { + if (ars == null && ags == null && areaId == null) + throw new ArgumentException("Either ars, ags or areaId must be specified."); + + var result = await RestCall<RoutesListDto>($"/routes?leikaKey={leikaKey}" + + (ags != null ? $"&ags={ags}" : "") + + (ars != null ? $"&ars={ars}" : "") + + (areaId != null ? $"&areaId={areaId}" : ""), + HttpMethod.Get); + + return result?.Routes?.ToList() ?? new List<Route>(); + } + + public async Task<AreaList?> GetAreas(string filter, int offset = 0, int limit = 100) { + var result = await RestCall<AreaList>( + $"/areas?areaSearchexpression={filter}&offset={offset}&limit={limit}", + HttpMethod.Get); + + return result; + } +} diff --git a/FitConnect/Services/SubmissionService.cs b/FitConnect/Services/SubmissionService.cs new file mode 100644 index 0000000000000000000000000000000000000000..198106f82d119ab73cf2fd0b9554160bd03e7eee --- /dev/null +++ b/FitConnect/Services/SubmissionService.cs @@ -0,0 +1,147 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using FitConnect.Services.Interfaces; +using FitConnect.Services.Models.v1.Case; +using FitConnect.Services.Models.v1.Submission; +using Microsoft.Extensions.Logging; + +namespace FitConnect.Services; + +public class SubmissionService : RestCallService, ISubmissionService { + private readonly ILogger? _logger; + private readonly IOAuthService _oAuthService; + + public SubmissionService(string baseUrl, IOAuthService oAuthService, + string version = "v1", ILogger? logger = null) : base( + $"{baseUrl}/{version}", logger) { + _oAuthService = oAuthService; + _logger = logger; + } + + /// <summary> + /// <para>@PostMapping("/submissions")</para> + /// </summary> + /// <param name="submissionDto">RequestBody</param> + /// <returns></returns> + public SubmissionCreatedDto CreateSubmission(CreateSubmissionDto submissionDto) { + _oAuthService.EnsureAuthenticated(); + var result = RestCall<SubmissionCreatedDto>( + "/submissions", + HttpMethod.Post, + JsonSerializer.Serialize(submissionDto)).Result; + + if (result == null) + throw new ArgumentException("Submission creation failed"); + + return result; + } + + + /// <summary> + /// <para> + /// @PutMapping(value = "/submissions/{submissionId}/attachments/{attachmentId}", consumes = + /// "application/jose") + /// </para> + /// </summary> + /// <param name="submissionId">PathVariable</param> + /// <param name="attachmentId">PathVariable</param> + /// <param name="encryptedAttachmentContent">RequestBody</param> + /// <returns></returns> + public bool AddSubmissionAttachment(string submissionId, string attachmentId, + string encryptedAttachmentContent) { + _oAuthService.EnsureAuthenticated(); + var result = RestCallForString( + $"/submissions/{submissionId}/attachments/{attachmentId}", + HttpMethod.Put, + encryptedAttachmentContent, + "application/jose").Result; + return true; + } + + /// <summary> + /// <para>@PutMapping(value = "/submissions/{submissionId}", consumes = "application/json") </para> + /// </summary> + /// <param name="submissionId">PathVariable</param> + /// <param name="submitSubmission">RequestBody</param> + /// <returns></returns> + public async Task<SubmissionReducedDto?> SubmitSubmission(string submissionId, + SubmitSubmissionDto submitSubmission) { + _oAuthService.EnsureAuthenticated(); + var body = JsonSerializer.Serialize(submitSubmission, new JsonSerializerOptions { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true, + Encoder = JavaScriptEncoder.Default + }); + + var result = await RestCall<SubmissionReducedDto>($"/submissions/{submissionId}", + HttpMethod.Put, + body); + + return result; + } + + + /// <summary> + /// <para>@GetMapping("/submissions")</para> + /// </summary> + /// <param name="destinationId">RequestParam</param> + /// <param name="offset">RequestParam</param> + /// <param name="limit">RequestParam</param> + /// <returns></returns> + public async Task<SubmissionsForPickupDto> ListSubmissions(string? destinationId, + int offset = 0, + int limit = 100) { + _oAuthService.EnsureAuthenticated(); + var url = destinationId != null + ? $"/submissions?destinationId={destinationId}&offset={offset}&limit={limit}" + : $"/submissions?offset={offset}&limit={limit}"; + return RestCall<SubmissionsForPickupDto>(url, HttpMethod.Get).Result ?? + new SubmissionsForPickupDto(); + } + + /// <summary> + /// <para>@GetMapping("/submissions/{submissionId}")</para> + /// </summary> + /// <param name="submissionId">PathVariable</param> + /// <returns></returns> + public SubmissionDto GetSubmission(string submissionId) { + _oAuthService.EnsureAuthenticated(); + var result = + RestCall<SubmissionDto>($"/submissions/{submissionId}", HttpMethod.Get).Result ?? + new SubmissionDto(); + + return result; + } + + + /// <summary> + /// <para> + /// @GetMapping(value = "/submissions/{submissionId}/attachments/{attachmentId}", produces = + /// "application/jose") + /// </para> + /// </summary> + /// <param name="submissionId">PathVariable</param> + /// <param name="attachmentId">PathVariable</param> + /// <returns></returns> + public string GetAttachment(string submissionId, string attachmentId) { + _oAuthService.EnsureAuthenticated(); + var result = RestCallForString($"/submissions/{submissionId}/attachments/{attachmentId}", + HttpMethod.Get, + null, + "application/jose", + "application/jose").Result; + + return result; + } + + public string GetKey(string keyId) { + throw new NotImplementedException(); + } + + public async Task<List<string>> GetStatusForSubmissionAsync(string caseId) { + _oAuthService.EnsureAuthenticated(); + var events = await RestCall<EventLogDto>($"/cases/{caseId}/events", HttpMethod.Get); + return events.EventLog.ToList(); + } +} diff --git a/FitConnect/Subscriber.cs b/FitConnect/Subscriber.cs new file mode 100644 index 0000000000000000000000000000000000000000..fcdd076df1df9e11f0efb0ec942da75d99b0a073 --- /dev/null +++ b/FitConnect/Subscriber.cs @@ -0,0 +1,208 @@ +using System.Reflection; +using Autofac; +using FitConnect.Encryption; +using FitConnect.Interfaces.Subscriber; +using FitConnect.Models; +using FitConnect.Models.v1.Api; +using FitConnect.Services.Models.v1.Submission; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using NJsonSchema; +using NJsonSchema.Validation; +using Metadata = FitConnect.Models.Api.Metadata.Metadata; + +namespace FitConnect; + +/// <summary> +/// Fluent API for the FitConnect.Subscriber +/// </summary> +public class Subscriber : FitConnectClient, + ISubscriber, + ISubscriberWithSubmission { + public Subscriber(FitConnectEnvironment environment, + string clientId, string clientSecret, + string privateKeyDecryption, + string privateKeySigning, + string publicKeyEncryption, + string publicKeySignatureVerification, + IContainer container) : base(environment, clientId, clientSecret, + container, + privateKeyDecryption, privateKeySigning, publicKeyEncryption, + publicKeySignatureVerification) { + Encryption = new FitEncryption(privateKeyDecryption, privateKeySigning, publicKeyEncryption, + publicKeySignatureVerification, + container.Resolve<ILogger>()); + } + + public Subscriber(FitConnectEnvironment environment, + string clientId, string clientSecret, + string privateKeyDecryption, + string privateKeySigning, + string publicKeyEncryption, + string publicKeySignatureVerification, + ILogger? logger = null) : base(environment, clientId, clientSecret, logger, + privateKeyDecryption, privateKeySigning) { + Encryption = new FitEncryption(privateKeyDecryption, privateKeySigning, publicKeyEncryption, + publicKeySignatureVerification, logger); + } + + + /// <summary> + /// Receives the available submissions from the server + /// </summary> + /// <param name="destinationId"></param> + /// <param name="skip"></param> + /// <param name="take"></param> + /// <returns></returns> + public IEnumerable<SubmissionForPickupDto> GetAvailableSubmissions(string? destinationId = null, + int skip = 0, + int take = 100) { + var submissionsResult = SubmissionService.ListSubmissions(destinationId, 0, 100).Result; + + // Creating a dictionary of destinationId to submissionIds from the REST API result + return submissionsResult.Submissions; + } + + + /// <summary> + /// Receives a specific submission from the server. + /// </summary> + /// <param name="submissionId"></param> + /// <param name="skipSchemaTest"></param> + /// <returns></returns> + public ISubscriberWithSubmission RequestSubmission(string? submissionId, + bool skipSchemaTest = false) { + if (submissionId == null) { + throw new ArgumentNullException($"{nameof(submissionId)} has to be set"); + } + + var submission = (Submission)SubmissionService.GetSubmission(submissionId); + var (metaDataString, _, metaHash) = Encryption.Decrypt(submission.EncryptedMetadata!); + + if (!skipSchemaTest) { + var errors = VerifyMetadata(metaDataString); + if (errors.Count > 0) { + Logger?.LogWarning("Invalid metadata: {MetaData}", metaDataString); + foreach (var error in errors) Logger?.LogError("Error: {Error}", error.ToString()); + + throw new Exception($"Metadata validation failed: {string.Join(", ", errors)}"); + } + } + + submission.Metadata = JsonConvert.DeserializeObject<Metadata>(metaDataString); + + + if (submission.EncryptedData != null) { + var (dataString, _, dataHash) = Encryption.Decrypt(submission.EncryptedData); + submission.Data = dataString; + + if (submission?.Metadata?.ContentStructure.Data.Hash.Content != + FitEncryption.CalculateHash(dataString)) { + Logger?.LogWarning("Data hash mismatch: {DataHash} != {CalculatedHash}", + submission?.Metadata?.ContentStructure.Data.Hash.Content, + FitEncryption.CalculateHash(dataString)); + throw new Exception("Data hash mismatch"); + } + } + + Submission = submission; + return this; + } + + public Submission? Submission { get; private set; } + + /// <summary> + /// Reading attachments for a submission. + /// </summary> + /// <returns></returns> + public IEnumerable<Attachment> GetAttachments() { + // TODO add guard calls + + var attachments = new List<Attachment>(); + foreach (var id in Submission!.AttachmentIds) { + var encryptedAttachment = SubmissionService.GetAttachment(Submission.Id, id); + var (_, content, hash) = Encryption.Decrypt(encryptedAttachment); + var attachmentMeta = + Submission.Metadata.ContentStructure.Attachments.First(a => a.AttachmentId == id); + + attachments.Add(new Attachment(attachmentMeta, content)); + } + + Submission.Attachments = attachments; + return attachments; + } + + public void AcceptSubmission() { + CompleteSubmission(Submission!, FinishSubmissionStatus.Accepted); + } + + public void RejectSubmission(params Problems[] problems) { + CompleteSubmission(Submission!, FinishSubmissionStatus.Rejected, problems); + } + + public void ForwardSubmission() { + CompleteSubmission(Submission!, FinishSubmissionStatus.Forwarded); + } + + public void CompleteSubmission(FinishSubmissionStatus status) { + CompleteSubmission(Submission!, status); + } + + + /// <summary> + /// Verify the metadata hash and content to fit the schema + /// </summary> + /// <param name="metadataString"></param> + /// <returns></returns> + public static ICollection<ValidationError> VerifyMetadata(string metadataString) { + var schemaString = LoadContentOfResource("metadata.schema.json"); + var schema = JsonSchema.FromJsonAsync(schemaString).Result; + return schema.Validate(metadataString); + } + + private static string LoadContentOfResource(string resourceName) { + var assembly = Assembly.GetExecutingAssembly(); + var fullQualifiedName = $"{assembly.GetName().Name}.{resourceName}"; + var resourceStream = assembly.GetManifestResourceStream(fullQualifiedName); + var reader = new StreamReader(resourceStream); + return reader.ReadToEnd(); + } + + + private string GetEvent(FinishSubmissionStatus state) { + return state switch { + FinishSubmissionStatus.Accepted => + "https://schema.fitko.de/fit-connect/events/accept-submission", + FinishSubmissionStatus.Rejected => + "https://schema.fitko.de/fit-connect/events/reject-submission", + FinishSubmissionStatus.Forwarded => + "https://schema.fitko.de/fit-connect/events/forward-submission", + _ => throw new ArgumentException("Invalid state") + }; + } + + public void CompleteSubmission(SubmissionForPickupDto submission, + FinishSubmissionStatus status, Problems[]? problems = null) { + if (submission.SubmissionId == null || submission.CaseId == null || + submission.DestinationId == null) + throw new ArgumentException("Submission does not contain all required fields"); + + if (status != FinishSubmissionStatus.Rejected && problems != null) + throw new ArgumentException("Problems can only be set for rejected submissions"); + + var eventName = GetEvent(status); + + var token = + Encryption.CreateSecurityEventToken(submission.SubmissionId, submission.CaseId, + submission.DestinationId, eventName, problems); + + var result = CasesService.FinishSubmission(submission.CaseId, token); + Logger?.LogInformation("Submission completed {status}", result); + } +} + +public enum FinishSubmissionStatus { + Accepted, + Rejected, + Forwarded +} diff --git a/FitConnect/metadata.schema.json b/FitConnect/metadata.schema.json index 0f730cd32e5411cf4d8d898e589307102f9c40b5..1d07dc235c85b1735ce09a6323dbe9a464906fed 100644 --- a/FitConnect/metadata.schema.json +++ b/FitConnect/metadata.schema.json @@ -1,5 +1,5 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", + "$schema": "https://json-schema.org/draft-07/schema", "$id": "https://schema.fitko.de/fit-connect/metadata/1.0.0/metadata.schema.json", "type": "object", "title": "Metadaten", diff --git a/IntegrationTests/HelperMethods.cs b/IntegrationTests/HelperMethods.cs new file mode 100644 index 0000000000000000000000000000000000000000..e076c7014e78d6d0d189afeb49a04a98b34ce707 --- /dev/null +++ b/IntegrationTests/HelperMethods.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using Newtonsoft.Json; + +namespace IntegrationTests; + +public static class HelperMethods { + public static (string id, string secret) GetSecrets() { + // relative to the project execution directory + const string secretFile = "../../../http-client.env.json"; + + if (!File.Exists(secretFile)) { + // If the secret file is not found, create it with the default values + // The file will be pretty in C#11, when """ is introduced + File.WriteAllText(secretFile, @" +{ + ""sender"": { + ""id"": ""00000000-0000-0000-0000-000000000000"", + ""secret"": ""0000000000000000000000000000000000000000000"", + ""scope"": ""send:region:DE"" + } +}"); + throw new Exception("Please fill the secret.json file with your sender credentials"); + } + + var jsonContent = File.ReadAllText(secretFile); + var secret = JsonConvert.DeserializeObject<dynamic>(jsonContent); + return (secret.sender.id, secret.sender.secret); + } +} diff --git a/IntegrationTests/IntegrationTests.csproj b/IntegrationTests/IntegrationTests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..d95a0c10813bbec26c5f71ac327cc1d9ab2f2030 --- /dev/null +++ b/IntegrationTests/IntegrationTests.csproj @@ -0,0 +1,32 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <Nullable>enable</Nullable> + + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="DotNet.Testcontainers" Version="1.6.0" /> + <PackageReference Include="FluentAssertions" Version="6.7.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" /> + <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.21.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> + <PackageReference Include="NUnit" Version="3.13.2" /> + <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> + <PackageReference Include="coverlet.collector" Version="3.1.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> + <ProjectReference Include="..\MockContainer\MockContainer.csproj" /> + </ItemGroup> + + <ItemGroup> + <None Update="Test.pdf"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + </ItemGroup> + +</Project> diff --git a/IntegrationTests/OAuthServiceTest.cs b/IntegrationTests/OAuthServiceTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..6f1f29fd860b4e9b0231ddba5f56eced4f2c9144 --- /dev/null +++ b/IntegrationTests/OAuthServiceTest.cs @@ -0,0 +1,53 @@ +using System.Security.Authentication; +using FitConnect.Models; +using FitConnect.Services; +using FluentAssertions; +using NUnit.Framework; + +namespace IntegrationTests; + +public class OAuthServiceTest { + private string _clientId; + private string _clientSecret; + private OAuthService _oAuthService = null!; + + [OneTimeSetUp] + public void OneTimeSetup() { + (_clientId, _clientSecret) = HelperMethods.GetSecrets(); + } + + + [SetUp] + public void SetUp() { + var endpoints = FitConnectEnvironment.Testing; + _oAuthService = new OAuthService(endpoints.TokenUrl, "v1", _clientId, _clientSecret, null); + } + + [Test] + public void GetAccessToken_ExpiresInShouldBe1800_WithoutScope() { + _oAuthService.AuthenticateAsync().Wait(); + var token = _oAuthService.Token; + token.Should().NotBeNull(); + token!.ExpiresIn.Should().Be(1800); + token.Scope.Should().Be("send:region:DE"); + } + + [Test] + public void GetAccessToken_WrongCredentials_ThrowInvalidCredentialException() { + var exception = Assert.ThrowsAsync<InvalidCredentialException>(() => + new OAuthService(FitConnectEnvironment.Testing.TokenUrl, "v1", "wrong", "wrong", + null) + .AuthenticateAsync() + ); + exception!.Message.Replace(" ", "").Should().Contain("\"error\":\"invalid_client\""); + } + + [Test] + public void GetAccessToken_ScopeShouldMatch_WithScope() { + _oAuthService.AuthenticateAsync("send:region:DE01010").Wait(); + var token = _oAuthService.Token; + token.Should().NotBeNull(); + token!.ExpiresIn.Should().Be(1800); + token.Scope.Should().Be("send:region:DE01010"); + } +} diff --git a/IntegrationTests/ProxyTest.cs b/IntegrationTests/ProxyTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..7503e959b26a48d09255021fff5bfca11fa2b385 --- /dev/null +++ b/IntegrationTests/ProxyTest.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.Net; +using System.Reflection; +using System.Threading; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using FitConnect; +using FitConnect.Models; +using FitConnect.Services; +using FluentAssertions; +using NUnit.Framework; + +namespace IntegrationTests; + +[Ignore("Not testable in docker container, take to long to run every time")] +[TestFixture] +public class ProxyTest { + [OneTimeSetUp] + public void OneTimeSetup() { + var path = $"{Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)}/proxy"; + Console.WriteLine($"Creating directory: {path}"); + Directory.CreateDirectory(path); + (_id, _secret) = HelperMethods.GetSecrets(); + File.WriteAllText("proxy/access.log", ""); + + _container = new TestcontainersBuilder<TestcontainersContainer>() + .WithImage("ubuntu/squid") + .WithPortBinding("3128", "3128") + .WithBindMount(path, @"/var/log/squid") + .Build(); + _container.StartAsync().Wait(); + Thread.Sleep(5000); + } + + [SetUp] + public void Setup() { + sender = + new FitConnect.Sender(FitConnectEnvironment.Testing, + _id, + _secret) + .WithProxy("localhost", 3128); + + _webClient = new WebClient { + Proxy = + new WebProxy("http://localhost:3128") + }; + _webClient.DownloadString("https://www.fitko.de/"); + } + + + [TearDown] + public void TearDown() { + _webClient.DownloadString("https://www.google.de/").Should().NotBeNull(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() { + Thread.Sleep(10000); + _container.StopAsync().Wait(); + var content = File.ReadAllText("proxy/access.log"); + Console.WriteLine(content); + content.Should().Contain("auth-testing.fit-connect.fitko.dev:443"); + } + + private WebClient _webClient = null!; + + private TestcontainersContainer _container = null!; + private string _id = null!; + private string _secret = null!; + private FitConnectClient sender = null!; + + [Test] + [Order(1)] + public void ContainerIsRunning() { + sender.GetProxy()!.Address.Should().Be("http://localhost:3128"); + _container.Should().NotBeNull(); + _container.State.Should().Be(TestcontainersState.Running); + } + + [Test] + [Order(10)] + public void SendSubmissionViaProxy_None_Work() { + var sender = new FitConnect.Sender( + FitConnectEnvironment.Testing, "", "") + .WithProxy("localhost", 3128); + + (sender as FitConnect.Sender).OAuthService.Proxy.Should().NotBeNull(); + } + + [Test] + [Order(20)] + public void RequestOAuthToken() { + // Arrange + var testUrl = FitConnectEnvironment.Testing + .TokenUrl; + + var oAuthService = new OAuthService(testUrl, "v1", _id, _secret, null) { + Proxy = new WebProxy("http://localhost:3128") + }; + + // Act + oAuthService.AuthenticateAsync().Wait(); + var token = oAuthService.Token; + + // Assert + token.Should().NotBeNull(); + } +} diff --git a/IntegrationTests/Sender/SenderTestBase.cs b/IntegrationTests/Sender/SenderTestBase.cs new file mode 100644 index 0000000000000000000000000000000000000000..6ba21abe72cbf05d2f5a2dc0afffb5632560e587 --- /dev/null +++ b/IntegrationTests/Sender/SenderTestBase.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.Linq; +using FitConnect.Interfaces.Sender; +using FitConnect.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace IntegrationTests.Sender; + +[Order(100)] +[TestFixture] +public abstract class SenderTestBase { + protected string LeikaKey = ""; + + /// <summary> + /// Setup creates json file for credentials if not found + /// </summary> + /// <exception cref="Exception"></exception> + [OneTimeSetUp] + public void OneTimeSetup() { + // relative to the project execution directory + const string secretFile = "../../../http-client.env.json"; + + if (!File.Exists(secretFile)) { + // If the secret file is not found, create it with the default values + // The file will be pretty in C#11, when """ is introduced + File.WriteAllText(secretFile, @" +{ + ""sender"": { + ""id"": ""00000000-0000-0000-0000-000000000000"", + ""secret"": ""0000000000000000000000000000000000000000000"", + ""scope"": ""send:region:DE"" + } +}"); + throw new Exception("Please fill the secret.json file with your sender credentials"); + } + + var jsonContent = File.ReadAllText(secretFile); + var secret = JsonConvert.DeserializeObject<dynamic>(jsonContent); + _clientId = secret.sender.id; + _clientSecret = secret.sender.secret; + } + + [SetUp] + public void Setup() { + var logger = LoggerFactory.Create(b => b.AddConsole()).CreateLogger<FitConnect.Sender>(); + Sender = new FitConnect.Sender( + FitConnectEnvironment.Testing, + _clientId, _clientSecret, logger); + } + + protected const string desitnationId = "aa3704d6-8bd7-4d40-a8af-501851f93934"; + protected string _clientId = "73a8ff88-076b-4263-9a80-8ebadac97b0d"; + protected string _clientSecret = "rdlXms-4ikO47AbTmmCTdzFoE4cTSt13JmSbcY5Dhsw"; + protected ISender Sender; + + protected Submission GetSubmissionInfo(ISender sender) { + var submission = sender.GetType().GetProperties() + .FirstOrDefault(p => p.Name == "Submission") + .GetValue(Sender) as Submission; + + return submission; + } +} diff --git a/IntegrationTests/Sender/SenderTestHappyPath.cs b/IntegrationTests/Sender/SenderTestHappyPath.cs new file mode 100644 index 0000000000000000000000000000000000000000..44612373f725f6a6a3ba45a6614032b24c1f8ec1 --- /dev/null +++ b/IntegrationTests/Sender/SenderTestHappyPath.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.IO; +using FitConnect.Models; +using FluentAssertions; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace IntegrationTests.Sender; + +[TestFixture] +public class SenderTestHappyPath : SenderTestBase { + [Test] + public void CheckIfSecretsAreValid() { + _clientId.Should().NotBe("00000000-0000-0000-0000-000000000000"); + _clientSecret.Should().NotBe("0000000000000000000000000000000000000000000"); + } + + [Test] + public void WithDestination_PrepareSubmissionToServer() { + // Arrange + + // Act + Sender.WithDestination(desitnationId); + + // Assert + Sender.PublicKey.Should().NotBeNullOrEmpty(); + } + + [Test] + public void WithAttachments_IntroduceSubmission_ShouldGetIdFromServer() { + // Arrange + var dut = Sender + .WithDestination(desitnationId) + .WithServiceType("ServiceName", "urn:de:fim:leika:leistung:99400048079000"); + + var attachments = new List<Attachment>(); + var attachment = new Attachment("Test.pdf", "Just an attachment"); + + attachments.Add(attachment); + + // {'attachmentId': 'c96cce5b-f35d-4c38-ac19-9bc47721c0f9', 'hash': {'type': 'sha512', 'content': '8b1042900c2039f65fe6c4cb1bca31e2a7a04b61d3ca7d9ae9fc4077068b82ad5512fa298385b025db70551113b762064444b87737e45e657a71be5b88b06e59'}, 'mimeType': 'application/pdf', 'purpose': 'attachment'} + + + // Act + dut.WithAttachments(attachments); + + + // Assert + attachment.Hash.Should() + .Be( + "8b1042900c2039f65fe6c4cb1bca31e2a7a04b61d3ca7d9ae9fc4077068b82ad5512fa298385b025db70551113b762064444b87737e45e657a71be5b88b06e59"); + var submission = GetSubmissionInfo(Sender); + submission.Attachments.Should().HaveCount(attachments.Count); + + submission.Id.Should().NotBeNullOrWhiteSpace(); + Console.WriteLine(submission.CaseId); + } + + + [TestCase("0b8e6fbd-62e2-4b6f-b333-5308d82e0a00")] + [TestCase("d2be2027-9368-4c0c-a265-2fdbf7ecd4d9")] + public void GetSubmissionStatus(string caseId) { + var status = Sender.GetStatusForSubmission(caseId); + status.ForEach(s => Console.WriteLine($"{s.EventTime} - {s.EventType}")); + status.Count.Should().BeGreaterThan(0); + } + + [Test] + public void Submit_FinishSubmission_ShouldGetIdFromServer() { + // Arrange + + var attachments = new List<Attachment>(); + + var attachment = new Attachment("Test.pdf", "Just an attachment"); + attachments.Add(attachment); + var dut = Sender + .WithDestination(desitnationId) + .WithServiceType("ServiceName", "urn:de:fim:leika:leistung:99400048079000") + .WithAttachments(attachments); + + // Act + dut.Submit(); + + // Assert + var submission = GetSubmissionInfo(Sender); + Console.WriteLine($"Case ID: {submission.CaseId}"); + submission.Should().NotBeNull(); + } + + [Test] + public void Submit_FinishSubmissionWithData_ShouldGetIdFromServer() { + // Arrange + + var attachments = new List<Attachment>(); + + var content = File.ReadAllBytes("Test.pdf"); + var attachment = new Attachment("Test.pdf", "Just an attachment") { + Filename = "RandomBytes", + MimeType = "application/pdf", + Description = "Just a test", + Purpose = "attachment" + }; + attachments.Add(attachment); + var dut = Sender + .WithDestination(desitnationId) + .WithServiceType("ServiceName", "urn:de:fim:leika:leistung:99400048079000") + .WithAttachments(attachments) + .WithData(JsonConvert.SerializeObject(new { + FirstName = "John", + LastName = "Doe", + Age = 42, + Birthday = DateTime.Today.AddYears(-42) + })); + + // Act + dut.Submit(); + + // Assert + var submission = GetSubmissionInfo(Sender); + var statusForSubmission = Sender.GetStatusForSubmission(submission.CaseId); + foreach (var securityEventToken in statusForSubmission) + Console.WriteLine(securityEventToken.Token.Subject); + + statusForSubmission.Should().HaveCountGreaterThan(0); + Console.WriteLine($"Case ID: {submission.CaseId}"); + Console.WriteLine($"Submission ID: {submission.Id}"); + submission.Should().NotBeNull(); + } + + [Ignore("Destination can not be found")] + [Test] + public void Submit_FinishSubmissionWithDataFindDestination_ShouldGetIdFromServer() { + // Arrange + + var attachments = new List<Attachment>(); + + var content = File.ReadAllBytes("Test.pdf"); + var attachment = new Attachment("Test.pdf", "Just an attachment") { + Filename = "RandomBytes", + MimeType = "application/pdf", + Description = "Just a test", + Purpose = "attachment" + }; + attachments.Add(attachment); + var dut = Sender + .FindDestinationId("99400048079000", ars: "09372126") + .WithServiceType("ServiceName", "urn:de:fim:leika:leistung:99400048079000") + .WithAttachments(attachments) + .WithData(JsonConvert.SerializeObject(new { + FirstName = "John", + LastName = "Doe", + Age = 42, + Birthday = DateTime.Today.AddYears(-42) + })); + + // Act + dut.Submit(); + + // Assert + var submission = GetSubmissionInfo(Sender); + var statusForSubmission = Sender.GetStatusForSubmission(submission.CaseId); + foreach (var securityEventToken in statusForSubmission) + Console.WriteLine(securityEventToken.Token.Subject); + + statusForSubmission.Should().HaveCountGreaterThan(0); + Console.WriteLine($"Case ID: {submission.CaseId}"); + Console.WriteLine($"Submission ID: {submission.Id}"); + submission.Should().NotBeNull(); + } + + [Test] + public void GetAreas_ShouldGetAreasFromServer() { + // Arrange + var areas = Sender.GetAreas("Furth*", out var _); + areas.Should().HaveCountGreaterThan(0); + } + + [Test] + public void GetDestinations_ShouldGetDestinationsFromServer() { + // Arrange + var destinations = Sender.FindDestinationId("99123456760610", + ars: "064350014014"); + Sender.PublicKey.Should().NotBeNull(); //destinations.Should().HaveCountGreaterThan(0)); + + // foreach (var destination in destinations) { + // destination.DestinationParameters.PublicKeys.Keys.ForEach(k => + // Console.WriteLine(k.ToString())); + // destination.DestinationParameters.Status.Should().Be("active"); + // } + } +} diff --git a/IntegrationTests/Sender/SenderTestUnhappyPath.cs b/IntegrationTests/Sender/SenderTestUnhappyPath.cs new file mode 100644 index 0000000000000000000000000000000000000000..fd6fdc47b1a3faab2f43bc1252169b10b32b800f --- /dev/null +++ b/IntegrationTests/Sender/SenderTestUnhappyPath.cs @@ -0,0 +1,34 @@ +using System; +using System.Net.Http; +using FitConnect.Models; +using FluentAssertions; +using NUnit.Framework; + +namespace IntegrationTests.Sender; + +[TestFixture] +public class SenderTestUnhappyPath : SenderTestBase { + [Test] + public void WithDestination_UnknownUUID_ShouldThrowAggregateWithInnerHttpRequestException() { + Assert.Throws<AggregateException>(() => { Sender.WithDestination(Guid.Empty.ToString()); }) + .InnerExceptions.Should().ContainItemsAssignableTo<HttpRequestException>(); + } + + + [Test] + public void WithDestination_CompletelyWrong_ShouldThrowArgumentException() { + Assert.Throws<ArgumentException>(() => { Sender.WithDestination("This is very wrong"); }) + .Message.Should().Be("The destination must be a valid GUID"); + } + + [Test] + public void WithData_DataIsInvalidJson_ShouldThrowArgumentException() { + Assert.Throws<ArgumentException>(() => { + Sender.WithDestination(desitnationId) + .WithServiceType("Name", leikaKey: "urn:de:fim:leika:leistung:00000000000000") + .WithAttachments(new Attachment("Test.pdf", "Test PDF")) + .WithData("This is very wrong"); + }) + .Message.Should().Be("The data must be valid JSON string"); + } +} diff --git a/IntegrationTests/Sender/ThreadTest.cs b/IntegrationTests/Sender/ThreadTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..7e932904170ed4297c50436ee9a3188d6e83cc1c --- /dev/null +++ b/IntegrationTests/Sender/ThreadTest.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Autofac; +using FitConnect.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using MockContainer; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace IntegrationTests.Sender; + +/// <summary> +/// This test runs multi sender and one subscriber which "cleans" the server +/// so other subscriber won't get any submissions. +/// </summary> +[Ignore("Can not be combined with other tests - need to be run separately")] +[TestFixture] +public class ThreadTest { + private IContainer _container; + private MockSettings _setting; + private ILogger _logger; + private const int NumberOfThreads = 34; + + [SetUp] + public void Setup() { + _container = MockContainer.Container.Create(); + _setting = _container.Resolve<MockSettings>(); + _logger = LoggerFactory.Create(b => b.AddConsole()) + .CreateLogger<FitConnect.Sender>(); + } + + [Test] + [Order(1)] + public void ThreadedSender() { + var tasks = new List<Task>(); + + for (int i = 0; i < NumberOfThreads; i++) { + tasks.Add(Task.Run(() => { + var counter = Thread.CurrentThread.ManagedThreadId; + var Sender = new FitConnect.Sender( + FitConnectEnvironment.Testing, + _setting.SenderClientId, _setting.SenderClientSecret, _logger); + + var delayed = Sender.WithDestination(_setting.DestinationId) + .WithServiceType($"ThreadTest_{counter}", _setting.LeikaKey) + .WithAttachments(new Attachment("Test.pdf", $"Attachment_{counter}")); + + Thread.Sleep(RandomNumberGenerator.GetInt32(100, 500)); + delayed + .WithData(JsonConvert.SerializeObject(new { + Name = $"ThreadTest_{counter}", + ThreadId = counter + })) + .Submit(); + })); + } + + while (!tasks.All(t => t.IsCompleted)) { + } + } + + [Test] + [Order(2)] + public void GetSubmissions() { + var subscriber = FitConnect.Client.GetSubscriber(FitConnectEnvironment.Testing, + _setting.SubscriberClientId, _setting.SubscriberClientSecret, + _setting.PrivateKeyDecryption, _setting.PrivateKeySigning, + _setting.PublicKeyEncryption, _setting.PublicKeySignatureVerification, _logger); + + var submissions = subscriber.GetAvailableSubmissions().ToList(); + + foreach (var submission in submissions) { + subscriber + .RequestSubmission(submission.SubmissionId) + .AcceptSubmission(); + } + + submissions.Count.Should().Be(NumberOfThreads); + } +} diff --git a/IntegrationTests/Subscriber/SubscriberTestBase.cs b/IntegrationTests/Subscriber/SubscriberTestBase.cs new file mode 100644 index 0000000000000000000000000000000000000000..9b5dfa8863edc9d31f4073cebb872ba43ffd9d54 --- /dev/null +++ b/IntegrationTests/Subscriber/SubscriberTestBase.cs @@ -0,0 +1,55 @@ +using Autofac; +using FitConnect.Interfaces.Subscriber; +using FitConnect.Models; +using Microsoft.Extensions.Logging; +using MockContainer; +using NUnit.Framework; + +namespace IntegrationTests.Subscriber; + +public abstract class SubscriberTestBase { + protected const string desitnationId = "aa3704d6-8bd7-4d40-a8af-501851f93934"; + protected string _clientId = "20175c2b-c4dd-4a01-99b1-3a08436881a1"; + protected string _clientSecret = "KV2qd7qc5n-xESB6dvfrTlMDx2BWHJd5hXJ6pKKnbEQ"; + private IContainer _container; + protected ISubscriber subscriber { get; set; } + protected ILogger Logger; + + [OneTimeSetUp] + public void OneTimeSetUp() { + // One-time initialization code + _container = Container.Create(); + } + + [SetUp] + public void SetUp() { + // Code here will be called before every test + + + Logger = LoggerFactory.Create(b => { + b.AddConsole(); + b.SetMinimumLevel(LogLevel.Trace); + }) + .CreateLogger<FitConnect.Sender>(); + subscriber = new FitConnect.Subscriber( + FitConnectEnvironment.Testing, + _clientId, + _clientSecret, + _container.Resolve<MockSettings>().PrivateKeyDecryption, + _container.Resolve<MockSettings>().PrivateKeySigning, + _container.Resolve<MockSettings>().PublicKeyEncryption, + _container.Resolve<MockSettings>().PublicKeySignatureVerification, + Logger); + } + + + [TearDown] + public void TearsDown() { + // Code here will be called after every test + } + + [OneTimeTearDown] + public void OneTimeTearDown() { + // One-time cleanup code + } +} diff --git a/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs b/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs new file mode 100644 index 0000000000000000000000000000000000000000..bfe0ed58a6adb48c12f44abc14dadec2fa55c889 --- /dev/null +++ b/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs @@ -0,0 +1,140 @@ +using System; +using System.IO; +using System.Linq; +using FitConnect.Models.v1.Api; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace IntegrationTests.Subscriber; + +[Order(200)] +[TestFixture] +public class SubscriberTestHappyPath : SubscriberTestBase { + [Order(201)] + [Test] + public void GetAvailableSubmissions_WithDestinationId_ShouldReturnSubmissionsForPickupDto() { + // Act + var submissions = subscriber.GetAvailableSubmissions(desitnationId).ToList(); + + // Assert + submissions.Count().Should().BeGreaterThanOrEqualTo(2); + submissions.All(s => s.DestinationId == desitnationId).Should().BeTrue(); + } + + [Order(202)] + [Test] + public void GetAvailableSubmissions_WithOutDestinationId_ShouldReturnSubmissionsForPickupDto() { + // Act + var submissions = subscriber.GetAvailableSubmissions().ToList(); + + // Assert + submissions.Count().Should().BeGreaterThan(0); + } + + + [Order(1001)] + [Test] + public void GetAllSubmission_WithSubmissionId_ShouldReturnSubmissionsForPickupDto() { + // Arrange + var errorCounter = 0; + var submissions = subscriber.GetAvailableSubmissions().ToList(); + submissions.Count().Should().BeGreaterThan(0); + var i = 0; + foreach (var submissionId in submissions.Select(s => s.SubmissionId)) { + // Act + Console.WriteLine($"Getting submission {submissionId}"); + var dto = subscriber.RequestSubmission(submissionId); + + // Assert + errorCounter.Should().BeLessThan(submissions.Count()); + Console.WriteLine($"Error counter: {errorCounter}/{submissions.Count()}"); + if (i++ % 2 == 0) + dto.AcceptSubmission(); + else + dto.RejectSubmission(new Problems() { Description = "A really critical problem" }); + } + } + + [Ignore("ID is already been used")] + [TestCase("303c60d3-3f6f-4913-bb51-4e66fe133c7f")] + public void GetAttachment_WithSubmissionId_ShouldReturnSubmissionsForPickupDto( + string submissionId) { + // Act + var attachments = subscriber.RequestSubmission(submissionId).GetAttachments(); + foreach (var attachment in attachments) { + attachment.Content.Length.Should().BeGreaterThan(0); + File.WriteAllBytes("attachments/test.pdf", attachment.Content); + } + } + + [Test] + [Order(203)] + public void GetStatus_ForAllPendingSubmissions() { + var submissions = subscriber.GetAvailableSubmissions().ToList(); + submissions.Count().Should().BeGreaterThan(0); + + foreach (var submission in submissions) { + subscriber.GetStatusForSubmission(submission.CaseId).ForEach(s => + Logger.LogInformation("{SubmissionCaseId} - {ObjEventTime} - {ObjEventType}", + submission.CaseId, s.EventTime, s.EventType)); + } + } + + //[Ignore("Takes a while")] + [Test] + [Order(204)] + public void GetAttachment_FromAllPendingSubmission_ShouldReturnAttachment() { + // Arrange + var submissions = subscriber.GetAvailableSubmissions().ToList(); + submissions.Count().Should().BeGreaterThan(0); + + foreach (var submission in submissions) { + Console.WriteLine( + $"Getting submission {submission.SubmissionId} - case {submission.CaseId}"); + var submissionId = submission.SubmissionId!; + if (!Directory.Exists($"./attachments/{submissionId}/")) + Directory.CreateDirectory($"./attachments/{submissionId}/"); + + subscriber.GetStatusForSubmission(submission.CaseId).ForEach(s => + Console.WriteLine($"{s.EventTime} - {s.EventType}")); + + var subscriberWithSubmission = subscriber + .RequestSubmission(submissionId, true); + var attachments = subscriberWithSubmission + .GetAttachments(); + foreach (var attachment in attachments) + if ((attachment?.Content?.Length ?? 0) > 0) + File.WriteAllBytes( + Path.Combine($"./attachments/{submissionId}/", + attachment.Filename), + attachment.Content); + + Console.WriteLine($"Json Fachdaten: \r\n{subscriberWithSubmission.GetDataJson()}"); + Console.WriteLine($"Success {submissionId}"); + } + + + // Process.Start("open", "./attachments"); + } + + [Ignore("ID is already been used")] + [TestCase("b0bd846f-f56a-4d06-a586-14fb0ea94fd0")] + public void GetSingleSubmission_WithSubmissionId_ShouldReturnSubmissionsForPickupDto( + string submissionId) { + // Arrange + var submissions = subscriber.GetAvailableSubmissions().ToList(); + submissions.Count().Should().BeGreaterThan(0); + + Console.WriteLine($"Getting submission {submissionId}"); + var dto = subscriber.RequestSubmission(submissionId); + + // Assert + dto.Submission.Id.Should().Be(submissionId); + + + Console.WriteLine(dto.Submission.Data); + Console.WriteLine(JsonConvert.SerializeObject(dto.Submission.Metadata)); + } +} diff --git a/IntegrationTests/Test.pdf b/IntegrationTests/Test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..50a038977b732f8c86889b164166410d5d60e0de Binary files /dev/null and b/IntegrationTests/Test.pdf differ diff --git a/IntegrationTests/http-client.env.json b/IntegrationTests/http-client.env.json new file mode 100644 index 0000000000000000000000000000000000000000..a7edad8821b9c1673e132ac9b6e88fc511e6f142 --- /dev/null +++ b/IntegrationTests/http-client.env.json @@ -0,0 +1,8 @@ +{ + "sender": { + "baseurl": "https://auth-testing.fit-connect.fitko.dev", + "id": "73a8ff88-076b-4263-9a80-8ebadac97b0d", + "secret": "rdlXms-4ikO47AbTmmCTdzFoE4cTSt13JmSbcY5Dhsw", + "scope": "send:region:DE" + } +} \ No newline at end of file diff --git a/IntegrationTests/token.http b/IntegrationTests/token.http new file mode 100644 index 0000000000000000000000000000000000000000..ecfa23a547330937de73b59e17b59559f507cfa6 --- /dev/null +++ b/IntegrationTests/token.http @@ -0,0 +1,14 @@ +### Getting the data from the database BEARER +POST {{baseurl}}/token +Content-Type: application/x-www-form-urlencoded +Accept: application/json + +grant_type=client_credentials&client_id={{id}}&client_secret={{secret}} + +### Getting the data from the database BEARER +POST {{baseurl}}/token +Content-Type: application/x-www-form-urlencoded +Accept: application/json + +grant_type=client_credentials&client_id=WRONG&client_secret=WRONG + diff --git a/MockContainer/MockContainer.cs b/MockContainer/MockContainer.cs new file mode 100644 index 0000000000000000000000000000000000000000..2bfa17bde88b8a27fb6ef0f67c70d0a278edcc30 --- /dev/null +++ b/MockContainer/MockContainer.cs @@ -0,0 +1,181 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Autofac; +using FitConnect.Encryption; +using FitConnect.Models; +using FitConnect.Models.Api.Metadata; +using FitConnect.Services.Interfaces; +using FitConnect.Services.Models; +using FitConnect.Services.Models.v1.Destination; +using FitConnect.Services.Models.v1.Submission; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json; +using Data = FitConnect.Models.Data; +using JsonSerializer = System.Text.Json.JsonSerializer; +using Metadata = FitConnect.Models.Api.Metadata.Metadata; +using Route = FitConnect.Services.Models.v1.Routes.Route; + +namespace MockContainer; + +public record MockSettings(string PrivateKeyDecryption, string PrivateKeySigning, + string PublicKeyEncryption, string PublicKeySignatureVerification, string SenderClientId, + string SenderClientSecret, string SubscriberClientId, string SubscriberClientSecret, + string DestinationId, string LeikaKey); + +public class TestFile { + public byte[] Content; + public string Name; + + public TestFile() { + Content = File.ReadAllBytes("Test.pdf"); + Name = "Test.pdf"; + } +} + +public static class Container { + public static IContainer Create() { + var builder = new ContainerBuilder(); + + builder.Register(c => new TestFile()).As<TestFile>(); + builder.Register(c => CreateOAuthService().Object).As<IOAuthService>(); + builder.Register(c => CreateRouteService().Object).As<IRouteService>(); + builder.Register(c => CreateSubmissionService().Object).As<ISubmissionService>(); + builder.Register(c => CreateDestinationService().Object).As<IDestinationService>(); + builder.Register(c => Mock.Of<ICasesService>()).As<ICasesService>(); + builder.Register(c => LoggerFactory.Create( + b => { + b.AddSimpleConsole(); + b.SetMinimumLevel(LogLevel.Information); + }).CreateLogger("FluentSenderTests") + ).As<ILogger>(); + + CreateEncryptionSettings(builder); + + return builder.Build(); + } + + private static Mock<IDestinationService> CreateDestinationService() { + var mock = new Mock<IDestinationService>(); + mock.Setup(x => x.GetPublicKey(It.IsAny<string>())).Returns(() => + File.ReadAllTextAsync("./encryptionKeys/publicKey_encryption.json")); + return mock; + } + + private static void CreateEncryptionSettings(ContainerBuilder builder) { + var privateKeyDecryption = File.ReadAllText("./encryptionKeys/privateKey_decryption.json"); + var privateKeySigning = File.ReadAllText("./encryptionKeys/privateKey_signing.json"); + var publicKeyEncryption = File.ReadAllText("./encryptionKeys/publicKey_encryption.json"); + var publicKeySignature = + File.ReadAllText("./encryptionKeys/publicKey_signature_verification.json"); + + var credentials = + JsonConvert.DeserializeObject<dynamic>( + File.ReadAllText("./encryptionKeys/credentials.json")); + + var senderClientId = (string)credentials.sender.clientId; + var senderClientSecret = (string)credentials.sender.clientSecret; + var subscriberClientId = (string)credentials.subscriber.clientId; + var subscriberClientSecret = (string)credentials.subscriber.clientSecret; + var destinationId = (string)credentials.destinationId; + var leikaKey = (string)credentials.leikaKey; + + builder.Register(c => new MockSettings( + privateKeyDecryption, privateKeySigning, + publicKeyEncryption, publicKeySignature, + senderClientId, senderClientSecret, + subscriberClientId, subscriberClientSecret, + destinationId, leikaKey)) + .As<MockSettings>(); + builder.Register(c => new KeySet { + PrivateKeyDecryption = privateKeyDecryption, + PrivateKeySigning = privateKeySigning, + PublicKeyEncryption = publicKeyEncryption, + PublicKeySignatureVerification = publicKeySignature + }).As<KeySet>(); + } + + private static Mock<IRouteService> CreateRouteService() { + var routeService = new Mock<IRouteService>(); + routeService.Setup(r => r.GetDestinationIdAsync( + It.Is<string>(s => !string.IsNullOrWhiteSpace(s)), + It.IsAny<string>(), + It.IsAny<string>(), + It.IsAny<string?>())) + .Returns(() => Task.Run(() => new List<Route>())); + return routeService; + } + + private static Mock<IOAuthService> CreateOAuthService() { + var oAuthService = new Mock<IOAuthService>(); + oAuthService.SetupGet(o => o.IsAuthenticated).Returns(true); + return oAuthService; + } + + private static Mock<ISubmissionService> CreateSubmissionService() { + var submissionService = new Mock<ISubmissionService>(); + submissionService.Setup(s => + s.ListSubmissions(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).Returns( + () => + Task.Run(() => JsonConvert.DeserializeObject<SubmissionsForPickupDto>( + @"{""offset"":0,""count"":3,""totalCount"":3,""submissions"":[{""destinationId"":""879ee109-a690-4db8-ab32-424284184d7d"",""submissionId"":""ce75a6b8-d72f-4b94-b09e-af6be35bc2ae""},{""destinationId"":""19c8489b-29b8-422f-b7db-919852cfb04b"",""submissionId"":""e364430f-5a3b-4284-ba9a-f2867ba421e6""},{""destinationId"":""80a0aac3-148d-42bb-9366-516ce6355348"",""submissionId"":""530ba588-2db9-4899-ab0d-0c0b57689271""}]}")) + ); + submissionService.Setup(s => s.CreateSubmission(It.IsAny<CreateSubmissionDto>())).Returns( + () => new SubmissionCreatedDto { + DestinationId = "destinationId", + CaseId = "caseId" + }); + + submissionService.Setup(s => s.GetKey(It.IsAny<string>())).Returns(() => + "{\"kty\":\"RSA\",\"key_ops\":[\"wrapKey\"],\"alg\":\"RSA-OAEP-256\",\"x5c\":[\"MIIFCTCCAvECBAONrDowDQYJKoZIhvcNAQENBQAwSTELMAkGA1UEBhMCREUxFTATBgNVBAoMDFRlc3RiZWhvZXJkZTEjMCEGA1UEAwwaRklUIENvbm5lY3QgVGVzdHplcnRpZmlrYXQwHhcNMjIwNjI4MTE1MzE1WhcNMzIwNjI1MTE1MzE1WjBJMQswCQYDVQQGEwJERTEVMBMGA1UECgwMVGVzdGJlaG9lcmRlMSMwIQYDVQQDDBpGSVQgQ29ubmVjdCBUZXN0emVydGlmaWthdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM4xtyHNdvpBlol6lROwg4yAsgG9zKQ7oQkjNTUxnbsuiqhRPpniY1/ZbtTEZqrsDLtSzb9B48eJLIgdGS+mI5Z6kFTG1XaMZ46XEJxZNY/Ldf53YTmrW3QqxGV05t7/ylqWQZtSCPDsHjfX5SyoJOFTPWMqFjLiKllJzBi2j/FTbJ73K1gVUv7V7UiRuARfqScDUH6egNKza7CpvzARft7kyF3za3cm4w1rtV6yBkduWOq/JLzctMsp7vbX4S8AW6m4BvD2JLSMukx7KXY2u0AgtmnzNCDXItRo/+PGH3rC3C27/oyIyykJ94fZsSiz2FffnbEUgmU+x6RzwWfT3KorgG3wI6cECiBmFCZLdBhjoZzPpYyr6LBrIeMbAip3CWxNnayB2zNZIhlrhkPw1WkxcZDBLJaql/P7dQKknU1g94KiPtNjxnO8NSfolFkLGQ3zT0LIoSXYPyHMOiG8xgKczSMgBXkkoCTTSUqWOXlx2QeFdCKBkxxW947fMKpA8nGIhIk4JiycpV1Xf5eQ5alz+Ox3QeUSi22mSN4hhDMyvfLwZeKlpIsQwcQ8zjWuXllHJAdfr3WGLj5LCk6i5NVpYfWtm++SrDiSnGgTlxB7n1SAWcvGvhhm5INta5xgmFOY5i3DwYo9A9NzxZw+7tEZT2k3uY1gmxd6GDJkOhIXAgMBAAEwDQYJKoZIhvcNAQENBQADggIBAI2knmZldxsrJp6Lyk+eLEVbrZI3BuHdfs+9q5I8R2b/RoqEuI05ZxoFPMZGyOMUINsXpthWMGsyALs512nmzR4bD1NGu5Ada3P/UiK3zaZk+xpJ9qAJo9xATRlGkdPhW5HyztUap1M+UW0GA/fDZJLRDVAlF+8czCPahP8ZhP5pkZcPTL/xxvQwDmadmdDRAEObOQlx58SwqQYC/FnO6lEFRhY+Hak9W4E1MoegoG8KFwOvqVRiRx3IVy1vdMQZgRRLDLkZzKIlI8WwkJONObVqdSrF2HfnEk6jhsG7/4Prn16XBSRi7wdqLOnnUpxwKsvL7BZVqAPcJ821XLxZ8wmRVItMnO3qJPP6RqVj7wfB7hnUOLHDywbGGWSrk0gi3x81x3tYXf8S+AbEn59uGIiArZjKmErvgFdtCWS8ILd7EFGYaMSYCCaJxoM/N5LckxAQXmsCgiLeOxXez2+H/uTRbctc5XEgNuVt+k92bF8amaGi5gUO60v7k4hPsnSHFzIqhsfiiOE6Pewyt96teAx4Xc2vMULcc2MUOHeiqK77OzHj8jN+/nztVSnYSrbaPYhJuIXVW5UZExG8kCTSNKK3aS+4++El7sbIHsqBTKvTmQvOMaBrGbZZR5jy5RTZSPi4njBdkjzRogloZRBoOSLebbR4B+4LMeoidxoOQN6O\"],\"kid\":\"vr7cWKGl-g4Wa_CRGowNhAYW_gQb-akMbiigxN0EkDI\",\"n\":\"zjG3Ic12-kGWiXqVE7CDjICyAb3MpDuhCSM1NTGduy6KqFE-meJjX9lu1MRmquwMu1LNv0Hjx4ksiB0ZL6YjlnqQVMbVdoxnjpcQnFk1j8t1_ndhOatbdCrEZXTm3v_KWpZBm1II8OweN9flLKgk4VM9YyoWMuIqWUnMGLaP8VNsnvcrWBVS_tXtSJG4BF-pJwNQfp6A0rNrsKm_MBF-3uTIXfNrdybjDWu1XrIGR25Y6r8kvNy0yynu9tfhLwBbqbgG8PYktIy6THspdja7QCC2afM0INci1Gj_48YfesLcLbv-jIjLKQn3h9mxKLPYV9-dsRSCZT7HpHPBZ9PcqiuAbfAjpwQKIGYUJkt0GGOhnM-ljKvosGsh4xsCKncJbE2drIHbM1kiGWuGQ_DVaTFxkMEslqqX8_t1AqSdTWD3gqI-02PGc7w1J-iUWQsZDfNPQsihJdg_Icw6IbzGApzNIyAFeSSgJNNJSpY5eXHZB4V0IoGTHFb3jt8wqkDycYiEiTgmLJylXVd_l5DlqXP47HdB5RKLbaZI3iGEMzK98vBl4qWkixDBxDzONa5eWUckB1-vdYYuPksKTqLk1Wlh9a2b75KsOJKcaBOXEHufVIBZy8a-GGbkg21rnGCYU5jmLcPBij0D03PFnD7u0RlPaTe5jWCbF3oYMmQ6Ehc\",\"e\":\"AQAB\",\"use\":null}"); + + submissionService + .Setup(c => c.SubmitSubmission(It.IsAny<string>(), It.IsAny<SubmitSubmissionDto>())) + .Returns( + (string id, SubmitSubmissionDto dto) => { + Console.WriteLine( + $@"Submitting submission {id} with + {JsonSerializer.Serialize(dto, new JsonSerializerOptions { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true, + Encoder = JavaScriptEncoder.Default + })}"); + return Task.Run(() => new SubmissionReducedDto()); + }); + + var data = new Data(); + + var metadata = new Metadata { + PublicServiceType = new Verwaltungsleistung { + Name = "Verwaltungsleistung" + } + }; + + + var encryptData = ""; + + var encryptMetadata = ""; + + submissionService.Setup(s => s.GetSubmission(It.IsAny<string>())) + .Returns((string id) => + new SubmissionDto { + SubmissionId = id, + DestinationId = "destination", + Callback = new CallbackDto { + Secret = "secret", + Url = "http://localhost:8080/callback" + }, + EncryptedData = encryptData, + EncryptedMetadata = encryptMetadata, + Attachments = new List<string>(), + CaseId = "caseId", + ServiceType = new ServiceTypeDto { + Description = "ServiceType", + Identifier = "Service identifier", + Name = "Dummy Service Type" + } + }); + return submissionService; + } +} diff --git a/MockContainer/MockContainer.csproj b/MockContainer/MockContainer.csproj new file mode 100644 index 0000000000000000000000000000000000000000..aa996437736a92a58456a8e7d0d74f0a15a938c7 --- /dev/null +++ b/MockContainer/MockContainer.csproj @@ -0,0 +1,45 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Autofac" Version="6.4.0" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" /> + <PackageReference Include="Moq" Version="4.18.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> + </ItemGroup> + + <ItemGroup> + <None Update="encryptionKeys\privateKey_decryption.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="encryptionKeys\privateKey_signing.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="encryptionKeys\publicKey_encryption.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="encryptionKeys\publicKey_signature_verification.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="encryptionKeys\set-public-keys.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="Test.pdf"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="encryptionKeys\credentials.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + </ItemGroup> + +</Project> diff --git a/MockContainer/Test.pdf b/MockContainer/Test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..50a038977b732f8c86889b164166410d5d60e0de Binary files /dev/null and b/MockContainer/Test.pdf differ diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000000000000000000000000000000000000..1a588142cbbc9dcf8188bb279c128e462a77aedf --- /dev/null +++ b/changelog.md @@ -0,0 +1,3 @@ +# Changelog + +## [Unreleased] diff --git a/readme.md b/readme.md index 901c707ae22453f6b40f4c80d75c515cd9f48a1d..4a04ed5766510d6e3887d3bbd932c5434c72567a 100644 --- a/readme.md +++ b/readme.md @@ -1,28 +1,204 @@ # Fit-Connect .NET SDK +For an implementation example take a look at the [DemoRunner](DemoRunner/Program.cs) + +## **!! IN DEVELOPMENT NOT FOR PRODUCTION USE !!** + **Fit-Connect .NET SDK** is a .NET library for the Fit-Connect API. -**IN DEVELOPMENT NOT FOR PRODUCTION USE** +Die Verwendung des SDKs ist in der [documentation (ger)](./Documentation/documentation.de-DE.md) +erklärt + +## For information how to use the SDK and FitConnect visit: + +* [SDK-Dokumentation](./documentation/documentation.de-DE.md) +* [FIT-Connect Dokumentation](https://docs.fitko.de/fit-connect/docs) +* [FIT-Connect Dokumentation](https://fit-connect.com/docs) + +<!-- +## Structure +For the structure look the [Structure documentation](structure.md) +--> + +# Allgemeines + +## Environment + +Das FitConnect SDK kann an die folgenden Umgebungen angeschlossen werden: -For an implementation example take a look at the [DummyClient](DummyClient/Program.cs) +- Testing: ```FitConnectEnvironment.Testing``` +- Staging: ```FitConnectEnvironment.Staging``` +- Production: ```FitConnectEnvironment.Production``` -## Ignored Files -You need a secret file for e2e test like: +[FIT Connect Umgebungen](https://docs.fitko.de/fit-connect/docs/getting-started/first-steps/#environments) -```json -{ - "sender": { - "id": "00000000-0000-0000-0000-000000000000", - "secret": "0000000000000000000000000000000000000000000", - "scope": "send:region:DE" - } -} +Hierfür muss der Client die Environment auswählen oder einen eigenen Environment-Parameter übergeben. + +## Credentials + +ClientId und ClientSecret sind die Grundlage, um einen Token vom OAuth-Server abfragen zu können. +Die beiden Werte sind im [Self-Service Portal der Testumgebung von FIT-Connect](https://portal.auth-testing.fit-connect.fitko.dev/clients) zu erstellen. + +# Sender + +```mermaid +flowchart LR + start([GetSender]) + destination(WithDestination) + service(WithServiceType) + attachments(WithAttachments) + data([WithData]) + + start-->destination-->service-->attachments-->data ``` -[glossary](https://docs.fitko.de/fit-connect/docs/glossary/) +Das SDK verhindert auf Grund der FluentAPI, dass die Methoden in der falschen Reihe aufgerufen werden können. + +## GetSender(FitConnectEnvironment.Testing, clientId, clientSecret, logger) + +Hier werden die FIT Connect Environment ausgewählt und die Credentials übergeben. +Der Parameter ```logger``` ist optional und muss das Interface ```Microsoft.Extensions.Logging.ILogger``` implementieren. + +## .WithDestination(destinationId) + +Die Destination ID des Zustelldienstes muss an dieser Stelle übergeben werden. + +_Noch nicht vollständig getestet!_<br> +Eine Möglichkeit, die Destination ID zu ermitteln, geht über die Methode ``FindDestinationId()`` des Senders. + +## .WithServiceType("FIT Connect Demo", leikaKey) + +Der Service Type muss an dieser Stelle übergeben werden. +Hierfür wird ein Leistungsschlüssel (LeiKa-Schlüssel) benötigt. +Leistungsschlüssel haben diese Form ```urn:de:fim:leika:leistung:99400048079000``` + +## .WithAttachments(new Attachment("Test.pdf", "Test Attachment")) + +Die Anhänge zum Antrag werden mit ```.WithAttachments``` übergeben. +Diese Methode erwartet ein Array von Attachment Objekten die als ```params``` übergeben werden können. + +Das Attachment kann mit den folgenden Parametern erstellt werden: + +- Metadaten und byte[] content +- Dateiname und Beschreibung + +Dazu werden zwei Konstruktoren bereitgestellt: + +- ```Attachment(Api.Metadata.Attachment metadata, byte[] content)``` +- ```Attachment(string fileName, string description,)``` + +## .WithData("{\"message\":\"Hello World\"}") + +Die Fachdaten werden als JSON String übergeben. + +## .Submit() + +Das Abschicken der Submission erfolgt mit diesem Aufruf. -### Tickets +## Beispiel -- [Method signatures](https://git.fitko.de/fit-connect/planning/-/issues/438) -- [Java SDK](https://git.fitko.de/fit-connect/planning/-/issues/413) \ No newline at end of file +```csharp +var submission = Client + .GetSender(FitConnectEnvironment.Development, clientId, clientSecret, logger) + .WithDestination(destinationId) + .WithServiceType("FIT Connect Demo", leikaKey) + .WithAttachments(new Attachment("Test.pdf", "Test Attachment")) + .WithData("{\"message\":\"Hello World\"}") + .Submit(); +``` + +# Subscriber + +Der Subscriber braucht zusätzliche Informationen, um die Submissions abrufen zu können. +Hier sind zusätzlich die Schlüssel zum Ver- und Entschlüsseln notwendig. + +````mermaid +flowchart LR + + start([GetSubscriber]) + availableSub(GetAvailableSubmission) + requestSub(RequestSubmission) + subscriberWithSub([SubscriberWithSubmission]) + subscriberWithSub_([SubscriberWithSubmission]) + data(GetDataJson) + attachments(GetAttachments) + accept(AcceptSubmission) + reject(RejectSubmission) + forward(ForwardSumbission) + finished{{Abgeschlossen}} + rejected{{Zurueckgewiesen}} + + start-->availableSub-->requestSub-->subscriberWithSub + + subscriberWithSub_-->data-->attachments + attachments-->accept-->finished + attachments-->reject-->rejected + attachments-->forward + + +```` + +## .GetSubscriber(...) + +Hier werden die FIT Connect Environment ausgewählt, die Keys und die Credentials übergeben. +Der Parameter ```logger``` ist optional und muss das Interface ```Microsoft.Extensions.Logging.ILogger``` implementieren. + +## .GetAvailableSubmissions() + +Liefert eine Liste mit den verfügbaren Submissions zurück. + +## .RequestSubmission(submissionId) + +Hiermit wird die Submission abgerufen und im Subscriber gespeichert. +Der Rückgabewert der Funktion ist also _Subscriber mit einer Submission_ + +## .GetDataJson() + +Liefert die Fachdaten als JSON String zurück. + +## .GetAttachments() + +Gibt eine Liste mit den Attachments der Submission zurück. +Die Attachments können so geprüft werden. + +## .AcceptSubmission() + +Akzepiert die Submission und löscht diese vom Server. + +## .RejectSubmission() + +Weißt die Submission zurück. + +## .ForwardSubmission() + +## Beispiel + +```csharp + var subscriber = Client.GetSubscriber(FitConnectEnvironment.Testing, clientId, + clientSecret, + privateKeyDecryption, + privateKeySigning, + publicKeyEncryption, + publicKeySignatureVerification, + logger); + + var submissions = subscriber.GetAvailableSubmissions(); + + // Alle verfügbaren Submissions laden + foreach (var submission in submissions) { + var subscriberWithSubmission = subscriber.RequestSubmission(submission.SubmissionId); + + // Laden der Anhänge + var attachments = subscriberWithSubmission + .GetAttachments(); + + // Ausgabe der Fachdaten auf der Konsole + logger.LogInformation("Fachdaten: {Data}", subscriberWithSubmission.GetDataJson()); + + // Submission akzeptieren und abschließen + subscriberWithSubmission.AcceptSubmission(); + } +``` + +[glossary](https://docs.fitko.de/fit-connect/docs/glossary/) diff --git a/structure.md b/structure.md new file mode 100644 index 0000000000000000000000000000000000000000..7449047f09344202257861f9c18b288de0a57562 --- /dev/null +++ b/structure.md @@ -0,0 +1,126 @@ +# Structure + +## Functional Diagram + +```mermaid + classDiagram + class FitConnectClient{ + FitConnectApiService + } + + class FitConnectApiService{ + CasesService + DestinationService + InfoService + OAuthService + RouteService + } + + FitConnectClient ..> SubmissionSender : public + FitConnectClient ..> SubmissionSubscriber : public + SubmissionSender --|> FunctionalBaseClass + SubmissionSubscriber --|> FunctionalBaseClass + FunctionalBaseClass ..> FitConnectApiService : protected + + FitConnectApiService ..> RouteService : public + FitConnectApiService ..> CasesService : public + FitConnectApiService ..> DestinationService : public + FitConnectApiService ..> InfoService : public + FitConnectApiService ..> OAuthService : public + + RouteService --|> RestCallService : Inheritance + CasesService --|> RestCallService : Inheritance + DestinationService --|> RestCallService : Inheritance + InfoService --|> RestCallService : Inheritance + OAuthService --|> RestCallService : Inheritance + +``` + +With that structure the user of the SDK is intended to see the following: (For easier reading all +methods are shown sync) + +```mermaid + classDiagram + class FitConnectClient { + << Draft >> + - Sender + - Subscriber + + SendSumission(submission) + + GetSubmissions(offset, limit) + + GetSubmission(id) + } + + class Sender { + << Draft >> + + bool CheckPublicKey(string publicKey) + + bool CheckPublicKey(byte[] publicKey) + + string EncryptData(string? data) + + string EncryptAttachment(byte[] attachment) + + string CreateMetadata(string data, byte[] attachment) ??? + + * Pruefung von oeffentlichen Schluesseln und Zertifikatsketten + OCSP-Check + * Verschluesselung von Fachdaten [JSON, XML] mittels JWE + * Verschluesselung von Anhängen [Binaerdaten] mittels JWE + * Korrekte Erzeugung eines Metadatensatzes inkl. Hashwerte + } + + class Subscriber { + << Draft >> + + byte[] DecryptAttachment(string attachment) + + SecurityEventToken CreateSecurityEventToken(string data, string attachment, string privateKey) + + bool CheckMetadata(string metaData) + + string DecryptData(string data) + + bool CheckHash(string metaData, string hash) + + * Entschluesselung von Fachdaten [JSON oder XML] mittels JWE + * Entschluesselung von Anhaengen [Binaerdaten] mittels JWE + * Pruefung der empfangenen Metadaten gegen das zugehörige JSON-Schema + * Pruefung der Hashwerte aus dem Metadatensatz. + * SET-Erstellung inkl. Signaturerzeugung + + } + + class FunctionalBaseClass{ + + GetOAuthTokenAsync(string clientId, string clientSecret, string? scope) + + SecurityEventToken GetSetData() + * SET-Empfang inkl. Signaturpruefung + * Unterstuezung / Abstraktion der API-Nutzung + * Abruf des OAuth-Tokens + } + + FitConnectClient <--> Sender + FitConnectClient <--> Subscriber + + Sender --|> FunctionalBaseClass + Subscriber --|> FunctionalBaseClass +``` + +## Class diagram + +```mermaid + classDiagram + class Client{ + } +``` + +## Annotation + +The structure ensures an abstraction of the API calls for the user and a 'simple' way to change the +API calls if a new version of the API is released. Currently the API call is not versioned and hard +coded. + +### Abstractions for API calls + +- The use of a dependency injection can be added to + inject the API calls into the SDK. But is not yet implemented. +- Another approach would be using a generic class for the API calls. + +## Implementation + +### Sender + +#### WithDestination(string destinationId) + +1. Call server to check if the destination is valid. +2. Get public key for the destination and store the public key in the submission. +3. Create the submission to get the submissionId diff --git a/test.Dockerfile b/test.Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..7c37cb3a64135354d516de3366e209896dfc16a8 --- /dev/null +++ b/test.Dockerfile @@ -0,0 +1,6 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 +WORKDIR /test +COPY . . +RUN dotnet restore +RUN dotnet build +RUN dotnet test --filter E2ETests.SenderTest diff --git a/version.sh b/version.sh index 45424906652f8cb4330861392b7062a224e8295a..e3da4df0add6637e02224dad4127ccc1d30be6b2 100755 --- a/version.sh +++ b/version.sh @@ -1,34 +1,59 @@ #!/bin/zsh +### Setup OpenSSL +if [ "$(uname)" = "Darwin" ]; then + echo "macOS" + echo "Trying to import openssl" + OPENSSL_PATH=/usr/local/opt/openssl@1.1/lib + if test -d "$OPENSSL_PATH"; then + export DYLD_LIBRARY_PATH=$OPENSSL_PATH + else + echo "OpenSSL not found" + exit 100 + fi +elif [ "$(uname)" = "Linux" ]; then + echo "Linux" +else + echo "Unknown" +fi + +### Running build script if [ -z "$1" ]; then echo "Usage: $0 <version>" exit 1 fi -sed -i "" -e "s|<AssemblyVersion>.*</AssemblyVersion>|<AssemblyVersion>$1</AssemblyVersion>|" FitConnect/FitConnect.csproj -sed -i "" -e "s|<FileVersion>.*</FileVersion>|<FileVersion>$1</FileVersion>|" FitConnect/FitConnect.csproj +FILE_VERSION=$($1 | sed -r 's|^(([0-9]+).([0-9]+).([0-9]+))(.*)$|\1|') + +sed -i "" -e "s|<AssemblyVersion>.*</AssemblyVersion>|<AssemblyVersion>$FILE_VERSION</AssemblyVersion>|" FitConnect/FitConnect.csproj +sed -i "" -e "s|<FileVersion>.*</FileVersion>|<FileVersion>$FILE_VERSION</FileVersion>|" FitConnect/FitConnect.csproj sed -i "" -e "s|<PackageVersion>.*</PackageVersion>|<PackageVersion>$1</PackageVersion>|" FitConnect/FitConnect.csproj +dotnet clean -if ! dotnet test; then - echo "Test failed" - exit 1 -fi +#if ! dotnet test -c Release; then +# echo "Test failed" +# exit 1 +#fi -if ! dotnet build; then +if ! dotnet build FitConnect -c Release; then echo "Build failed" exit 1 fi -if ! dotnet pack; then +if ! dotnet pack -c Release; then echo "Pack failed" exit 1 fi -CURRENT=$(git branch | grep \* | cut -d ' ' -f2) -if [ "$CURRENT" != "master" ]; then - echo "Not on master branch" - exit 1 -fi +cp "FitConnect/bin/Release/FitConnect.$1.nupkg" . + +exit 0 + +#CURRENT=$(git branch | grep \* | cut -d ' ' -f2) +#if [ "$CURRENT" != "master" ]; then +# echo "Not on master branch" +# exit 1 +#fi -# Here you can do the release things... \ No newline at end of file +# Here you can do the release things... diff --git a/working_notes.md b/working_notes.md index 517d433b53dbfa873b286a70452895f17e60a3eb..37494dfd9b86ca9b98063b8c25ba0b35243dc2da 100644 --- a/working_notes.md +++ b/working_notes.md @@ -2,21 +2,7 @@ # TODOS -| interface | implemented | tested | scope | description | -|:---------:|:-----------:|:------:|:-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| X | X | X | Sender,Subscriber | Abruf von OAuth-Tokens | -| X | | | Sender | Prüfung von öffentlichen Schlüsseln und Zertifikatsketten + OCSP-Check (vgl. [#119](https://git.fitko.de/fit-connect/planning/-/issues/119)) | -| X | | | Sender | Verschlüsselung von Fachdaten (JSON, XML) mittels JWE | -| X | | | Sender | Verschlüsselung von Anhängen (Binärdaten) mittels JWE | -| X | | | Sender | Korrekte Erzeugung eines Metadatensatzes inkl. [Hashwerte](https://docs.fitko.de/fit-connect/docs/sending/metadata#integrity) | -| X | | | Subscriber | Entschlüsselung von Fachdaten (JSON oder XML) mittels JWE | -| X | | | Subscriber | Entschlüsselung von Anhängen (Binärdaten) mittels JWE | -| X | X | | Subscriber | Prüfung der empfangenen Metadaten gegen das zugehörige JSON-Schema | -| X | | | Subscriber | [Prüfung der Hashwerte](https://docs.fitko.de/fit-connect/docs/receiving/verification#integrity) aus dem Metadatensatz. | -| X | | | Subscriber | SET-Erstellung inkl. Signaturerzeugung | -| X | | | Sender, Subscriber | SET-Empfang inkl. Signaturprüfung | -| X | | | Sender, Subscriber | Unterstüzung / Abstraktion der API-Nutzung (`fitconnect.sendSubmission(metadata, destinationID, ...)` o.ä.) für die oben genannten Use-Cases | -| X | | | Sender, Subscriber | Logging (Logging-Modul muss von außen kommen) | +Ticket: [Issue 440](https://git.fitko.de/fit-connect/planning/-/issues/440) ## Links