From a140a481136eb2e97338b96be5a5732086945a34 Mon Sep 17 00:00:00 2001 From: Phil Schneider Date: Wed, 17 Jul 2024 18:01:14 +0200 Subject: [PATCH] feat: add technical user deletion logic and adjust exception handling for encryption (#50) Reviewed-By: Evelyn Gurschler --- .../argocd-app-templates/appsetup-dev.yaml | 2 +- .../argocd-app-templates/appsetup-int.yaml | 4 +- .../argocd-app-templates/appsetup-rc.yaml | 42 ++ consortia/environments/values-dev.yaml | 4 +- consortia/environments/values-rc.yaml | 105 ++++ src/clients/Dim.Clients/Api/Cf/CfClient.cs | 11 +- src/clients/Dim.Clients/Api/Cf/ICfClient.cs | 1 + src/clients/Dim.Clients/Dim.Clients.csproj | 6 +- src/database/Dim.DbAccess/Dim.DbAccess.csproj | 4 +- .../Repositories/ITenantRepository.cs | 3 + .../Repositories/TenantRepository.cs | 16 + src/database/Dim.Entities/Dim.Entities.csproj | 2 +- .../Dim.Entities/Enums/ProcessStepTypeId.cs | 6 +- .../Dim.Entities/Enums/ProcessTypeId.cs | 2 +- .../Dim.Migrations/Dim.Migrations.csproj | 6 +- .../20240717155124_1.2.0.Designer.cs | 575 ++++++++++++++++++ .../Migrations/20240717155124_1.2.0.cs | 94 +++ .../Migrations/DimDbContextModelSnapshot.cs | 18 +- src/database/Dim.Migrations/Program.cs | 4 +- .../TechnicalUserProcessTypeExecutor.cs | 12 +- .../Callback/CallbackService.cs | 7 + .../Callback/ICallbackService.cs | 2 + .../DimProcess.Library.csproj | 4 +- .../ITechnicalUserProcessHandler.cs | 4 +- .../TechnicalUserProcessHandler.cs | 53 +- .../Processes.Worker.Library.csproj | 6 +- .../Processes.Worker/Processes.Worker.csproj | 2 +- src/processes/Processes.Worker/Program.cs | 2 +- .../Dim.Web/BusinessLogic/DimBusinessLogic.cs | 24 +- .../BusinessLogic/IDimBusinessLogic.cs | 3 +- src/web/Dim.Web/Controllers/DimController.cs | 11 +- src/web/Dim.Web/Dim.Web.csproj | 2 +- .../ErrorHandling/DimErrorMessageContainer.cs | 4 +- src/web/Dim.Web/Program.cs | 6 +- .../TechnicalUserProcessHandlerTests.cs | 129 +++- 35 files changed, 1103 insertions(+), 73 deletions(-) create mode 100644 consortia/argocd-app-templates/appsetup-rc.yaml create mode 100644 consortia/environments/values-rc.yaml create mode 100644 src/database/Dim.Migrations/Migrations/20240717155124_1.2.0.Designer.cs create mode 100644 src/database/Dim.Migrations/Migrations/20240717155124_1.2.0.cs diff --git a/consortia/argocd-app-templates/appsetup-dev.yaml b/consortia/argocd-app-templates/appsetup-dev.yaml index 56d22ad..a6d9f0c 100644 --- a/consortia/argocd-app-templates/appsetup-dev.yaml +++ b/consortia/argocd-app-templates/appsetup-dev.yaml @@ -28,7 +28,7 @@ spec: server: 'https://kubernetes.default.svc' source: path: charts/dim - repoURL: 'https://github.com/sap/dim-client.git' + repoURL: 'https://github.com/sap/ssi-dim-middle-layer.git' targetRevision: main plugin: env: diff --git a/consortia/argocd-app-templates/appsetup-int.yaml b/consortia/argocd-app-templates/appsetup-int.yaml index 81e9343..99b179a 100644 --- a/consortia/argocd-app-templates/appsetup-int.yaml +++ b/consortia/argocd-app-templates/appsetup-int.yaml @@ -28,8 +28,8 @@ spec: server: 'https://kubernetes.default.svc' source: path: charts/dim - repoURL: 'https://github.com/sap/dim-client.git' - targetRevision: dim-1.1.0 + repoURL: 'https://github.com/sap/ssi-dim-middle-layer.git' + targetRevision: dim-1.0.0 plugin: env: - name: AVP_SECRET diff --git a/consortia/argocd-app-templates/appsetup-rc.yaml b/consortia/argocd-app-templates/appsetup-rc.yaml new file mode 100644 index 0000000..2acf2be --- /dev/null +++ b/consortia/argocd-app-templates/appsetup-rc.yaml @@ -0,0 +1,42 @@ +############################################################### +# Copyright (c) 2024 BMW Group AG +# Copyright 2024 SAP SE or an SAP affiliate company and ssi-dim-middle-layer contributors. +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: dim +spec: + destination: + namespace: product-iam + server: 'https://kubernetes.default.svc' + source: + path: charts/dim + repoURL: 'https://github.com/sap/ssi-dim-middle-layer.git' + targetRevision: main + plugin: + env: + - name: AVP_SECRET + value: vault-secret + - name: helm_args + value: '-f values.yaml -f ../../consortia/environments/values-rc.yaml' + project: project-portal + syncPolicy: + automated: + prune: true diff --git a/consortia/environments/values-dev.yaml b/consortia/environments/values-dev.yaml index 56b8b40..c341c3d 100644 --- a/consortia/environments/values-dev.yaml +++ b/consortia/environments/values-dev.yaml @@ -43,7 +43,7 @@ dim: tag: "main" imagePullPolicy: "Always" swaggerEnabled: true - rootDirectoryId: "27fee02a-e265-4cfc-af70-4f217a33840b" + rootDirectoryId: "ee464a81-fca4-431d-8315-5db5e49b4c3c" operatorId: "27fee02a-e265-4cfc-af70-4f217a33840b" migrations: @@ -63,7 +63,7 @@ processesworker: adminMail: "phil.schneider@digitalnativesolutions.de" clientIdCisCentral: "" clientSecretCisCentral: "" - authUrl: "https://catena-x-int-dim.authentication.eu10.hana.ondemand.com" + authUrl: "https://catena-x-dev-dim.authentication.eu10.hana.ondemand.com" subaccount: # -- Url to the subaccount service api baseUrl: "https://accounts-service.cfapps.eu10.hana.ondemand.com" diff --git a/consortia/environments/values-rc.yaml b/consortia/environments/values-rc.yaml new file mode 100644 index 0000000..67b7f0b --- /dev/null +++ b/consortia/environments/values-rc.yaml @@ -0,0 +1,105 @@ +############################################################### +# Copyright (c) 2024 BMW Group AG +# Copyright 2024 SAP SE or an SAP affiliate company and ssi-dim-middle-layer contributors. +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### + +ingress: + enabled: true + className: "nginx" + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/proxy-body-size: "8m" + nginx.ingress.kubernetes.io/cors-allow-origin: "http://localhost:3000, https://*.dev.demo.catena-x.net" + tls: + - secretName: "tls-secret" + hosts: + - "dim-rc.dev.demo.catena-x.net" + hosts: + - host: "dim-rc.dev.demo.catena-x.net" + paths: + - path: "/api/dim" + pathType: "Prefix" + backend: + port: 8080 + +dim: + image: + tag: "main" + imagePullPolicy: "Always" + swaggerEnabled: true + rootDirectoryId: "ee464a81-fca4-431d-8315-5db5e49b4c3c" + operatorId: "27fee02a-e265-4cfc-af70-4f217a33840b" + +migrations: + image: + tag: "main" + imagePullPolicy: "Always" + logging: + default: "Debug" + +processesworker: + image: + tag: "main" + imagePullPolicy: "Always" + logging: + default: "Debug" + dim: + adminMail: "phil.schneider@digitalnativesolutions.de" + clientIdCisCentral: "" + clientSecretCisCentral: "" + authUrl: "https://catena-x-dev-dim.authentication.eu10.hana.ondemand.com" + subaccount: + # -- Url to the subaccount service api + baseUrl: "https://accounts-service.cfapps.eu10.hana.ondemand.com" + entitlement: + # -- Url to the entitlement service api + baseUrl: "https://entitlements-service.cfapps.eu10.hana.ondemand.com" + cf: + clientId: "" + clientSecret: "" + tokenAddress: "https://login.cf.eu10.hana.ondemand.com/oauth/token" + # -- Url to the cf service api + baseUrl: "https://api.cf.eu10.hana.ondemand.com" + grantType: "client_credentials" + callback: + scope: "openid" + grantType: "client_credentials" + # -- Provide client-id for callback. + clientId: "" + # -- Client-secret for callback client-id. Secret-key 'callback-client-secret'. + clientSecret: "" + tokenAddress: "http://centralidp-rc.dev.demo.catena-x.net/auth/realms/CX-Central/protocol/openid-connect/token" + # -- Url to the cf service api + baseAddress: "https://portal-backend-rc.dev.demo.catena-x.net" + technicalUserCreation: + encryptionConfigs: + index0: + encryptionKey: "<" + +idp: + address: "https://centralidp-rc.dev.demo.catena-x.net" + jwtBearerOptions: + tokenValidationParameters: + validAudience: "DIM-Middle-Layer" + +postgresql: + auth: + postgrespassword: "" + password: "" + replicationPassword: "" \ No newline at end of file diff --git a/src/clients/Dim.Clients/Api/Cf/CfClient.cs b/src/clients/Dim.Clients/Api/Cf/CfClient.cs index 9b34567..1df819e 100644 --- a/src/clients/Dim.Clients/Api/Cf/CfClient.cs +++ b/src/clients/Dim.Clients/Api/Cf/CfClient.cs @@ -79,8 +79,8 @@ private static async Task GetEnvironmentId(string tenantName, Cancellation .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) .ConfigureAwait(false); - var tenantEnvironment = environments.Resources.Where(x => x.Name == tenantName); - if (tenantEnvironment.Count() > 1) + var tenantEnvironment = environments?.Resources.Where(x => x.Name == tenantName); + if (tenantEnvironment == null || tenantEnvironment.Count() > 1) { throw new ConflictException($"There should only be one cf environment for tenant {tenantName}"); } @@ -277,4 +277,11 @@ public async Task GetServiceBindingDetai throw new ServiceException(je.Message); } } + + public async Task DeleteServiceInstanceBindings(Guid serviceBindingId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + await client.DeleteAsync($"/v3/service_credential_bindings/{serviceBindingId}", cancellationToken) + .CatchingIntoServiceExceptionFor("delete-si-bindings", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS); + } } diff --git a/src/clients/Dim.Clients/Api/Cf/ICfClient.cs b/src/clients/Dim.Clients/Api/Cf/ICfClient.cs index 2a08224..1e34fc6 100644 --- a/src/clients/Dim.Clients/Api/Cf/ICfClient.cs +++ b/src/clients/Dim.Clients/Api/Cf/ICfClient.cs @@ -30,4 +30,5 @@ public interface ICfClient Task CreateServiceInstanceBindings(string tenantName, string? keyName, Guid spaceId, CancellationToken cancellationToken); Task GetServiceBinding(string tenantName, Guid spaceId, string bindingName, CancellationToken cancellationToken); Task GetServiceBindingDetails(Guid id, CancellationToken cancellationToken); + Task DeleteServiceInstanceBindings(Guid serviceBindingId, CancellationToken cancellationToken); } diff --git a/src/clients/Dim.Clients/Dim.Clients.csproj b/src/clients/Dim.Clients/Dim.Clients.csproj index 598ed58..dfbb878 100644 --- a/src/clients/Dim.Clients/Dim.Clients.csproj +++ b/src/clients/Dim.Clients/Dim.Clients.csproj @@ -36,9 +36,9 @@ - - - + + + diff --git a/src/database/Dim.DbAccess/Dim.DbAccess.csproj b/src/database/Dim.DbAccess/Dim.DbAccess.csproj index f63bb72..599f399 100644 --- a/src/database/Dim.DbAccess/Dim.DbAccess.csproj +++ b/src/database/Dim.DbAccess/Dim.DbAccess.csproj @@ -30,8 +30,8 @@ - - + + diff --git a/src/database/Dim.DbAccess/Repositories/ITenantRepository.cs b/src/database/Dim.DbAccess/Repositories/ITenantRepository.cs index 5c28938..4600594 100644 --- a/src/database/Dim.DbAccess/Repositories/ITenantRepository.cs +++ b/src/database/Dim.DbAccess/Repositories/ITenantRepository.cs @@ -43,4 +43,7 @@ public interface ITenantRepository Task<(Guid? spaceId, string technicalUserName)> GetSpaceIdAndTechnicalUserName(Guid technicalUserId); Task<(Guid ExternalId, string? TokenAddress, string? ClientId, byte[]? ClientSecret, byte[]? InitializationVector, int? EncryptionMode)> GetTechnicalUserCallbackData(Guid technicalUserId); Task<(Guid? DimInstanceId, Guid? CompanyId)> GetDimInstanceIdAndDid(Guid tenantId); + Task<(bool Exists, Guid TechnicalUserId, Guid ProcessId)> GetTechnicalUserForBpn(string bpn, string technicalUserName); + Task GetExternalIdForTechnicalUser(Guid technicalUserId); + void RemoveTechnicalUser(Guid technicalUserId); } diff --git a/src/database/Dim.DbAccess/Repositories/TenantRepository.cs b/src/database/Dim.DbAccess/Repositories/TenantRepository.cs index 8c5d049..2823801 100644 --- a/src/database/Dim.DbAccess/Repositories/TenantRepository.cs +++ b/src/database/Dim.DbAccess/Repositories/TenantRepository.cs @@ -145,4 +145,20 @@ public void AttachAndModifyTechnicalUser(Guid technicalUserId, Action x.Id == tenantId) .Select(x => new ValueTuple(x.DimInstanceId, x.CompanyId)) .SingleOrDefaultAsync(); + + public Task<(bool Exists, Guid TechnicalUserId, Guid ProcessId)> GetTechnicalUserForBpn(string bpn, string technicalUserName) => + context.TechnicalUsers + .Where(x => x.TechnicalUserName == technicalUserName && x.Tenant!.Bpn == bpn) + .Select(x => new ValueTuple(true, x.Id, x.ProcessId)) + .SingleOrDefaultAsync(); + + public Task GetExternalIdForTechnicalUser(Guid technicalUserId) => + context.TechnicalUsers + .Where(x => x.Id == technicalUserId) + .Select(x => x.ExternalId) + .SingleOrDefaultAsync(); + + public void RemoveTechnicalUser(Guid technicalUserId) => + context.TechnicalUsers + .Remove(new TechnicalUser(technicalUserId, default, default, null!, default)); } diff --git a/src/database/Dim.Entities/Dim.Entities.csproj b/src/database/Dim.Entities/Dim.Entities.csproj index e2022c5..abb9099 100644 --- a/src/database/Dim.Entities/Dim.Entities.csproj +++ b/src/database/Dim.Entities/Dim.Entities.csproj @@ -34,6 +34,6 @@ all - + diff --git a/src/database/Dim.Entities/Enums/ProcessStepTypeId.cs b/src/database/Dim.Entities/Enums/ProcessStepTypeId.cs index 918ce57..c11b7de 100644 --- a/src/database/Dim.Entities/Enums/ProcessStepTypeId.cs +++ b/src/database/Dim.Entities/Enums/ProcessStepTypeId.cs @@ -45,5 +45,9 @@ public enum ProcessStepTypeId // Create Technical User CREATE_TECHNICAL_USER = 100, GET_TECHNICAL_USER_DATA = 101, - SEND_TECHNICAL_USER_CALLBACK = 102, + SEND_TECHNICAL_USER_CREATION_CALLBACK = 102, + + // Delete Technical User + DELETE_TECHNICAL_USER = 200, + SEND_TECHNICAL_USER_DELETION_CALLBACK = 201 } diff --git a/src/database/Dim.Entities/Enums/ProcessTypeId.cs b/src/database/Dim.Entities/Enums/ProcessTypeId.cs index 9a35238..d3447d3 100644 --- a/src/database/Dim.Entities/Enums/ProcessTypeId.cs +++ b/src/database/Dim.Entities/Enums/ProcessTypeId.cs @@ -23,5 +23,5 @@ namespace Dim.Entities.Enums; public enum ProcessTypeId { SETUP_DIM = 1, - CREATE_TECHNICAL_USER = 2 + TECHNICAL_USER = 2, } diff --git a/src/database/Dim.Migrations/Dim.Migrations.csproj b/src/database/Dim.Migrations/Dim.Migrations.csproj index 240e8b3..07bdced 100644 --- a/src/database/Dim.Migrations/Dim.Migrations.csproj +++ b/src/database/Dim.Migrations/Dim.Migrations.csproj @@ -45,9 +45,9 @@ - - - + + + diff --git a/src/database/Dim.Migrations/Migrations/20240717155124_1.2.0.Designer.cs b/src/database/Dim.Migrations/Migrations/20240717155124_1.2.0.Designer.cs new file mode 100644 index 0000000..7e2da07 --- /dev/null +++ b/src/database/Dim.Migrations/Migrations/20240717155124_1.2.0.Designer.cs @@ -0,0 +1,575 @@ +/******************************************************************************** + * Copyright (c) 2024 BMW Group AG + * Copyright 2024 SAP SE or an SAP affiliate company and ssi-dim-middle-layer contributors. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// +using System; +using Dim.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Dim.Migrations.Migrations +{ + [DbContext(typeof(DimDbContext))] + [Migration("20240717155124_1.2.0")] + partial class _120 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("dim") + .UseCollation("en_US.utf8") + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Dim.Entities.Entities.Process", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("LockExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("lock_expiry_date"); + + b.Property("ProcessTypeId") + .HasColumnType("integer") + .HasColumnName("process_type_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uuid") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_processes"); + + b.HasIndex("ProcessTypeId") + .HasDatabaseName("ix_processes_process_type_id"); + + b.ToTable("processes", "dim"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_created"); + + b.Property("DateLastChanged") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_last_changed"); + + b.Property("Message") + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("ProcessStepStatusId") + .HasColumnType("integer") + .HasColumnName("process_step_status_id"); + + b.Property("ProcessStepTypeId") + .HasColumnType("integer") + .HasColumnName("process_step_type_id"); + + b.HasKey("Id") + .HasName("pk_process_steps"); + + b.HasIndex("ProcessId") + .HasDatabaseName("ix_process_steps_process_id"); + + b.HasIndex("ProcessStepStatusId") + .HasDatabaseName("ix_process_steps_process_step_status_id"); + + b.HasIndex("ProcessStepTypeId") + .HasDatabaseName("ix_process_steps_process_step_type_id"); + + b.ToTable("process_steps", "dim"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepStatus", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_process_step_statuses"); + + b.ToTable("process_step_statuses", "dim"); + + b.HasData( + new + { + Id = 1, + Label = "TODO" + }, + new + { + Id = 2, + Label = "DONE" + }, + new + { + Id = 3, + Label = "SKIPPED" + }, + new + { + Id = 4, + Label = "FAILED" + }, + new + { + Id = 5, + Label = "DUPLICATE" + }); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepType", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_process_step_types"); + + b.ToTable("process_step_types", "dim"); + + b.HasData( + new + { + Id = 1, + Label = "CREATE_SUBACCOUNT" + }, + new + { + Id = 2, + Label = "CREATE_SERVICEMANAGER_BINDINGS" + }, + new + { + Id = 3, + Label = "ASSIGN_ENTITLEMENTS" + }, + new + { + Id = 4, + Label = "CREATE_SERVICE_INSTANCE" + }, + new + { + Id = 5, + Label = "CREATE_SERVICE_BINDING" + }, + new + { + Id = 6, + Label = "SUBSCRIBE_APPLICATION" + }, + new + { + Id = 7, + Label = "CREATE_CLOUD_FOUNDRY_ENVIRONMENT" + }, + new + { + Id = 8, + Label = "CREATE_CLOUD_FOUNDRY_SPACE" + }, + new + { + Id = 9, + Label = "ADD_SPACE_MANAGER_ROLE" + }, + new + { + Id = 10, + Label = "ADD_SPACE_DEVELOPER_ROLE" + }, + new + { + Id = 11, + Label = "CREATE_DIM_SERVICE_INSTANCE" + }, + new + { + Id = 12, + Label = "CREATE_SERVICE_INSTANCE_BINDING" + }, + new + { + Id = 13, + Label = "GET_DIM_DETAILS" + }, + new + { + Id = 14, + Label = "CREATE_APPLICATION" + }, + new + { + Id = 15, + Label = "CREATE_COMPANY_IDENTITY" + }, + new + { + Id = 16, + Label = "ASSIGN_COMPANY_APPLICATION" + }, + new + { + Id = 17, + Label = "CREATE_STATUS_LIST" + }, + new + { + Id = 18, + Label = "SEND_CALLBACK" + }, + new + { + Id = 100, + Label = "CREATE_TECHNICAL_USER" + }, + new + { + Id = 101, + Label = "GET_TECHNICAL_USER_DATA" + }, + new + { + Id = 102, + Label = "SEND_TECHNICAL_USER_CREATION_CALLBACK" + }, + new + { + Id = 200, + Label = "DELETE_TECHNICAL_USER" + }, + new + { + Id = 201, + Label = "SEND_TECHNICAL_USER_DELETION_CALLBACK" + }); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessType", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_process_types"); + + b.ToTable("process_types", "dim"); + + b.HasData( + new + { + Id = 1, + Label = "SETUP_DIM" + }, + new + { + Id = 2, + Label = "TECHNICAL_USER" + }); + }); + + modelBuilder.Entity("Dim.Entities.Entities.TechnicalUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ClientId") + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .HasColumnType("bytea") + .HasColumnName("client_secret"); + + b.Property("EncryptionMode") + .HasColumnType("integer") + .HasColumnName("encryption_mode"); + + b.Property("ExternalId") + .HasColumnType("uuid") + .HasColumnName("external_id"); + + b.Property("InitializationVector") + .HasColumnType("bytea") + .HasColumnName("initialization_vector"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("TechnicalUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("technical_user_name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TokenAddress") + .HasColumnType("text") + .HasColumnName("token_address"); + + b.HasKey("Id") + .HasName("pk_technical_users"); + + b.HasIndex("ProcessId") + .HasDatabaseName("ix_technical_users_process_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_technical_users_tenant_id"); + + b.ToTable("technical_users", "dim"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("text") + .HasColumnName("application_id"); + + b.Property("ApplicationKey") + .HasColumnType("text") + .HasColumnName("application_key"); + + b.Property("Bpn") + .IsRequired() + .HasColumnType("text") + .HasColumnName("bpn"); + + b.Property("CompanyId") + .HasColumnType("uuid") + .HasColumnName("company_id"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("company_name"); + + b.Property("Did") + .HasColumnType("text") + .HasColumnName("did"); + + b.Property("DidDocumentLocation") + .IsRequired() + .HasColumnType("text") + .HasColumnName("did_document_location"); + + b.Property("DidDownloadUrl") + .HasColumnType("text") + .HasColumnName("did_download_url"); + + b.Property("DimInstanceId") + .HasColumnType("uuid") + .HasColumnName("dim_instance_id"); + + b.Property("IsIssuer") + .HasColumnType("boolean") + .HasColumnName("is_issuer"); + + b.Property("OperatorId") + .HasColumnType("uuid") + .HasColumnName("operator_id"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("ServiceBindingName") + .HasColumnType("text") + .HasColumnName("service_binding_name"); + + b.Property("ServiceInstanceId") + .HasColumnType("text") + .HasColumnName("service_instance_id"); + + b.Property("SpaceId") + .HasColumnType("uuid") + .HasColumnName("space_id"); + + b.Property("SubAccountId") + .HasColumnType("uuid") + .HasColumnName("sub_account_id"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("ProcessId") + .HasDatabaseName("ix_tenants_process_id"); + + b.ToTable("tenants", "dim"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Process", b => + { + b.HasOne("Dim.Entities.Entities.ProcessType", "ProcessType") + .WithMany("Processes") + .HasForeignKey("ProcessTypeId") + .IsRequired() + .HasConstraintName("fk_processes_process_types_process_type_id"); + + b.Navigation("ProcessType"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStep", b => + { + b.HasOne("Dim.Entities.Entities.Process", "Process") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessId") + .IsRequired() + .HasConstraintName("fk_process_steps_processes_process_id"); + + b.HasOne("Dim.Entities.Entities.ProcessStepStatus", "ProcessStepStatus") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessStepStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_process_steps_process_step_statuses_process_step_status_id"); + + b.HasOne("Dim.Entities.Entities.ProcessStepType", "ProcessStepType") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessStepTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_process_steps_process_step_types_process_step_type_id"); + + b.Navigation("Process"); + + b.Navigation("ProcessStepStatus"); + + b.Navigation("ProcessStepType"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.TechnicalUser", b => + { + b.HasOne("Dim.Entities.Entities.Process", "Process") + .WithMany("TechnicalUsers") + .HasForeignKey("ProcessId") + .IsRequired() + .HasConstraintName("fk_technical_users_processes_process_id"); + + b.HasOne("Dim.Entities.Entities.Tenant", "Tenant") + .WithMany("TechnicalUsers") + .HasForeignKey("TenantId") + .IsRequired() + .HasConstraintName("fk_technical_users_tenants_tenant_id"); + + b.Navigation("Process"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Tenant", b => + { + b.HasOne("Dim.Entities.Entities.Process", "Process") + .WithMany("Tenants") + .HasForeignKey("ProcessId") + .IsRequired() + .HasConstraintName("fk_tenants_processes_process_id"); + + b.Navigation("Process"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Process", b => + { + b.Navigation("ProcessSteps"); + + b.Navigation("TechnicalUsers"); + + b.Navigation("Tenants"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepStatus", b => + { + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepType", b => + { + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessType", b => + { + b.Navigation("Processes"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Tenant", b => + { + b.Navigation("TechnicalUsers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/database/Dim.Migrations/Migrations/20240717155124_1.2.0.cs b/src/database/Dim.Migrations/Migrations/20240717155124_1.2.0.cs new file mode 100644 index 0000000..c4ee8a1 --- /dev/null +++ b/src/database/Dim.Migrations/Migrations/20240717155124_1.2.0.cs @@ -0,0 +1,94 @@ +/******************************************************************************** + * Copyright (c) 2024 BMW Group AG + * Copyright 2024 SAP SE or an SAP affiliate company and ssi-dim-middle-layer contributors. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Dim.Migrations.Migrations +{ + /// + public partial class _120 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + schema: "dim", + table: "process_step_types", + keyColumn: "id", + keyValue: 102, + column: "label", + value: "SEND_TECHNICAL_USER_CREATION_CALLBACK"); + + migrationBuilder.InsertData( + schema: "dim", + table: "process_step_types", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 200, "DELETE_TECHNICAL_USER" }, + { 201, "SEND_TECHNICAL_USER_DELETION_CALLBACK" } + }); + + migrationBuilder.UpdateData( + schema: "dim", + table: "process_types", + keyColumn: "id", + keyValue: 2, + column: "label", + value: "TECHNICAL_USER"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + schema: "dim", + table: "process_step_types", + keyColumn: "id", + keyValue: 200); + + migrationBuilder.DeleteData( + schema: "dim", + table: "process_step_types", + keyColumn: "id", + keyValue: 201); + + migrationBuilder.UpdateData( + schema: "dim", + table: "process_step_types", + keyColumn: "id", + keyValue: 102, + column: "label", + value: "SEND_TECHNICAL_USER_CALLBACK"); + + migrationBuilder.UpdateData( + schema: "dim", + table: "process_types", + keyColumn: "id", + keyValue: 2, + column: "label", + value: "CREATE_TECHNICAL_USER"); + } + } +} diff --git a/src/database/Dim.Migrations/Migrations/DimDbContextModelSnapshot.cs b/src/database/Dim.Migrations/Migrations/DimDbContextModelSnapshot.cs index d803800..e3b42c2 100644 --- a/src/database/Dim.Migrations/Migrations/DimDbContextModelSnapshot.cs +++ b/src/database/Dim.Migrations/Migrations/DimDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -/******************************************************************************** +/******************************************************************************** * Copyright (c) 2024 BMW Group AG * Copyright 2024 SAP SE or an SAP affiliate company and ssi-dim-middle-layer contributors. * @@ -35,7 +35,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder .HasDefaultSchema("dim") .UseCollation("en_US.utf8") - .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("ProductVersion", "8.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -281,7 +281,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = 102, - Label = "SEND_TECHNICAL_USER_CALLBACK" + Label = "SEND_TECHNICAL_USER_CREATION_CALLBACK" + }, + new + { + Id = 200, + Label = "DELETE_TECHNICAL_USER" + }, + new + { + Id = 201, + Label = "SEND_TECHNICAL_USER_DELETION_CALLBACK" }); }); @@ -311,7 +321,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = 2, - Label = "CREATE_TECHNICAL_USER" + Label = "TECHNICAL_USER" }); }); diff --git a/src/database/Dim.Migrations/Program.cs b/src/database/Dim.Migrations/Program.cs index ccc4463..8b257e6 100644 --- a/src/database/Dim.Migrations/Program.cs +++ b/src/database/Dim.Migrations/Program.cs @@ -45,7 +45,7 @@ .AddLogging() .Build(); - await host.Services.InitializeDatabasesAsync(); // We don't actually run anything here. The magic happens in InitializeDatabasesAsync + await host.Services.InitializeDatabasesAsync().ConfigureAwait(ConfigureAwaitOptions.None); // We don't actually run anything here. The magic happens in InitializeDatabasesAsync } catch (Exception ex) when (!ex.GetType().Name.Equals("StopTheHostException", StringComparison.Ordinal)) { @@ -55,5 +55,5 @@ finally { Log.Information("Process Shutting down..."); - Log.CloseAndFlush(); + await Log.CloseAndFlushAsync().ConfigureAwait(false); } diff --git a/src/processes/DimProcess.Executor/TechnicalUserProcessTypeExecutor.cs b/src/processes/DimProcess.Executor/TechnicalUserProcessTypeExecutor.cs index 2f2b455..0919575 100644 --- a/src/processes/DimProcess.Executor/TechnicalUserProcessTypeExecutor.cs +++ b/src/processes/DimProcess.Executor/TechnicalUserProcessTypeExecutor.cs @@ -36,12 +36,14 @@ public class TechnicalUserProcessTypeExecutor( private readonly IEnumerable _executableProcessSteps = ImmutableArray.Create( ProcessStepTypeId.CREATE_TECHNICAL_USER, ProcessStepTypeId.GET_TECHNICAL_USER_DATA, - ProcessStepTypeId.SEND_TECHNICAL_USER_CALLBACK); + ProcessStepTypeId.SEND_TECHNICAL_USER_CREATION_CALLBACK, + ProcessStepTypeId.DELETE_TECHNICAL_USER, + ProcessStepTypeId.SEND_TECHNICAL_USER_DELETION_CALLBACK); private Guid _technicalUserId; private string? _tenantName; - public ProcessTypeId GetProcessTypeId() => ProcessTypeId.CREATE_TECHNICAL_USER; + public ProcessTypeId GetProcessTypeId() => ProcessTypeId.TECHNICAL_USER; public bool IsExecutableStepTypeId(ProcessStepTypeId processStepTypeId) => _executableProcessSteps.Contains(processStepTypeId); public IEnumerable GetExecutableStepTypeIds() => _executableProcessSteps; public ValueTask IsLockRequested(ProcessStepTypeId processStepTypeId) => new(false); @@ -79,7 +81,11 @@ public class TechnicalUserProcessTypeExecutor( .ConfigureAwait(false), ProcessStepTypeId.GET_TECHNICAL_USER_DATA => await technicalUserProcessHandler.GetTechnicalUserData(_tenantName, _technicalUserId, cancellationToken) .ConfigureAwait(false), - ProcessStepTypeId.SEND_TECHNICAL_USER_CALLBACK => await technicalUserProcessHandler.SendCallback(_technicalUserId, cancellationToken) + ProcessStepTypeId.SEND_TECHNICAL_USER_CREATION_CALLBACK => await technicalUserProcessHandler.SendCreateCallback(_technicalUserId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.DELETE_TECHNICAL_USER => await technicalUserProcessHandler.DeleteServiceInstanceBindings(_tenantName, _technicalUserId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.SEND_TECHNICAL_USER_DELETION_CALLBACK => await technicalUserProcessHandler.SendDeleteCallback(_technicalUserId, cancellationToken) .ConfigureAwait(false), _ => (null, ProcessStepStatusId.TODO, false, null) }; diff --git a/src/processes/DimProcess.Library/Callback/CallbackService.cs b/src/processes/DimProcess.Library/Callback/CallbackService.cs index 008eb8a..5fc8565 100644 --- a/src/processes/DimProcess.Library/Callback/CallbackService.cs +++ b/src/processes/DimProcess.Library/Callback/CallbackService.cs @@ -58,4 +58,11 @@ public async Task SendTechnicalUserCallback(Guid externalId, string tokenAddress clientSecret); await httpClient.PostAsJsonAsync($"/api/administration/serviceAccount/callback/{externalId}", data, JsonSerializerExtensions.Options, cancellationToken).ConfigureAwait(false); } + + public async Task SendTechnicalUserDeletionCallback(Guid externalId, CancellationToken cancellationToken) + { + var httpClient = await tokenService.GetAuthorizedClient(_settings, cancellationToken) + .ConfigureAwait(false); + await httpClient.PostAsync($"/api/administration/serviceAccount/callback/{externalId}/delete", null, cancellationToken).ConfigureAwait(false); + } } diff --git a/src/processes/DimProcess.Library/Callback/ICallbackService.cs b/src/processes/DimProcess.Library/Callback/ICallbackService.cs index f0a57d3..5f8806d 100644 --- a/src/processes/DimProcess.Library/Callback/ICallbackService.cs +++ b/src/processes/DimProcess.Library/Callback/ICallbackService.cs @@ -28,4 +28,6 @@ public interface ICallbackService Task SendCallback(string bpn, ServiceCredentialBindingDetailResponse dimDetails, JsonDocument didDocument, string did, CancellationToken cancellationToken); Task SendTechnicalUserCallback(Guid externalId, string tokenAddress, string clientId, string clientSecret, CancellationToken cancellationToken); + + Task SendTechnicalUserDeletionCallback(Guid externalId, CancellationToken cancellationToken); } diff --git a/src/processes/DimProcess.Library/DimProcess.Library.csproj b/src/processes/DimProcess.Library/DimProcess.Library.csproj index 63bae3f..d2e120c 100644 --- a/src/processes/DimProcess.Library/DimProcess.Library.csproj +++ b/src/processes/DimProcess.Library/DimProcess.Library.csproj @@ -32,8 +32,8 @@ - - + + diff --git a/src/processes/DimProcess.Library/ITechnicalUserProcessHandler.cs b/src/processes/DimProcess.Library/ITechnicalUserProcessHandler.cs index 3d8bc10..800c055 100644 --- a/src/processes/DimProcess.Library/ITechnicalUserProcessHandler.cs +++ b/src/processes/DimProcess.Library/ITechnicalUserProcessHandler.cs @@ -26,5 +26,7 @@ public interface ITechnicalUserProcessHandler { Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceInstanceBindings(string tenantName, Guid technicalUserId, CancellationToken cancellationToken); Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> GetTechnicalUserData(string tenantName, Guid technicalUserId, CancellationToken cancellationToken); - Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SendCallback(Guid technicalUserId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SendCreateCallback(Guid technicalUserId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> DeleteServiceInstanceBindings(string tenantName, Guid technicalUserId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SendDeleteCallback(Guid technicalUserId, CancellationToken cancellationToken); } diff --git a/src/processes/DimProcess.Library/TechnicalUserProcessHandler.cs b/src/processes/DimProcess.Library/TechnicalUserProcessHandler.cs index 6e8972d..18f414e 100644 --- a/src/processes/DimProcess.Library/TechnicalUserProcessHandler.cs +++ b/src/processes/DimProcess.Library/TechnicalUserProcessHandler.cs @@ -26,6 +26,7 @@ using DimProcess.Library.DependencyInjection; using Microsoft.Extensions.Options; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration; using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Encryption; namespace DimProcess.Library; @@ -66,8 +67,8 @@ public class TechnicalUserProcessHandler( var dimInstanceId = await cfClient.GetServiceBinding(tenantName, spaceId.Value, $"{technicalUserName}-dim-key01", cancellationToken).ConfigureAwait(false); var dimDetails = await cfClient.GetServiceBindingDetails(dimInstanceId, cancellationToken).ConfigureAwait(false); - var cryptoConfig = _settings.EncryptionConfigs.SingleOrDefault(x => x.Index == _settings.EncryptionConfigIndex) ?? throw new ConfigurationException($"encryptionConfigIndex {_settings.EncryptionConfigIndex} is not configured"); - var (secret, initializationVector) = CryptoHelper.Encrypt(dimDetails.Credentials.Uaa.ClientSecret, Convert.FromHexString(cryptoConfig.EncryptionKey), cryptoConfig.CipherMode, cryptoConfig.PaddingMode); + var cryptoHelper = _settings.EncryptionConfigs.GetCryptoHelper(_settings.EncryptionConfigIndex); + var (secret, initializationVector) = cryptoHelper.Encrypt(dimDetails.Credentials.Uaa.ClientSecret); dimRepositories.GetInstance().AttachAndModifyTechnicalUser(technicalUserId, technicalUser => { @@ -86,15 +87,15 @@ public class TechnicalUserProcessHandler( technicalUser.EncryptionMode = _settings.EncryptionConfigIndex; }); return new ValueTuple?, ProcessStepStatusId, bool, string?>( - Enumerable.Repeat(ProcessStepTypeId.SEND_TECHNICAL_USER_CALLBACK, 1), + Enumerable.Repeat(ProcessStepTypeId.SEND_TECHNICAL_USER_CREATION_CALLBACK, 1), ProcessStepStatusId.DONE, false, null); } - public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SendCallback(Guid technicalUserId, CancellationToken cancellationToken) + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SendCreateCallback(Guid technicalUserId, CancellationToken cancellationToken) { - var (externalId, tokenAddress, clientId, clientSecret, initializationVector, encryptionMode) = await dimRepositories.GetInstance().GetTechnicalUserCallbackData(technicalUserId).ConfigureAwait(false); + var (externalId, tokenAddress, clientId, clientSecret, initializationVector, _) = await dimRepositories.GetInstance().GetTechnicalUserCallbackData(technicalUserId).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(clientId)) { @@ -106,7 +107,13 @@ public class TechnicalUserProcessHandler( throw new ConflictException("TokenAddress must not be null"); } - var secret = Decrypt(clientSecret, initializationVector, encryptionMode); + if (clientSecret == null) + { + throw new ConflictException("Secret must not be null"); + } + + var cryptoHelper = _settings.EncryptionConfigs.GetCryptoHelper(_settings.EncryptionConfigIndex); + var secret = cryptoHelper.Decrypt(clientSecret, initializationVector); await callbackService.SendTechnicalUserCallback(externalId, tokenAddress, clientId, secret, cancellationToken).ConfigureAwait(false); @@ -117,20 +124,36 @@ public class TechnicalUserProcessHandler( null); } - private string Decrypt(byte[]? clientSecret, byte[]? initializationVector, int? encryptionMode) + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> DeleteServiceInstanceBindings(string tenantName, Guid technicalUserId, CancellationToken cancellationToken) { - if (clientSecret == null) + var (spaceId, technicalUserName) = await dimRepositories.GetInstance().GetSpaceIdAndTechnicalUserName(technicalUserId).ConfigureAwait(false); + if (spaceId == null) { - throw new ConflictException("ClientSecret must not be null"); + throw new ConflictException("SpaceId must not be null."); } - if (encryptionMode == null) - { - throw new ConflictException("EncryptionMode must not be null"); - } + var serviceBindingId = await cfClient.GetServiceBinding(tenantName, spaceId.Value, $"{technicalUserName}-dim-key01", cancellationToken).ConfigureAwait(false); + await cfClient.DeleteServiceInstanceBindings(serviceBindingId, cancellationToken).ConfigureAwait(false); - var cryptoConfig = _settings.EncryptionConfigs.SingleOrDefault(x => x.Index == encryptionMode) ?? throw new ConfigurationException($"EncryptionModeIndex {encryptionMode} is not configured"); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.SEND_TECHNICAL_USER_DELETION_CALLBACK, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SendDeleteCallback(Guid technicalUserId, CancellationToken cancellationToken) + { + var tenantRepository = dimRepositories.GetInstance(); + + var externalId = await tenantRepository.GetExternalIdForTechnicalUser(technicalUserId).ConfigureAwait(false); + tenantRepository.RemoveTechnicalUser(technicalUserId); + await callbackService.SendTechnicalUserDeletionCallback(externalId, cancellationToken).ConfigureAwait(false); - return CryptoHelper.Decrypt(clientSecret, initializationVector, Convert.FromHexString(cryptoConfig.EncryptionKey), cryptoConfig.CipherMode, cryptoConfig.PaddingMode); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + null, + ProcessStepStatusId.DONE, + false, + null); } } diff --git a/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj b/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj index b1593ee..3721642 100644 --- a/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj +++ b/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj @@ -32,9 +32,9 @@ - - - + + + diff --git a/src/processes/Processes.Worker/Processes.Worker.csproj b/src/processes/Processes.Worker/Processes.Worker.csproj index 8160173..db761d4 100644 --- a/src/processes/Processes.Worker/Processes.Worker.csproj +++ b/src/processes/Processes.Worker/Processes.Worker.csproj @@ -41,7 +41,7 @@ - + diff --git a/src/processes/Processes.Worker/Program.cs b/src/processes/Processes.Worker/Program.cs index 9e846ea..1d26e66 100644 --- a/src/processes/Processes.Worker/Program.cs +++ b/src/processes/Processes.Worker/Program.cs @@ -66,5 +66,5 @@ finally { Log.Information("Server Shutting down"); - Log.CloseAndFlush(); + await Log.CloseAndFlushAsync().ConfigureAwait(false); } diff --git a/src/web/Dim.Web/BusinessLogic/DimBusinessLogic.cs b/src/web/Dim.Web/BusinessLogic/DimBusinessLogic.cs index 376526f..7496cdc 100644 --- a/src/web/Dim.Web/BusinessLogic/DimBusinessLogic.cs +++ b/src/web/Dim.Web/BusinessLogic/DimBusinessLogic.cs @@ -109,7 +109,7 @@ public async Task CreateStatusList(string bpn, CancellationToken cancell return await dimClient.CreateStatusList(dimAuth, dimBaseUrl, companyId.Value, cancellationToken).ConfigureAwait(false); } - public async Task CreateTechnicalUser(string bpn, TechnicalUserData technicalUserData, CancellationToken cancellationToken) + public async Task CreateTechnicalUser(string bpn, TechnicalUserData technicalUserData) { var (exists, tenantId) = await dimRepositories.GetInstance().GetTenantForBpn(bpn).ConfigureAwait(false); @@ -119,11 +119,31 @@ public async Task CreateTechnicalUser(string bpn, TechnicalUserData technicalUse } var processStepRepository = dimRepositories.GetInstance(); - var processId = processStepRepository.CreateProcess(ProcessTypeId.CREATE_TECHNICAL_USER).Id; + var processId = processStepRepository.CreateProcess(ProcessTypeId.TECHNICAL_USER).Id; processStepRepository.CreateProcessStep(ProcessStepTypeId.CREATE_TECHNICAL_USER, ProcessStepStatusId.TODO, processId); dimRepositories.GetInstance().CreateTenantTechnicalUser(tenantId, technicalUserData.Name, technicalUserData.ExternalId, processId); await dimRepositories.SaveAsync().ConfigureAwait(false); } + + public async Task DeleteTechnicalUser(string bpn, TechnicalUserData technicalUserData) + { + var (exists, technicalUserId, processId) = await dimRepositories.GetInstance().GetTechnicalUserForBpn(bpn, technicalUserData.Name).ConfigureAwait(false); + if (!exists) + { + throw NotFoundException.Create(DimErrors.NO_TECHNICAL_USER_FOUND, new ErrorParameter[] { new("bpn", bpn) }); + } + + var processStepRepository = dimRepositories.GetInstance(); + processStepRepository.CreateProcessStep(ProcessStepTypeId.DELETE_TECHNICAL_USER, ProcessStepStatusId.TODO, processId); + + dimRepositories.GetInstance().AttachAndModifyTechnicalUser(technicalUserId, null, t => + { + t.ExternalId = technicalUserData.ExternalId; + t.ProcessId = processId; + }); + + await dimRepositories.SaveAsync().ConfigureAwait(false); + } } diff --git a/src/web/Dim.Web/BusinessLogic/IDimBusinessLogic.cs b/src/web/Dim.Web/BusinessLogic/IDimBusinessLogic.cs index 9263959..603bb0e 100644 --- a/src/web/Dim.Web/BusinessLogic/IDimBusinessLogic.cs +++ b/src/web/Dim.Web/BusinessLogic/IDimBusinessLogic.cs @@ -28,5 +28,6 @@ public interface IDimBusinessLogic : ITransient Task StartSetupDim(string companyName, string bpn, string didDocumentLocation, bool isIssuer); Task GetStatusList(string bpn, CancellationToken cancellationToken); Task CreateStatusList(string bpn, CancellationToken cancellationToken); - Task CreateTechnicalUser(string bpn, TechnicalUserData technicalUserData, CancellationToken cancellationToken); + Task CreateTechnicalUser(string bpn, TechnicalUserData technicalUserData); + Task DeleteTechnicalUser(string bpn, TechnicalUserData technicalUserData); } diff --git a/src/web/Dim.Web/Controllers/DimController.cs b/src/web/Dim.Web/Controllers/DimController.cs index c4d6cba..0696e81 100644 --- a/src/web/Dim.Web/Controllers/DimController.cs +++ b/src/web/Dim.Web/Controllers/DimController.cs @@ -66,11 +66,18 @@ public static RouteGroupBuilder MapDimApi(this RouteGroupBuilder group) .RequireAuthorization(r => r.RequireRole("create_status_list")) .Produces(StatusCodes.Status200OK, responseType: typeof(string), contentType: Constants.JsonContentType); - policyHub.MapPost("technical-user/{bpn}", ([FromRoute] string bpn, [FromBody] TechnicalUserData technicalUserData, CancellationToken cancellationToken, [FromServices] IDimBusinessLogic dimBusinessLogic) => dimBusinessLogic.CreateTechnicalUser(bpn, technicalUserData, cancellationToken)) + policyHub.MapPost("technical-user/{bpn}", ([FromRoute] string bpn, [FromBody] TechnicalUserData technicalUserData, [FromServices] IDimBusinessLogic dimBusinessLogic) => dimBusinessLogic.CreateTechnicalUser(bpn, technicalUserData)) .WithSwaggerDescription("Creates a technical user for the dim of the given bpn", "Example: Post: api/dim/technical-user/{bpn}", "bpn of the company") - // .RequireAuthorization(r => r.RequireRole("create_technical_user")) + .RequireAuthorization(r => r.RequireRole("create_technical_user")) + .Produces(StatusCodes.Status200OK, contentType: Constants.JsonContentType); + + policyHub.MapPost("technical-user/{bpn}/delete", ([FromRoute] string bpn, [FromBody] TechnicalUserData technicalUserData, [FromServices] IDimBusinessLogic dimBusinessLogic) => dimBusinessLogic.DeleteTechnicalUser(bpn, technicalUserData)) + .WithSwaggerDescription("Deletes a technical user with the given name of the given bpn", + "Example: Post: api/dim/technical-user/{bpn}/delete", + "bpn of the company") + .RequireAuthorization(r => r.RequireRole("delete_technical_user")) .Produces(StatusCodes.Status200OK, contentType: Constants.JsonContentType); return group; diff --git a/src/web/Dim.Web/Dim.Web.csproj b/src/web/Dim.Web/Dim.Web.csproj index ce3759f..a6d3eeb 100644 --- a/src/web/Dim.Web/Dim.Web.csproj +++ b/src/web/Dim.Web/Dim.Web.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/web/Dim.Web/ErrorHandling/DimErrorMessageContainer.cs b/src/web/Dim.Web/ErrorHandling/DimErrorMessageContainer.cs index d944a73..3843828 100644 --- a/src/web/Dim.Web/ErrorHandling/DimErrorMessageContainer.cs +++ b/src/web/Dim.Web/ErrorHandling/DimErrorMessageContainer.cs @@ -31,6 +31,7 @@ public class DimErrorMessageContainer : IErrorMessageContainer { DimErrors.NO_COMPANY_FOR_BPN, "No Tenant found for Bpn {bpn}" }, { DimErrors.NO_COMPANY_ID_SET, "No Company Id set" }, { DimErrors.NO_INSTANCE_ID_SET, "No Instnace Id set" }, + { DimErrors.NO_TECHNICAL_USER_FOUND, "No Technical User found" }, }.ToImmutableDictionary(x => (int)x.Key, x => x.Value); public Type Type { get => typeof(DimErrors); } @@ -41,5 +42,6 @@ public enum DimErrors { NO_COMPANY_FOR_BPN, NO_COMPANY_ID_SET, - NO_INSTANCE_ID_SET + NO_INSTANCE_ID_SET, + NO_TECHNICAL_USER_FOUND } diff --git a/src/web/Dim.Web/Program.cs b/src/web/Dim.Web/Program.cs index ea55d10..6b5e0be 100644 --- a/src/web/Dim.Web/Program.cs +++ b/src/web/Dim.Web/Program.cs @@ -31,8 +31,8 @@ const string Version = "v1"; -WebApplicationBuildRunner - .BuildAndRunWebApplication(args, "dim", Version, "dim", +await WebApplicationBuildRunner + .BuildAndRunWebApplicationAsync(args, "dim", Version, "dim", builder => { builder.Services @@ -57,4 +57,4 @@ app.MapGroup("/api") .WithOpenApi() .MapDimApi(); - }); + }).ConfigureAwait(ConfigureAwaitOptions.None); diff --git a/tests/processes/DimProcess.Library.Tests/TechnicalUserProcessHandlerTests.cs b/tests/processes/DimProcess.Library.Tests/TechnicalUserProcessHandlerTests.cs index 2e5fd88..88cb19e 100644 --- a/tests/processes/DimProcess.Library.Tests/TechnicalUserProcessHandlerTests.cs +++ b/tests/processes/DimProcess.Library.Tests/TechnicalUserProcessHandlerTests.cs @@ -1,3 +1,23 @@ +/******************************************************************************** + * Copyright (c) 2024 BMW Group AG + * Copyright 2024 SAP SE or an SAP affiliate company and ssi-dim-middle-layer contributors. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + using Dim.Clients.Api.Cf; using Dim.DbAccess; using Dim.DbAccess.Repositories; @@ -6,6 +26,7 @@ using DimProcess.Library.Callback; using DimProcess.Library.DependencyInjection; using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration; using System.Security.Cryptography; @@ -13,29 +34,26 @@ namespace DimProcess.Library.Tests; public class TechnicalUserProcessHandlerTests { - private readonly ICallbackService _callbackService; - private readonly IFixture _fixture; - private readonly IDimRepositories _repositories; private readonly ITenantRepository _tenantRepositories; - private readonly IOptions _options; private readonly ICfClient _cfClient; + private readonly ICallbackService _callbackService; private readonly TechnicalUserProcessHandler _sut; public TechnicalUserProcessHandlerTests() { - _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); - _fixture.Behaviors.OfType().ToList() - .ForEach(b => _fixture.Behaviors.Remove(b)); - _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); - _repositories = A.Fake(); + var repositories = A.Fake(); _tenantRepositories = A.Fake(); - A.CallTo(() => _repositories.GetInstance()).Returns(_tenantRepositories); + A.CallTo(() => repositories.GetInstance()).Returns(_tenantRepositories); _cfClient = A.Fake(); _callbackService = A.Fake(); - _options = Options.Create(new TechnicalUserSettings + var options = Options.Create(new TechnicalUserSettings { EncryptionConfigIndex = 0, EncryptionConfigs = new[] @@ -50,7 +68,7 @@ public TechnicalUserProcessHandlerTests() } }); - _sut = new TechnicalUserProcessHandler(_repositories, _cfClient, _callbackService, _options); + _sut = new TechnicalUserProcessHandler(repositories, _cfClient, _callbackService, options); } #region CreateSubaccount @@ -82,11 +100,96 @@ public async Task CreateSubaccount_WithValidData_ReturnsExpected() result.modified.Should().BeFalse(); result.processMessage.Should().BeNull(); result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); - result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.SEND_TECHNICAL_USER_CALLBACK); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.SEND_TECHNICAL_USER_CREATION_CALLBACK); technicalUser.EncryptionMode.Should().NotBeNull().And.Be(0); technicalUser.ClientId.Should().Be("cl1"); } #endregion + #region DeleteServiceInstanceBindings + + [Fact] + public async Task DeleteServiceInstanceBindings_WithValidData_ReturnsExpected() + { + // Arrange + var technicalUserId = Guid.NewGuid(); + var serviceBindingId = Guid.NewGuid(); + var spaceId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSpaceIdAndTechnicalUserName(technicalUserId)) + .Returns(new ValueTuple(spaceId, "test")); + A.CallTo(() => _cfClient.GetServiceBinding("test", spaceId, A._, A._)) + .Returns(serviceBindingId); + + // Act + var result = await _sut.DeleteServiceInstanceBindings("test", technicalUserId, CancellationToken.None); + + // Assert + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.SEND_TECHNICAL_USER_DELETION_CALLBACK); + A.CallTo(() => _cfClient.DeleteServiceInstanceBindings(A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _cfClient.DeleteServiceInstanceBindings(serviceBindingId, A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task DeleteServiceInstanceBindings_WithoutSpaceId_ThrowsConflictException() + { + // Arrange + var technicalUserId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSpaceIdAndTechnicalUserName(technicalUserId)) + .Returns(new ValueTuple(null, "test")); + async Task Act() => await _sut.DeleteServiceInstanceBindings("test", technicalUserId, CancellationToken.None); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SpaceId must not be null."); + A.CallTo(() => _cfClient.DeleteServiceInstanceBindings(A._, A._)) + .MustNotHaveHappened(); + } + + #endregion + + #region SendCallback + + [Fact] + public async Task SendCallback_WithValidData_ReturnsExpected() + { + // Arrange + var technicalUserId = Guid.NewGuid(); + var externalId = Guid.NewGuid(); + var technicalUsers = new List + { + new(technicalUserId, Guid.NewGuid(), Guid.NewGuid(), "sa-t", Guid.NewGuid()) + }; + A.CallTo(() => _tenantRepositories.GetExternalIdForTechnicalUser(technicalUserId)) + .Returns(externalId); + A.CallTo(() => _tenantRepositories.RemoveTechnicalUser(A._)) + .Invokes((Guid tuId) => + { + var user = technicalUsers.Single(x => x.Id == tuId); + technicalUsers.Remove(user); + }); + + // Act + var result = await _sut.SendDeleteCallback(technicalUserId, CancellationToken.None); + + // Assert + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().BeNull(); + technicalUsers.Should().BeEmpty(); + A.CallTo(() => _callbackService.SendTechnicalUserDeletionCallback(A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _callbackService.SendTechnicalUserDeletionCallback(externalId, A._)) + .MustHaveHappenedOnceExactly(); + } + + #endregion }