diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5945d4a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,293 @@ +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8 +end_of_line = lf + +# XML project files +[*.{csproj,proj,projitems,shproj}] +indent_size = 2 +end_of_line = crlf + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Powershell files +[*.ps1] +indent_size = 2 + +# Shell script files +[*.sh] +end_of_line = lf +indent_size = 2 + +# Dotnet code style settings: +[*.{cs}] + +# IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity = warning + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = false +dotnet_separate_import_directive_groups = false +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_property = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_event = false + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Whitespace options +dotnet_style_allow_multiple_blank_lines_experimental = false + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Constants are PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static fields are camelCase and start with s_ +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_field_style.capitalization = pascal_case + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}' +dotnet_diagnostic.RS2008.severity = none + +# IDE0073: File header +dotnet_diagnostic.IDE0073.severity = warning +#file_header_template = /********************************************************************************\n * Copyright (c) 2021,2022 Contributors to the CatenaX (ng) GitHub Organisation.\n *\n * \nSee the NOTICE file(s) distributed with this work for additional\n * information regarding copyright ownership.\n *\n * This program and the accompanying materials are made available under the\n * terms of the \nApache License, Version 2.0 which is available at\n * https://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License \nis distributed on an "AS IS" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the \nLicense. *\n * SPDX-License-Identifier: Apache-2.0\n ********************************************************************************/\n + +# IDE0035: Remove unreachable code +dotnet_diagnostic.IDE0035.severity = warning + +# IDE0036: Order modifiers +dotnet_diagnostic.IDE0036.severity = warning + +# IDE0043: Format string contains invalid placeholder +dotnet_diagnostic.IDE0043.severity = warning + +# IDE0044: Make field readonly +dotnet_diagnostic.IDE0044.severity = warning + +# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings? +# IDE0051: Remove unused private member +dotnet_diagnostic.IDE0051.severity = warning + +# IDE0170: Prefer extended property pattern +dotnet_diagnostic.IDE0170.severity = warning + +# RS0016: Only enable if API files are present +dotnet_public_api_analyzer.require_api_files = true + +# dotnet_style_allow_multiple_blank_lines_experimental +dotnet_diagnostic.IDE2000.severity = warning + +# CSharp code style settings: +[*.cs] +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Whitespace options +csharp_style_allow_embedded_statements_on_same_line_experimental = false +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = true:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Blocks are allowed +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = warning + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline:warning +# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 +dotnet_diagnostic.IDE0011.severity = warning + +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.IDE0040.severity = warning + +# IDE0052: Remove unread private member +dotnet_diagnostic.IDE0052.severity = warning + +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.IDE0059.severity = warning + +# CA1012: Abstract types should not have public constructors +dotnet_diagnostic.CA1012.severity = warning + +# CA1822: Make member static +dotnet_diagnostic.CA1822.severity = warning + +# Prefer "var" everywhere +dotnet_diagnostic.IDE0007.severity = warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:warning + +# csharp_style_allow_embedded_statements_on_same_line_experimental +dotnet_diagnostic.IDE2001.severity = warning + +# csharp_style_allow_blank_lines_between_consecutive_braces_experimental +dotnet_diagnostic.IDE2002.severity = warning + +# dotnet_style_allow_statement_immediately_after_block_experimental +dotnet_diagnostic.IDE2003.severity = warning + +# csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental +dotnet_diagnostic.IDE2004.severity = warning \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..890737b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,58 @@ +############################################################### +# 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 +############################################################### + +--- +version: 2 +updates: + # NuGet + - + package-ecosystem: "nuget" + directory: / + labels: + - "dependabot" + - "dependencies" + schedule: + interval: "weekly" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + + # Github Actions + - + package-ecosystem: "github-actions" + directory: / + labels: + - "dependabot" + - "github-actions" + schedule: + interval: "weekly" + + # Docker + - + package-ecosystem: "docker" + directory: ./docker/ + labels: + - "dependabot" + - "docker" + schedule: + interval: "weekly" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..459f9b7 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ +## Description + +Please include a summary of the change. + +## Why + +Please include an explanation of why this change is necessary as well as relevant motivation and context. List any dependencies that are required for this change. + +## Issue + +Link to Github issue. + +## Checklist + +Please delete options that are not relevant. + +- [ ] I have performed a self-review of my own code +- [ ] I have successfully tested my changes locally +- [ ] I have added tests that prove my changes work +- [ ] I have checked that new and existing tests pass locally with my changes +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have added copyright and license headers, footers (for .md files) or files (for images) diff --git a/.github/workflows/chart-test.yml b/.github/workflows/chart-test.yml new file mode 100644 index 0000000..e0b422d --- /dev/null +++ b/.github/workflows/chart-test.yml @@ -0,0 +1,129 @@ +############################################################### +# 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 +############################################################### + +name: Lint and Test Chart + +on: + push: + paths: + - 'charts/dim/**' + branches: [main] + pull_request: + paths: + - 'charts/dim/**' + workflow_dispatch: + inputs: + node_image: + description: 'kindest/node image for k8s kind cluster' + # k8s version + default: 'kindest/node:v1.27.3' + required: false + type: string + upgrade_from: + description: 'dim chart version to upgrade from' + # tbd + default: 'tbd' + required: false + type: string + +jobs: + + lint-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + + - name: Kubernetes KinD Cluster + uses: container-tools/kind-action@0ad70e2299366b0e1552c7240f4e4567148f723e # v2.0.4 + with: + # upgrade version, default (v0.17.0) uses node image v1.21.1 and doesn't work with more recent node image versions + version: v0.20.0 + # default value for event_name != workflow_dispatch + node_image: ${{ github.event.inputs.node_image || 'kindest/node:v1.27.3' }} + + - name: Build migration image + id: build-migration-image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ./docker/Dockerfile-dim-migrations + push: true + tags: kind-registry:5000/dim-migrations:testing + + - name: Build service image + id: build-service-image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ./docker/Dockerfile-dim-service + push: true + tags: kind-registry:5000/dim-service:testing + + - name: Build processes worker + id: build-processes-worker-image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ./docker/Dockerfile-dim-processes-worker + push: true + tags: kind-registry:5000/dim-processes-worker:testing + + - name: Set up Helm + uses: azure/setup-helm@b7246b12e77f7134dc2d460a3d5bad15bbe29390 # v4 + with: + version: v3.9.3 + + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: '3.9' + check-latest: true + + - name: Set up chart-testing + uses: helm/chart-testing-action@e6669bcd63d7cb57cb4380c33043eebe5d111992 # v2.6.1 + + - name: Run chart-testing (list-changed) + id: list-changed + run: | + changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }}) + if [[ -n "$changed" ]]; then + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Run chart-testing (lint) + run: ct lint --validate-maintainers=false --check-version-increment=false --target-branch ${{ github.event.repository.default_branch }} + + - name: Run chart-testing (install) + run: ct install --charts charts/dim --config charts/chart-testing-config.yaml --helm-extra-set-args "--set dim.image.name=kind-registry:5000/dim-service --set dim.image.tag=testing --set migrations.image.name=kind-registry:5000/dim-migrations --set migrations.image.tag=testing --set processesworker.image.name=kind-registry:5000/dim-processes-worker --set processesworker.image.tag=testing" + if: github.event_name != 'pull_request' || steps.list-changed.outputs.changed == 'true' + + # TODO: re-add the step after the first version release + # Upgrade the released chart version with the locally available chart + # default value for event_name != workflow_dispatch + # - name: Run helm upgrade + # run: | + # helm repo add bitnami https://charts.bitnami.com/bitnami + # helm repo add tractusx-dev https://eclipse-tractusx.github.io/charts/dev + # helm install dim tractusx-dev/dim --version ${{ github.event.inputs.upgrade_from || 'tbd' }} --namespace upgrade --create-namespace + # helm dependency update charts/dim + # helm upgrade dim charts/dim --set dim.image.name=kind-registry:5000/dim-service --set dim.image.tag=testing --set migrations.image.name=kind-registry:5000/dim-migrations --set migrations.image.tag=testing --set processesworker.image.name=kind-registry:5000/dim-processes-worker --set processesworker.image.tag=testing --namespace upgrade + # if: github.event_name != 'pull_request' || steps.list-changed.outputs.changed == 'true' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..62250e8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,106 @@ +############################################################### +# 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 +############################################################### + +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# + +name: "CodeQL" + +on: + push: + branches: [main] + paths: + - 'src/**' + pull_request: + paths: + - 'src/**' + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["csharp"] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v2.227 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + queries: +security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # Automates dependency installation for Python, Ruby, and JavaScript, optimizing the CodeQL analysis setup. + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@1b1aada464948af03b950897e5eb522f92603cc2 # v2.227 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v2.227 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/kics.yml b/.github/workflows/kics.yml new file mode 100644 index 0000000..09540f4 --- /dev/null +++ b/.github/workflows/kics.yml @@ -0,0 +1,76 @@ +############################################################### +# 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 +############################################################### + +name: "KICS" + +on: + push: + branches: [main] + # pull_request: + # The branches below must be a subset of the branches above + # branches: [main, dev] + # paths-ignore: + # - "**/*.md" + # - "**/*.txt" + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: KICS scan + uses: checkmarx/kics-github-action@8a44970e3d2eca668be41abe9d4e06709c3b3609 # v1.7.0 + with: + # Scanning directory . + path: "." + # Fail on HIGH severity results + fail_on: high + # Disable secrets detection - we use GitGuardian + disable_secrets: true + # when provided with a directory on output_path + # it will generate the specified reports file named 'results.{extension}' + # in this example it will generate: + # - results-dir/results.json + # - results-dir/results.sarif + output_path: kicsResults/ + output_formats: "json,sarif" + # If you want KICS to ignore the results and return exit status code 0 unless a KICS engine error happens + # ignore_on_exit: results + # GITHUB_TOKEN enables this github action to access github API and post comments in a pull request + # token: ${{ secrets.GITHUB_TOKEN }} + # enable_comments: true + + # Upload findings to GitHub Advanced Security Dashboard + - name: Upload SARIF file for GitHub Advanced Security Dashboard + if: always() + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + sarif_file: kicsResults/results.sarif + diff --git a/.github/workflows/lint-pull-request.yml b/.github/workflows/lint-pull-request.yml new file mode 100644 index 0000000..ad3f841 --- /dev/null +++ b/.github/workflows/lint-pull-request.yml @@ -0,0 +1,62 @@ +# ############################################################################# +# 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 +# ############################################################################# + +name: "Lint PullRequest" + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@e9fabac35e210fea40ca5b14c0da95a099eff26f # v5.4.0 + id: lint_pr_title + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # v2.9.0 + # When the previous steps fail, the workflow would stop. By adding this + # condition you can continue the execution with the populated error message. + if: always() && (steps.lint_pr_title.outputs.error_message != null) + with: + header: pr-title-lint-error + message: | + Hey there and thank you for opening this pull request! 👋đŸŧ + + We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. + + Details: + + ``` + ${{ steps.lint_pr_title.outputs.error_message }} + ``` + + # Delete a previous comment when the issue has been resolved + - if: ${{ steps.lint_pr_title.outputs.error_message == null }} + uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # v2.9.0 + with: + header: pr-title-lint-error + delete: true diff --git a/.github/workflows/migrations-docker.yml b/.github/workflows/migrations-docker.yml new file mode 100644 index 0000000..6ebe686 --- /dev/null +++ b/.github/workflows/migrations-docker.yml @@ -0,0 +1,82 @@ +############################################################### +# 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 +############################################################### + +name: Build Migrations Image + +on: + push: + paths: + # service and transitive paths + - 'src/database/Dim.Migrations/**' + - 'src/database/Dim.Entities/**' + # workflow file + - '.github/workflows/migrations-docker.yml' + # dockerfile + - 'docker/Dockerfile-dim-migrations' + + branches: + - 'main' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}_dim-migrations + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # v3.2.0 + + - name: Docker meta + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=main + type=raw,value=${{ github.sha }} + + - name: Build and push Docker image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ./docker/Dockerfile-dim-migrations + platforms: linux/amd64, linux/arm64 + pull: true + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/owasp-zap.yml b/.github/workflows/owasp-zap.yml new file mode 100644 index 0000000..625b956 --- /dev/null +++ b/.github/workflows/owasp-zap.yml @@ -0,0 +1,143 @@ +############################################################### +# 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 +############################################################### + +name: "OWASP ZAP (DAST Scan)" + +on: + push: + branches: [main] + paths: + - 'src/**' + pull_request: + paths: + - 'src/**' + schedule: + # Once a day + - cron: "0 0 * * *" + workflow_dispatch: + # Trigger manually + inputs: + node_image: + description: 'kindest/node image for k8s kind cluster' + # k8s version + default: 'kindest/node:v1.27.3' + required: false + type: string + +jobs: + owasp-zap-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + + - name: Kubernetes KinD Cluster + uses: container-tools/kind-action@0ad70e2299366b0e1552c7240f4e4567148f723e # v2.0.4 + with: + node_image: ${{ github.event.inputs.node_image || 'kindest/node:v1.27.3' }} + version: v0.20.0 + + - name: Set up Helm + uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 + with: + version: v3.5.0 + + - name: Build migration image + id: build-migration-image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ./docker/Dockerfile-dim-migrations + push: true + tags: kind-registry:5000/dim-migrations:testing + + - name: Build service image + id: build-service-image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ./docker/Dockerfile-dim-service + push: true + tags: kind-registry:5000/dim-service:testing + + - name: Build Worker image + id: build-worker-image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ./docker/Dockerfile-dim-processes-worker + push: true + tags: kind-registry:5000/dim-processes-worker:testing + + - name: Add bitnami repo + run: | + helm repo add bitnami https://charts.bitnami.com/bitnami + helm repo update + + - name: Update Helm dependencies + run: | + cd charts/dim + helm dependency build + + - name: Install the chart on KinD cluster + run: helm install testing -n apps --create-namespace --wait --set dim.image.name=kind-registry:5000/dim-service --set dim.image.tag=testing --set migrations.image.name=kind-registry:5000/dim-migrations --set migrations.image.tag=testing --set processesworker.image.name=kind-registry:5000/dim-processes-worker --set processesworker.image.tag=testing --set dim.swaggerEnabled=true charts/dim + + - name: Configure port forward to app in KinD + run: | + echo "Getting Agent IP..." + IP_ADDR=$(hostname -i) + echo "-> IP: $IP_ADDR" + echo "IP_ADDR=$IP_ADDR" >> $GITHUB_ENV + + POD_NAME=$(kubectl get pods --namespace apps -l "app.kubernetes.io/name=dim,app.kubernetes.io/instance=testing" -o jsonpath="{.items[0].metadata.name}") + CONTAINER_PORT=$(kubectl get pod --namespace apps $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + + echo "Port-forwarding 0.0.0.0:8080 to $POD_NAME:$CONTAINER_PORT..." + + kubectl --namespace apps port-forward $POD_NAME 8080:$CONTAINER_PORT --address 0.0.0.0 & + + - name: Generating report skeletons + if: success() || failure() + run: | + touch report_md.md report_html.html + chmod a+w report_md.md report_html.html + ls -lrt + + - name: Run ZAP scan + run: | + set +e + + echo "Pulling ZAP image..." + docker pull ghcr.io/zaproxy/zaproxy:stable -q + + echo "Starting ZAP Docker container..." + docker run -v ${GITHUB_WORKSPACE}:/zap/wrk/:rw ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py -t http://$IP_ADDR:8080/api/swagger/v1/swagger.json -f openapi -w report_md.md -r report_html.html -T 1 + + echo "... done." + + - name: Upload HTML report + if: success() || failure() + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: ZAP scan report + path: ./report_html.html + retention-days: 1 diff --git a/.github/workflows/processes-worker-docker.yml b/.github/workflows/processes-worker-docker.yml new file mode 100644 index 0000000..100d4f3 --- /dev/null +++ b/.github/workflows/processes-worker-docker.yml @@ -0,0 +1,81 @@ +############################################################### +# 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 +############################################################### + +name: Build Processes Worker Image + +on: + push: + paths: + # service and transitive paths + - 'src/**' + # workflow file + - '.github/workflows/processes-worker-docker.yml' + # dockerfile + - 'docker/Dockerfile-dim-processes-worker' + + branches: + - 'main' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}_dim-processes-worker + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # v3.2.0 + + - name: Docker meta + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ env.REGISTRY}}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=main + type=raw,value=${{ github.sha }} + + - name: Build and push Docker image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ./docker/Dockerfile-dim-processes-worker + platforms: linux/amd64, linux/arm64 + pull: true + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..17242f3 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -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 +############################################################### + +name: Release Please + +on: + push: + branches: + - 'changelog/v*.*.*' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + prepare-release: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v4 + name: Prepare release + with: + target-branch: ${{ github.ref_name }} + release-type: simple + skip-github-release: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3529899 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,158 @@ +############################################################### +# 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 +############################################################### + +name: Release + +on: + workflow_dispatch: + push: + paths: + - 'charts/**' + branches: + - main + +jobs: + release-helm-chart: + # depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions + # see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token + permissions: + contents: write + runs-on: ubuntu-latest + outputs: + app-version: ${{ steps.app-version.outputs.current }} + version-check: ${{ steps.version-check.outputs.exists }} + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@b7246b12e77f7134dc2d460a3d5bad15bbe29390 # v4 + + - name: Update helm dependencies for dim + run: | + cd charts/dim + helm repo add bitnami https://charts.bitnami.com/bitnami + helm dependency update + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.6.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + CR_SKIP_EXISTING: "true" + + - name: Get current appVersion + id: app-version + run: | + current=$(cat ./charts/dim/Chart.yaml | grep "appVersion:" | head -1 | cut -d ":" -d " " -f2) + echo "current=$current" >> $GITHUB_OUTPUT + echo "Exported $current appVersion" + + - name: Check for previous version + id: version-check + run: | + exists=$(git tag -l "v${{ steps.app-version.outputs.current }}") + if [[ -n "$exists" ]]; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + release-images: + needs: release-helm-chart + if: needs.release-helm-chart.outputs.version-check == 'false' + permissions: + contents: read + packages: write + runs-on: ubuntu-latest + strategy: + matrix: + include: + - image: ghcr.io/${{ github.repository }}_dim-service + dockerfile: ./docker/Dockerfile-dim-service + - image: ghcr.io/${{ github.repository }}_dim-migrations + dockerfile: ./docker/Dockerfile-dim-migrations + - image: ghcr.io/${{ github.repository }}_dim-processes-worker + dockerfile: ./docker/Dockerfile-dim-processes-worker + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # v3.2.0 + + # Create SemVer or ref tags dependent of trigger event + - name: Docker meta + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ matrix.image }} + # Automatically prepare image tags; See action docs for more examples. + # semver patter will generate tags like these for example :1 :1.2 :1.2.3 + tags: | + type=ref,event=branch + type=ref,event=pr + type=raw,value=latest + type=semver,pattern={{version}},value=${{ needs.release-helm-chart.outputs.app-version }} + type=semver,pattern={{major}},value=${{ needs.release-helm-chart.outputs.app-version }} + type=semver,pattern={{major}}.{{minor}},value=${{ needs.release-helm-chart.outputs.app-version }} + + - name: Build and push Docker images + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: linux/amd64, linux/arm64 + pull: true + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + create-tag: + needs: [release-helm-chart, release-images] + if: needs.release-helm-chart.outputs.version-check == 'false' + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + + - name: Create and push git tag + run: | + git tag v${{ needs.release-helm-chart.outputs.app-version }} + git push origin v${{ needs.release-helm-chart.outputs.app-version }} diff --git a/.github/workflows/service-docker.yml b/.github/workflows/service-docker.yml new file mode 100644 index 0000000..9cb8d29 --- /dev/null +++ b/.github/workflows/service-docker.yml @@ -0,0 +1,81 @@ +############################################################### +# 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 +############################################################### + +name: Build Service Image + +on: + push: + paths: + # service and transitive paths + - 'src/**' + # workflow file + - '.github/workflows/service-docker.yml' + # dockerfile + - 'docker/Dockerfile-dim-service' + + branches: + - 'main' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}_dim-service + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # v3.2.0 + + - name: Docker meta + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ env.REGISTRY}}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=main + type=raw,value=${{ github.sha }} + + - name: Build and push Docker image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: ./docker/Dockerfile-dim-service + platforms: linux/amd64, linux/arm64 + pull: true + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000..d972173 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,80 @@ +############################################################### +# 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 +############################################################### + +name: Sonarcloud +on: + push: + branches: [main] + paths: + - 'src/**' + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'src/**' + workflow_dispatch: + +jobs: + build: + name: Build + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + strategy: + matrix: + dotnet-version: ['8.0'] + + steps: + - name: Set up JDK 17 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + with: + distribution: 'temurin' + java-version: '17' + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Cache SonarCloud packages + uses: actions/cache@v4 + with: + path: ~/sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache SonarCloud scanner + id: cache-sonar-scanner + uses: actions/cache@v4 + with: + path: ./.sonar/scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + - name: Install SonarCloud scanner + if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' + run: | + mkdir -p ./.sonar/scanner + dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + dotnet tool install --global dotnet-coverage + ./.sonar/scanner/dotnet-sonarscanner begin /k:"${{ vars.SONAR_PROJECT_KEY }}" /o:"${{ vars.SONAR_ORGANIZATION }}" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vscoveragexml.reportsPaths=src/coverage.xml + dotnet build src + cd src + dotnet-coverage collect 'dotnet test --no-restore --verbosity normal' -s 'settings-coverage.xml' -f xml -o 'coverage.xml' + cd .. + ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 0000000..6ba91f9 --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,171 @@ +############################################################### +# 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 +############################################################### + +# Depending on the location of your Docker container +# you need to change the path to the specific Docker registry. +# +name: "Trivy" + +on: + push: + branches: [main] + # pull_request: + # The branches below must be a subset of the branches above + # branches: [ main, master ] + # paths-ignore: + # - "**/*.md" + # - "**/*.txt" + schedule: + # Once a day + - cron: "0 0 * * *" + workflow_dispatch: + # Trigger manually + +env: + REGISTRY: ghcr.io + IMAGE_NAME_SERVICE: ${{ github.repository }}_dim-service + IMAGE_NAME_MIGRATIONS: ${{ github.repository }}_dim-migrations + IMAGE_NAME_WORKER: ${{ github.repository }}_dim-processes-worker + +jobs: + analyze-config: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55 # v0.19.0 + with: + scan-type: "config" + hide-progress: false + format: "sarif" + output: "trivy-results1.sarif" + vuln-type: "os,library" + skip-dirs: "docs/" + timeout: "3600s" + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + if: always() + with: + sarif_file: "trivy-results1.sarif" + + analyze-service: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + # It's also possible to scan your private registry with Trivy's built-in image scan. + # All you have to do is set ENV vars. + # Docker Hub needs TRIVY_USERNAME and TRIVY_PASSWORD. + # You don't need to set ENV vars when downloading from a public repository. + # For public images, no ENV vars must be set. + - name: Run Trivy vulnerability scanner + if: always() + uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55 # v0.19.0 + with: + # Path to Docker image + image-ref: "${{ env.REGISTRY}}/${{ env.IMAGE_NAME_SERVICE}}:main" + format: "sarif" + output: "trivy-results2.sarif" + vuln-type: "os,library" + + - name: Upload Trivy scan results to GitHub Security tab + if: always() + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + sarif_file: "trivy-results2.sarif" + + analyze-migrations: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + # It's also possible to scan your private registry with Trivy's built-in image scan. + # All you have to do is set ENV vars. + # Docker Hub needs TRIVY_USERNAME and TRIVY_PASSWORD. + # You don't need to set ENV vars when downloading from a public repository. + # For public images, no ENV vars must be set. + - name: Run Trivy vulnerability scanner + if: always() + uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55 # v0.19.0 + with: + # Path to Docker image + image-ref: "${{ env.REGISTRY}}/${{ env.IMAGE_NAME_MIGRATIONS}}:main" + format: "sarif" + output: "trivy-results3.sarif" + vuln-type: "os,library" + + - name: Upload Trivy scan results to GitHub Security tab + if: always() + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + sarif_file: "trivy-results4.sarif" + + analyze-processes-worker: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + # It's also possible to scan your private registry with Trivy's built-in image scan. + # All you have to do is set ENV vars. + # Docker Hub needs TRIVY_USERNAME and TRIVY_PASSWORD. + # You don't need to set ENV vars when downloading from a public repository. + # For public images, no ENV vars must be set. + - name: Run Trivy vulnerability scanner + if: always() + uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55 # v0.19.0 + with: + # Path to Docker image + image-ref: "${{ env.REGISTRY}}/${{ env.IMAGE_NAME_WORKER}}:main" + format: "sarif" + output: "trivy-results4.sarif" + vuln-type: "os,library" + + - name: Upload Trivy scan results to GitHub Security tab + if: always() + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + sarif_file: "trivy-results4.sarif" + \ No newline at end of file diff --git a/.github/workflows/unit.tests-formatting.yml b/.github/workflows/unit.tests-formatting.yml new file mode 100644 index 0000000..c537db8 --- /dev/null +++ b/.github/workflows/unit.tests-formatting.yml @@ -0,0 +1,57 @@ +############################################################### +# 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 +############################################################### + +name: Unit-Tests and Formatting + +on: + push: + branches: [main] + paths: + - 'src/**' + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'src/**' + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + dotnet-version: ['8.0'] + + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Setup .NET Core SDK ${{ matrix.dotnet-version }} + uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 + with: + dotnet-version: ${{ matrix.dotnet-version }} + - name: Install dotnet-format + run: dotnet tool install -g dotnet-format + - name: Install dependencies + run: dotnet restore src + - name: Build + run: dotnet build src --configuration Release --no-restore + - name: Check Format + run: dotnet format src --verify-no-changes --no-restore + - name: Test + run: dotnet test src --no-restore --verbosity normal diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28bd2c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode/* +!.vscode/settings.json + +src/.vscode/* +!src/.vscode/settings.json + +tests/.vscode/* +!src/.vscode/settings.json + +# Rider +.idea + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ + +### Helm ### +# Chart dependencies and local install +**/charts/*.tgz +Chart.lock +**/values-local.yaml + +# local dev configuration +appsettings.Development.json diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 0000000..9430b3f --- /dev/null +++ b/.reuse/dep5 @@ -0,0 +1,29 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: ssi-dim-middle-layer +Upstream-Contact: ospo@sap.com +Source: https://github.com/SAP/ssi-dim-middle-layer +Disclaimer: The code in this project may include calls to APIs ("API Calls") of + SAP or third-party products or services developed outside of this project + ("External Products"). + "APIs" means application programming interfaces, as well as their respective + specifications and implementing code that allows software to communicate with + other software. + API Calls to External Products are not licensed under the open source license + that governs this project. The use of such API Calls and related External + Products are subject to applicable additional agreements with the relevant + provider of the External Products. In no event shall the open source license + that governs this project grant any rights in or to any External Products,or + alter, expand or supersede any terms of the applicable additional agreements. + If you have a valid license agreement with SAP for the use of a particular SAP + External Product, then you may make use of any API Calls included in this + project's code for that SAP External Product, subject to the terms of such + license agreement. If you do not have a valid license agreement for the use of + a particular SAP External Product, then you may only make use of any API Calls + in this project for that SAP External Product for your internal, non-productive + and non-commercial test and evaluation of such API Calls. Nothing herein grants + you any rights to use or access any SAP External Product, or provide any third + parties the right to use of access any SAP External Product, through API Calls. + +Files: * +Copyright: 2024 SAP SE or an SAP affiliate company and ssi-dim-middle-layer contributors +License: Apache-2.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..294c2d9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +## 1.0.0 (2024-04-09) + + +### Features + +* **authorization:** add role authorization ([#19](https://github.com/SAP/ssi-dim-middle-layer/issues/19)) ([221a435](https://github.com/SAP/ssi-dim-middle-layer/commit/221a435c629149e5fadb0514be6a595fe968594a)) +* **client:** add dim client ([266e807](https://github.com/SAP/ssi-dim-middle-layer/commit/266e80764e0009be8cdad53781194f837140e151)) +* **net8:** upgrade to .net8 ([#23](https://github.com/SAP/ssi-dim-middle-layer/issues/23)) ([d3494de](https://github.com/SAP/ssi-dim-middle-layer/commit/d3494dedf046b05ffe7b346298abbfb2286f452f)) +* **statusList:** add statuslist endpoints ([#22](https://github.com/SAP/ssi-dim-middle-layer/issues/22)) ([167ff48](https://github.com/SAP/ssi-dim-middle-layer/commit/167ff48a404b17b226addac5695df02463cd5002)) + + +### Bug Fixes + +* **bindings:** adjust errorhandling for service instances ([#29](https://github.com/SAP/ssi-dim-middle-layer/issues/29)) ([5b8f6cc](https://github.com/SAP/ssi-dim-middle-layer/commit/5b8f6cc65a60e42d6791e8e3d5a85bbd2e2dffb3)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..aed3868 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing + +## Code of Conduct + +All members of the project community must abide by the [SAP Open Source Code of Conduct](https://github.com/SAP/.github/blob/main/CODE_OF_CONDUCT.md). +Only by respecting each other we can develop a productive, collaborative community. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting [a project maintainer](.reuse/dep5). + +## Engaging in Our Project + +We use GitHub to manage reviews of pull requests. + +* If you are a new contributor, see: [Steps to Contribute](#steps-to-contribute) + +* Before implementing your change, create an issue that describes the problem you would like to solve or the code that should be enhanced. Please note that you are willing to work on that issue. + +* The team will review the issue and decide whether it should be implemented as a pull request. In that case, they will assign the issue to you. If the team decides against picking up the issue, the team will post a comment with an explanation. + +## Steps to Contribute + +Should you wish to work on an issue, please claim it first by commenting on the GitHub issue that you want to work on. This is to prevent duplicated efforts from other contributors on the same issue. + +If you have questions about one of the issues, please comment on them, and one of the maintainers will clarify. + +## Contributing Code or Documentation + +You are welcome to contribute code in order to fix a bug or to implement a new feature that is logged as an issue. + +The following rule governs code contributions: + +* Contributions must be licensed under the [Apache 2.0 License](./LICENSE) +* Due to legal reasons, contributors will be asked to accept a Developer Certificate of Origin (DCO) when they create the first pull request to this project. This happens in an automated fashion during the submission process. SAP uses [the standard DCO text of the Linux Foundation](https://developercertificate.org/). + +## Issues and Planning + +* We use GitHub issues to track bugs and enhancement requests. + +* Please provide as much context as possible when you open an issue. The information you provide must be comprehensive enough to reproduce that issue for the assignee. diff --git a/DEPENDENCIES b/DEPENDENCIES new file mode 100644 index 0000000..184bf13 --- /dev/null +++ b/DEPENDENCIES @@ -0,0 +1,47 @@ +nuget/nuget/-/AutoFixture.AutoFakeItEasy/4.18.1, MIT, approved, #10064 +nuget/nuget/-/AutoFixture/4.18.1, MIT, approved, #10057 +nuget/nuget/-/Castle.Core/5.1.1, Apache-2.0, approved, #13966 +nuget/nuget/-/EFCore.NamingConventions/8.0.3, Apache-2.0, approved, #13983 +nuget/nuget/-/FakeItEasy/8.1.0, MIT, approved, #13970 +nuget/nuget/-/Fare/2.1.1, MIT, approved, clearlydefined +nuget/nuget/-/FluentAssertions/6.12.0, MIT AND Apache-2.0 AND BSD-3-Clause AND CC-BY-3.0-US AND (GPL-2.0-only OR MIT) AND OFL-1.1 AND WTFPL, approved, #13976 +nuget/nuget/-/Flurl.Http.Signed/3.2.4, MIT, approved, #3503 +nuget/nuget/-/Flurl.Signed/3.0.6, MIT, approved, #3501 +nuget/nuget/-/Humanizer.Core/2.14.1, MIT, approved, #10060 +nuget/nuget/-/Mono.TextTemplating/2.2.1, MIT, approved, clearlydefined +nuget/nuget/-/Newtonsoft.Json/13.0.1, MIT AND BSD-3-Clause, approved, #3266 +nuget/nuget/-/Newtonsoft.Json/13.0.3, MIT AND BSD-3-Clause, approved, #3266 +nuget/nuget/-/Npgsql.EntityFrameworkCore.PostgreSQL/8.0.2, PostgreSQL AND MIT, approved, #13972 +nuget/nuget/-/Npgsql/8.0.2, PostgreSQL, approved, #13963 +nuget/nuget/-/SSH.NET/2023.0.0, MIT AND (MIT AND MS-PL) AND ISC, approved, #13965 +nuget/nuget/-/Serilog.AspNetCore/8.0.1, Apache-2.0 AND MIT, approved, #13967 +nuget/nuget/-/Serilog.Enrichers.CorrelationId/3.0.1, MIT, approved, clearlydefined +nuget/nuget/-/Serilog.Enrichers.Environment/2.3.0, Apache-2.0, approved, #13959 +nuget/nuget/-/Serilog.Enrichers.Process/2.0.2, Apache-2.0, approved, clearlydefined +nuget/nuget/-/Serilog.Enrichers.Sensitive/1.7.3, MIT, approved, clearlydefined +nuget/nuget/-/Serilog.Enrichers.Thread/3.1.0, Apache-2.0, approved, clearlydefined +nuget/nuget/-/Serilog.Extensions.Hosting/8.0.0, Apache-2.0, approved, #13962 +nuget/nuget/-/Serilog.Extensions.Logging/8.0.0, Apache-2.0, approved, #13985 +nuget/nuget/-/Serilog.Formatting.Compact/2.0.0, Apache-2.0, approved, #13981 +nuget/nuget/-/Serilog.Settings.Configuration/8.0.0, Apache-2.0, approved, #13988 +nuget/nuget/-/Serilog.Sinks.Console/5.0.1, Apache-2.0, approved, #13980 +nuget/nuget/-/Serilog.Sinks.Debug/2.0.0, Apache-2.0, approved, clearlydefined +nuget/nuget/-/Serilog.Sinks.File/5.0.0, Apache-2.0, approved, #11116 +nuget/nuget/-/Serilog/3.1.1, Apache-2.0, approved, #13978 +nuget/nuget/-/SharpZipLib/1.4.2, MIT AND GFDL-1.3-or-later AND (Apache-2.0 AND MIT) AND WTFPL AND bzip2-1.0.6 AND LicenseRef-Permissive-license-with-conditions AND LicenseRef-Permission-Notice, approved, #10058 +nuget/nuget/-/SshNet.Security.Cryptography/1.3.0, MIT, approved, clearlydefined +nuget/nuget/-/Swashbuckle.AspNetCore.Swagger/6.5.0, MIT AND Apache-2.0, approved, #7160 +nuget/nuget/-/Swashbuckle.AspNetCore.SwaggerGen/6.5.0, MIT AND Apache-2.0, approved, #7156 +nuget/nuget/-/Swashbuckle.AspNetCore.SwaggerUI/6.5.0, MIT AND Apache-2.0, approved, #7158 +nuget/nuget/-/Swashbuckle.AspNetCore/6.5.0, MIT AND Apache-2.0, approved, #7159 +nuget/nuget/-/Testcontainers.PostgreSql/3.7.0, MIT, approved, #13960 +nuget/nuget/-/Testcontainers/3.7.0, MIT, approved, #13982 +nuget/nuget/-/coverlet.collector/6.0.2, MIT, approved, #10075 +nuget/nuget/-/xunit.abstractions/2.0.3, Apache-2.0, approved, clearlydefined +nuget/nuget/-/xunit.analyzers/1.11.0, Apache-2.0 AND MIT, approved, #14197 +nuget/nuget/-/xunit.assert/2.7.0, Apache-2.0 AND MIT, approved, #13971 +nuget/nuget/-/xunit.core/2.7.0, Apache-2.0, approved, #13979 +nuget/nuget/-/xunit.extensibility.core/2.7.0, Apache-2.0 AND MIT, approved, #13974 +nuget/nuget/-/xunit.extensibility.execution/2.7.0, Apache-2.0, approved, #13977 +nuget/nuget/-/xunit.runner.visualstudio/2.5.7, Apache-2.0 AND MIT, approved, #10065 +nuget/nuget/-/xunit/2.7.0, Apache-2.0 AND MIT, approved, #13969 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://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. diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6561c26 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +[![REUSE status](https://api.reuse.software/badge/github.com/SAP/ssi-dim-middle-layer)](https://api.reuse.software/info/github.com/SAP/ssi-dim-middle-layer) + +# ssi-dim-middle-layer + +## About this project + +Integration layer between DIM solution and Tractus-X Portal. + +## Requirements and Setup + +*Insert a short description what is required to get your project running...* + +## Support, Feedback, Contributing + +This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/SAP/ssi-dim-middle-layer/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). + +## Security / Disclosure +If you find any bug that may be a security problem, please follow our instructions at [in our security policy](https://github.com/SAP/ssi-dim-middle-layer/security/policy) on how to report it. Please do not create GitHub issues for security-related doubts or problems. + +## Code of Conduct + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](https://github.com/SAP/.github/blob/main/CODE_OF_CONDUCT.md) at all times. + +## Licensing + +Copyright 2024 SAP SE or an SAP affiliate company and ssi-dim-middle-layer contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/SAP/ssi-dim-middle-layer). diff --git a/charts/chart-testing-config.yaml b/charts/chart-testing-config.yaml new file mode 100644 index 0000000..a56be06 --- /dev/null +++ b/charts/chart-testing-config.yaml @@ -0,0 +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 +############################################################### + +validate-maintainers: false +chart-repos: + - bitnami=https://charts.bitnami.com/bitnami diff --git a/charts/dim/.helmignore b/charts/dim/.helmignore new file mode 100644 index 0000000..0bffc69 --- /dev/null +++ b/charts/dim/.helmignore @@ -0,0 +1,27 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ + +# Custom dirs and files +argocd/ +*.gotmpl diff --git a/charts/dim/Chart.yaml b/charts/dim/Chart.yaml new file mode 100644 index 0000000..b279a25 --- /dev/null +++ b/charts/dim/Chart.yaml @@ -0,0 +1,32 @@ +############################################################### +# 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: v2 +name: dim +type: application +version: 1.0.0 +appVersion: 1.0.0 +description: Helm chart for DIM Middle Layer +home: https://github.com/catenax-ng/dim-repo +dependencies: + - condition: postgresql.enabled + name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 12.12.x diff --git a/charts/dim/LICENSE b/charts/dim/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/charts/dim/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://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. \ No newline at end of file diff --git a/charts/dim/README.md b/charts/dim/README.md new file mode 100644 index 0000000..3c0c278 --- /dev/null +++ b/charts/dim/README.md @@ -0,0 +1,138 @@ +# Helm chart for DIM Middle Layer + +This helm chart installs the DIM Middle Layer. + +For further information please refer to [Technical Documentation](./docs/technical-documentation). + +The referenced container images are for demonstration purposes only. + +## Installation + +To install the chart with the release name `dim`: + +```shell +$ helm repo add ssi-dim-middle-layer https://sap.github.io/ssi-dim-middle-layer +$ helm install dim ssi-dim-middle-layer/dim +``` + +To install the helm chart into your cluster with your values: + +```shell +$ helm install -f your-values.yaml dim ssi-dim-middle-layer/dim +``` + +To use the helm chart as a dependency: + +```yaml +dependencies: + - name: dim + repository: https://sap.github.io/ssi-dim-middle-layer + version: 1.0.0 +``` + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://charts.bitnami.com/bitnami | postgresql | 12.12.x | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| dim.image.name | string | `"ghcr.io/sap/dim-client_dim-service"` | | +| dim.image.tag | string | `""` | | +| dim.imagePullPolicy | string | `"IfNotPresent"` | | +| dim.resources | object | `{"limits":{"cpu":"45m","memory":"300M"},"requests":{"cpu":"15m","memory":"300M"}}` | We recommend to review the default resource limits as this should a conscious choice. | +| dim.healthChecks.startup.path | string | `"/health/startup"` | | +| dim.healthChecks.startup.tags[0].name | string | `"HEALTHCHECKS__0__TAGS__1"` | | +| dim.healthChecks.startup.tags[0].value | string | `"dimdb"` | | +| dim.healthChecks.liveness.path | string | `"/healthz"` | | +| dim.healthChecks.readyness.path | string | `"/ready"` | | +| dim.swaggerEnabled | bool | `false` | | +| dim.rootDirectoryId | string | `"00000000-0000-0000-0000-000000000000"` | | +| dim.operatorId | string | `"00000000-0000-0000-0000-000000000000"` | | +| migrations.name | string | `"migrations"` | | +| migrations.image.name | string | `"ghcr.io/sap/dim-client_dim-migrations"` | | +| migrations.image.tag | string | `""` | | +| migrations.imagePullPolicy | string | `"IfNotPresent"` | | +| migrations.resources | object | `{"limits":{"cpu":"45m","memory":"200M"},"requests":{"cpu":"15m","memory":"200M"}}` | We recommend to review the default resource limits as this should a conscious choice. | +| migrations.seeding.testDataEnvironments | string | `""` | | +| migrations.seeding.testDataPaths | string | `"Seeder/Data"` | | +| migrations.logging.default | string | `"Information"` | | +| processesworker.name | string | `"processesworker"` | | +| processesworker.image.name | string | `"ghcr.io/sap/dim-client_dim-processes-worker"` | | +| processesworker.image.tag | string | `""` | | +| processesworker.imagePullPolicy | string | `"IfNotPresent"` | | +| processesworker.resources | object | `{"limits":{"cpu":"45m","memory":"300M"},"requests":{"cpu":"15m","memory":"300M"}}` | We recommend to review the default resource limits as this should a conscious choice. | +| processesworker.dim.adminMail | string | `"mail@example.org"` | | +| processesworker.dim.clientIdCisCentral | string | `""` | | +| processesworker.dim.clientSecretCisCentral | string | `""` | | +| processesworker.dim.authUrl | string | `""` | | +| processesworker.subaccount.baseUrl | string | `""` | Url to the subaccount service api | +| processesworker.entitlement.baseUrl | string | `""` | Url to the entitlement service api | +| processesworker.cf.clientId | string | `""` | | +| processesworker.cf.clientSecret | string | `""` | | +| processesworker.cf.tokenAddress | string | `""` | | +| processesworker.cf.baseUrl | string | `""` | Url to the cf service api | +| processesworker.cf.grantType | string | `"client_credentials"` | | +| processesworker.callback.scope | string | `"openid"` | | +| processesworker.callback.grantType | string | `"client_credentials"` | | +| processesworker.callback.clientId | string | `""` | Provide client-id for callback. | +| processesworker.callback.clientSecret | string | `""` | Client-secret for callback client-id. Secret-key 'callback-client-secret'. | +| processesworker.callback.tokenAddress | string | `""` | | +| processesworker.callback.baseAddress | string | `""` | Url to the cf service api | +| existingSecret | string | `""` | Secret containing "client-secret-cis-central", "client-secret-cf" and "client-secret-callback" | +| dotnetEnvironment | string | `"Production"` | | +| dbConnection.schema | string | `"dim"` | | +| dbConnection.sslMode | string | `"Disable"` | | +| postgresql.enabled | bool | `true` | PostgreSQL chart configuration; default configurations: host: "dim-postgresql-primary", port: 5432; Switch to enable or disable the PostgreSQL helm chart. | +| postgresql.image | object | `{"tag":"15-debian-12"}` | Setting image tag to major to get latest minor updates | +| postgresql.commonLabels."app.kubernetes.io/version" | string | `"15"` | | +| postgresql.auth.username | string | `"dim"` | Non-root username. | +| postgresql.auth.database | string | `"dim"` | Database name. | +| postgresql.auth.existingSecret | string | `"{{ .Release.Name }}-dim-postgres"` | Secret containing the passwords for root usernames postgres and non-root username dim. Should not be changed without changing the "dim-postgresSecretName" template as well. | +| postgresql.auth.postgrespassword | string | `""` | Password for the root username 'postgres'. Secret-key 'postgres-password'. | +| postgresql.auth.password | string | `""` | Password for the non-root username 'dim'. Secret-key 'password'. | +| postgresql.auth.replicationPassword | string | `""` | Password for the non-root username 'repl_user'. Secret-key 'replication-password'. | +| postgresql.architecture | string | `"replication"` | | +| postgresql.audit.pgAuditLog | string | `"write, ddl"` | | +| postgresql.audit.logLinePrefix | string | `"%m %u %d "` | | +| postgresql.primary.extendedConfiguration | string | `""` | Extended PostgreSQL Primary configuration (increase of max_connections recommended - default is 100) | +| postgresql.primary.initdb.scriptsConfigMap | string | `"{{ .Release.Name }}-dim-cm-postgres"` | | +| postgresql.readReplicas.extendedConfiguration | string | `""` | Extended PostgreSQL read only replicas configuration (increase of max_connections recommended - default is 100) | +| externalDatabase.host | string | `"dim-postgres-ext"` | External PostgreSQL configuration IMPORTANT: non-root db user needs to be created beforehand on external database. And the init script (02-init-db.sql) available in templates/configmap-postgres-init.yaml needs to be executed beforehand. Database host ('-primary' is added as postfix). | +| externalDatabase.port | int | `5432` | Database port number. | +| externalDatabase.username | string | `"dim"` | Non-root username for dim. | +| externalDatabase.database | string | `"dim"` | Database name. | +| externalDatabase.password | string | `""` | Password for the non-root username (default 'dim'). Secret-key 'password'. | +| externalDatabase.existingSecret | string | `"dim-external-db"` | Secret containing the password non-root username, (default 'dim'). | +| idp | object | `{"address":"https://centralidp.example.org","authRealm":"CX-Central","jwtBearerOptions":{"metadataPath":"/auth/realms/CX-Central/.well-known/openid-configuration","refreshInterval":"00:00:30","requireHttpsMetadata":"true","tokenValidationParameters":{"validAudience":"DIM-Middle-Layer","validIssuerPath":"/auth/realms/CX-Central"}},"tokenPath":"/auth/realms/CX-Central/protocol/openid-connect/token","useAuthTrail":true}` | Provide details about idp instance. | +| idp.address | string | `"https://centralidp.example.org"` | Provide idp base address, without trailing '/auth'. | +| idp.useAuthTrail | bool | `true` | Flag if the api should be used with an leading /auth path | +| ingress.enabled | bool | `false` | DIM ingress parameters, enable ingress record generation for dim. | +| ingress.tls[0] | object | `{"hosts":[""],"secretName":""}` | Provide tls secret. | +| ingress.tls[0].hosts | list | `[""]` | Provide host for tls secret. | +| ingress.hosts[0] | object | `{"host":"","paths":[{"backend":{"port":8080},"path":"/api/dim","pathType":"Prefix"}]}` | Provide default path for the ingress record. | +| portContainer | int | `8080` | | +| portService | int | `8080` | | +| replicaCount | int | `3` | | +| nodeSelector | object | `{}` | Node labels for pod assignment | +| tolerations | list | `[]` | Tolerations for pod assignment | +| affinity.podAntiAffinity | object | `{"preferredDuringSchedulingIgnoredDuringExecution":[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/name","operator":"DoesNotExist"}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}]}` | Following Catena-X Helm Best Practices, [reference](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity). | +| updateStrategy.type | string | `"RollingUpdate"` | Update strategy type, rolling update configuration parameters, [reference](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies). | +| updateStrategy.rollingUpdate.maxSurge | int | `1` | | +| updateStrategy.rollingUpdate.maxUnavailable | int | `0` | | +| startupProbe | object | `{"failureThreshold":30,"initialDelaySeconds":10,"periodSeconds":10,"successThreshold":1,"timeoutSeconds":1}` | Following Catena-X Helm Best Practices, [reference](https://github.com/helm/charts/blob/master/stable/nginx-ingress/values.yaml#L210). | +| livenessProbe.failureThreshold | int | `3` | | +| livenessProbe.initialDelaySeconds | int | `10` | | +| livenessProbe.periodSeconds | int | `10` | | +| livenessProbe.successThreshold | int | `1` | | +| livenessProbe.timeoutSeconds | int | `10` | | +| readinessProbe.failureThreshold | int | `3` | | +| readinessProbe.initialDelaySeconds | int | `10` | | +| readinessProbe.periodSeconds | int | `10` | | +| readinessProbe.successThreshold | int | `1` | | +| readinessProbe.timeoutSeconds | int | `1` | | + +Autogenerated with [helm docs](https://github.com/norwoodj/helm-docs) diff --git a/charts/dim/README.md.gotmpl b/charts/dim/README.md.gotmpl new file mode 100644 index 0000000..5daa8a4 --- /dev/null +++ b/charts/dim/README.md.gotmpl @@ -0,0 +1,37 @@ +# {{ template "chart.description" . }} + +This helm chart installs the DIM Middle Layer. + +For further information please refer to [Technical Documentation](./docs/technical-documentation). + +The referenced container images are for demonstration purposes only. + +## Installation + +To install the chart with the release name `{{ template "chart.name" . }}`: + +```shell +$ helm repo add ssi-dim-middle-layer https://sap.github.io/ssi-dim-middle-layer +$ helm install {{ template "chart.name" . }} ssi-dim-middle-layer/{{ template "chart.name" . }} +``` + +To install the helm chart into your cluster with your values: + +```shell +$ helm install -f your-values.yaml {{ template "chart.name" . }} ssi-dim-middle-layer/{{ template "chart.name" . }} +``` + +To use the helm chart as a dependency: + +```yaml +dependencies: + - name: {{ template "chart.name" . }} + repository: https://sap.github.io/ssi-dim-middle-layer + version: {{ template "chart.version" . }} +``` + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +Autogenerated with [helm docs](https://github.com/norwoodj/helm-docs) diff --git a/charts/dim/templates/_helpers.tpl b/charts/dim/templates/_helpers.tpl new file mode 100644 index 0000000..75c4d05 --- /dev/null +++ b/charts/dim/templates/_helpers.tpl @@ -0,0 +1,129 @@ +{{- /* +* 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 +*/}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "dim.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "dim.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "dim.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Determine secret name. +*/}} +{{- define "dim.secretName" -}} +{{- if .Values.existingSecret -}} +{{- .Values.existingSecret }} +{{- else -}} +{{- include "dim.fullname" . -}} +{{- end -}} +{{- end -}} + +{{/* +Define secret name of postgres dependency. +*/}} +{{- define "dim.postgresSecretName" -}} +{{- printf "%s-%s" .Release.Name "dim-postgres" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "dim.labels" -}} +helm.sh/chart: {{ include "dim.chart" . }} +{{ include "dim.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "dim.selectorLabels" -}} +app.kubernetes.io/name: {{ include "dim.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "dim.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "dim.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Determine database hostname for subchart +*/}} + +{{- define "dim.postgresql.primary.fullname" -}} +{{- if eq .Values.postgresql.architecture "replication" }} +{{- printf "%s-primary" (include "dim.chart.name.postgresql.dependency" .) | trunc 63 | trimSuffix "-" -}} +{{- else -}} + {{- include "dim.chart.name.postgresql.dependency" . -}} +{{- end -}} +{{- end -}} + +{{- define "dim.postgresql.readReplica.fullname" -}} +{{- printf "%s-read" (include "dim.chart.name.postgresql.dependency" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "dim.chart.name.postgresql.dependency" -}} +{{- if .Values.postgresql.fullnameOverride -}} +{{- .Values.postgresql.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default "postgresql" .Values.postgresql.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/charts/dim/templates/configmap-postgres-init.yaml b/charts/dim/templates/configmap-postgres-init.yaml new file mode 100644 index 0000000..75b3629 --- /dev/null +++ b/charts/dim/templates/configmap-postgres-init.yaml @@ -0,0 +1,36 @@ +{{- /* +* 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 +*/}} + +{{- if .Values.postgresql.enabled -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-dim-cm-postgres + namespace: {{ .Release.Namespace }} +data: + 02-init-db.sql: | + CREATE SCHEMA dim; + ALTER SCHEMA dim OWNER TO dim; + CREATE TABLE public.__efmigrations_history_dim ( + migration_id character varying(150) NOT NULL, + product_version character varying(32) NOT NULL + ); + ALTER TABLE public.__efmigrations_history_dim OWNER TO dim; +{{- end -}} diff --git a/charts/dim/templates/cronjob-processes.yaml b/charts/dim/templates/cronjob-processes.yaml new file mode 100644 index 0000000..53e04af --- /dev/null +++ b/charts/dim/templates/cronjob-processes.yaml @@ -0,0 +1,124 @@ +############################################################### +# 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: batch/v1 +kind: CronJob +metadata: + name: {{ include "dim.fullname" . }}-{{ .Values.processesworker.name }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "dim.labels" . | nindent 4 }} +spec: + schedule: "*/5 * * * *" + concurrencyPolicy: Forbid + jobTemplate: + metadata: + name: {{ include "dim.fullname" . }}-{{ .Values.processesworker.name }} + spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: {{ include "dim.fullname" . }}-{{ .Values.processesworker.name }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + image: "{{ .Values.processesworker.image.name }}:{{ .Values.processesworker.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: "{{ .Values.processesworker.imagePullPolicy }}" + env: + - name: DOTNET_ENVIRONMENT + value: "{{ .Values.dotnetEnvironment }}" + {{- if .Values.postgresql.enabled }} + - name: "DIM_PASSWORD" + valueFrom: + secretKeyRef: + name: "{{ template "dim.postgresSecretName" . }}" + key: "password" + - name: "CONNECTIONSTRINGS__DIMDB" + value: "Server={{ template "dim.postgresql.primary.fullname" . }};Database={{ .Values.postgresql.auth.database }};Port={{ .Values.postgresql.auth.port }};User Id={{ .Values.postgresql.auth.username }};Password=$(DIM_PASSWORD);Ssl Mode={{ .Values.dbConnection.sslMode }};" + {{- end }} + {{- if not .Values.postgresql.enabled }} + - name: "DIM_PASSWORD" + valueFrom: + secretKeyRef: + name: "{{ .Values.externalDatabase.secret }}" + key: "password" + - name: "CONNECTIONSTRINGS__DIMDB" + value: "Server={{ .Values.externalDatabase.host }};Database={{ .Values.externalDatabase.database }};Port={{ .Values.externalDatabase.port }};User Id={{ .Values.externalDatabase.username }};Password=$(DIM_PASSWORD);Ssl Mode={{ .Values.dbConnection.sslMode }};" + {{- end }} + - name: "DIM__ADMINMAIL" + value: "{{ .Values.processesworker.dim.adminMail }}" + - name: "DIM__ROOTDIRECTORYID" + value: "{{ .Values.dim.rootDirectoryId }}" + - name: "DIM__CLIENTIDCISCENTRAL" + value: "{{ .Values.processesworker.dim.clientIdCisCentral }}" + - name: "DIM__CLIENTSECRETCISCENTRAL" + valueFrom: + secretKeyRef: + name: "{{ template "dim.secretName" . }}" + key: "client-secret-cis-central" + - name: "DIM__AUTHURL" + value: "{{ .Values.processesworker.dim.authUrl }}" + - name: "SUBACCOUNT__BASEURL" + value: "{{ .Values.processesworker.subaccount.baseUrl }}" + - name: "ENTITLEMENT__BASEURL" + value: "{{ .Values.processesworker.entitlement.baseUrl }}" + - name: "CF__CLIENTID" + value: "{{ .Values.processesworker.cf.clientId }}" + - name: "CF__CLIENTSECRET" + valueFrom: + secretKeyRef: + name: "{{ template "dim.secretName" . }}" + key: "client-secret-cf" + - name: "CF__TOKENADDRESS" + value: "{{ .Values.processesworker.cf.tokenAddress }}" + - name: "CF__BASEURL" + value: "{{ .Values.processesworker.cf.baseUrl }}" + - name: "CF__GRANTTYPE" + value: "{{ .Values.processesworker.cf.grantType }}" + - name: "CALLBACK__USERNAME" + value: "empty" + - name: "CALLBACK__PASSWORD" + value: "empty" + - name: "CALLBACK__CLIENTID" + value: "{{ .Values.processesworker.callback.clientId }}" + - name: "CALLBACK__CLIENTSECRET" + valueFrom: + secretKeyRef: + name: "{{ template "dim.secretName" . }}" + key: "client-secret-callback" + - name: "CALLBACK__GRANTTYPE" + value: "{{ .Values.processesworker.callback.grantType }}" + - name: "CALLBACK__SCOPE" + value: "{{ .Values.processesworker.callback.scope }}" + - name: "CALLBACK__TOKENADDRESS" + value: "{{ .Values.processesworker.callback.tokenAddress }}" + - name: "CALLBACK__BASEADDRESS" + value: "{{ .Values.processesworker.callback.baseAddress }}" + ports: + - name: http + containerPort: {{ .Values.portContainer }} + protocol: TCP + resources: + {{- toYaml .Values.processesworker.resources | nindent 14 }} diff --git a/charts/dim/templates/deployment.yaml b/charts/dim/templates/deployment.yaml new file mode 100644 index 0000000..de8d07e --- /dev/null +++ b/charts/dim/templates/deployment.yaml @@ -0,0 +1,157 @@ +############################################################### +# 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: apps/v1 +kind: Deployment +metadata: + name: {{ include "dim.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "dim.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + {{- toYaml .Values.updateStrategy | nindent 4 }} + selector: + matchLabels: + {{- include "dim.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "dim.selectorLabels" . | nindent 8 }} + spec: + containers: + - name: {{ include "dim.fullname" . }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + image: "{{ .Values.dim.image.name }}:{{ .Values.dim.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: "{{ .Values.dim.imagePullPolicy }}" + env: + - name: DOTNET_ENVIRONMENT + value: "{{ .Values.dotnetEnvironment }}" + {{- if .Values.postgresql.enabled }} + - name: "DIM_PASSWORD" + valueFrom: + secretKeyRef: + name: "{{ template "dim.postgresSecretName" . }}" + key: "password" + - name: "CONNECTIONSTRINGS__DIMDB" + value: "Server={{ template "dim.postgresql.primary.fullname" . }};Database={{ .Values.postgresql.auth.database }};Port={{ .Values.postgresql.auth.port }};User Id={{ .Values.postgresql.auth.username }};Password=$(DIM_PASSWORD);Ssl Mode={{ .Values.dbConnection.sslMode }};" + {{- end }} + {{- if not .Values.postgresql.enabled }} + - name: "DIM_PASSWORD" + valueFrom: + secretKeyRef: + name: "{{ .Values.externalDatabase.existingSecret }}" + key: "password" + - name: "CONNECTIONSTRINGS__DIMDB" + value: "Server={{ .Values.externalDatabase.host }};Database={{ .Values.externalDatabase.database }};Port={{ .Values.externalDatabase.port }};User Id={{ .Values.externalDatabase.username }};Password=$(DIM_PASSWORD);Ssl Mode={{ .Values.dbConnection.sslMode }};" + {{- end }} + - name: "HEALTHCHECKS__0__PATH" + value: "{{ .Values.dim.healthChecks.startup.path}}" + {{- if .Values.dim.healthChecks.startup.tags }} + {{- toYaml .Values.dim.healthChecks.startup.tags | nindent 8 }} + {{- end }} + - name: "HEALTHCHECKS__1__PATH" + value: "{{ .Values.dim.healthChecks.readyness.path}}" + - name: "HEALTHCHECKS__2__PATH" + value: "{{ .Values.dim.healthChecks.liveness.path}}" + - name: "SWAGGERENABLED" + value: "{{ .Values.dim.swaggerEnabled }}" + - name: "DIM__ROOTDIRECTORYID" + value: "{{ .Values.dim.rootDirectoryId }}" + - name: "DIM__OPERATORID" + value: "{{ .Values.dim.operatorId }}" + - name: "JWTBEAREROPTIONS__METADATAADDRESS" + value: "{{ .Values.idp.address }}{{ .Values.idp.jwtBearerOptions.metadataPath }}" + - name: "JWTBEAREROPTIONS__REQUIREHTTPSMETADATA" + value: "{{ .Values.idp.jwtBearerOptions.requireHttpsMetadata }}" + - name: "JWTBEAREROPTIONS__TOKENVALIDATIONPARAMETERS__VALIDAUDIENCE" + value: "{{ .Values.idp.jwtBearerOptions.tokenValidationParameters.validAudience }}" + - name: "JWTBEAREROPTIONS__TOKENVALIDATIONPARAMETERS__VALIDISSUER" + value: "{{ .Values.idp.address }}{{ .Values.idp.jwtBearerOptions.tokenValidationParameters.validIssuerPath }}" + - name: "JWTBEAREROPTIONS__REFRESHINTERVAL" + value: "{{ .Values.idp.jwtBearerOptions.refreshInterval }}" + - name: "CF__CLIENTID" + value: "{{ .Values.processesworker.cf.clientId }}" + - name: "CF__CLIENTSECRET" + valueFrom: + secretKeyRef: + name: "{{ template "dim.secretName" . }}" + key: "client-secret-cf" + - name: "CF__TOKENADDRESS" + value: "{{ .Values.processesworker.cf.tokenAddress }}" + - name: "CF__BASEURL" + value: "{{ .Values.processesworker.cf.baseUrl }}" + - name: "CF__GRANTTYPE" + value: "{{ .Values.processesworker.cf.grantType }}" + ports: + - name: http + containerPort: {{ .Values.portContainer }} + protocol: TCP + startupProbe: + httpGet: + path: {{ .Values.dim.healthChecks.startup.path }} + port: {{ .Values.portContainer }} + scheme: HTTP + initialDelaySeconds: {{ .Values.startupProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.startupProbe.periodSeconds }} + timeoutSeconds: {{ .Values.startupProbe.timeoutSeconds }} + successThreshold: {{ .Values.startupProbe.successThreshold }} + failureThreshold: {{ .Values.startupProbe.failureThreshold }} + livenessProbe: + httpGet: + path: {{ .Values.dim.healthChecks.liveness.path }} + port: {{ .Values.portContainer }} + scheme: HTTP + initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} + successThreshold: {{ .Values.livenessProbe.successThreshold }} + failureThreshold: {{ .Values.livenessProbe.failureThreshold }} + readinessProbe: + httpGet: + path: {{ .Values.dim.healthChecks.readyness.path }} + port: {{ .Values.portContainer }} + scheme: HTTP + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} + successThreshold: {{ .Values.readinessProbe.successThreshold }} + failureThreshold: {{ .Values.readinessProbe.failureThreshold }} + resources: + {{- toYaml .Values.dim.resources | nindent 10 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/dim/templates/ingress.yaml b/charts/dim/templates/ingress.yaml new file mode 100644 index 0000000..c3e0fcc --- /dev/null +++ b/charts/dim/templates/ingress.yaml @@ -0,0 +1,81 @@ +{{- /* +* 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 +*/}} + +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "dim.fullname" . -}} +{{- $svcPort := .Values.portService -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "dim.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ .backend.port }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/dim/templates/job-migrations.yaml b/charts/dim/templates/job-migrations.yaml new file mode 100644 index 0000000..45295c6 --- /dev/null +++ b/charts/dim/templates/job-migrations.yaml @@ -0,0 +1,78 @@ +############################################################### +# 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: batch/v1 +kind: Job +metadata: + name: {{ include "dim.fullname" . }}-{{ .Values.migrations.name }} + annotations: + "batch.kubernetes.io/job-tracking": "true" + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "-5" +spec: + template: + metadata: + name: {{ include "dim.fullname" . }}-{{ .Values.migrations.name }} + spec: + restartPolicy: Never + containers: + - name: {{ include "dim.fullname" . }}-{{ .Values.migrations.name }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + image: "{{ .Values.migrations.image.name }}:{{ .Values.migrations.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: "{{ .Values.migrations.imagePullPolicy }}" + env: + - name: DOTNET_ENVIRONMENT + value: "{{ .Values.dotnetEnvironment }}" + {{- if .Values.postgresql.enabled }} + - name: "DIM_PASSWORD" + valueFrom: + secretKeyRef: + name: "{{ template "dim.postgresSecretName" . }}" + key: "password" + - name: "CONNECTIONSTRINGS__DIMDB" + value: "Server={{ template "dim.postgresql.primary.fullname" . }};Database={{ .Values.postgresql.auth.database }};Port={{ .Values.postgresql.auth.port }};User Id={{ .Values.postgresql.auth.username }};Password=$(DIM_PASSWORD);Ssl Mode={{ .Values.dbConnection.sslMode }};" + {{- end }} + {{- if not .Values.postgresql.enabled }} + - name: "DIM_PASSWORD" + valueFrom: + secretKeyRef: + name: "{{ .Values.externalDatabase.existingSecret }}" + key: "password" + - name: "CONNECTIONSTRINGS__DIMDB" + value: "Server={{ .Values.externalDatabase.host }};Database={{ .Values.externalDatabase.database }};Port={{ .Values.externalDatabase.port }};User Id={{ .Values.externalDatabase.username }};Password=$(DIM_PASSWORD);Ssl Mode={{ .Values.dbConnection.sslMode }};" + {{- end }} + - name: "SEEDING__TESTDATAENVIRONMENTS__0" + value: "{{ .Values.migrations.seeding.testDataEnvironments }}" + - name: "SEEDING__DATAPATHS__0" + value: "{{ .Values.migrations.seeding.testDataPaths }}" + - name: "SERILOG__MINIMUMLEVEL__Default" + value: "{{ .Values.migrations.logging.default }}" + ports: + - name: http + containerPort: {{ .Values.portContainer }} + protocol: TCP + resources: + {{- toYaml .Values.migrations.resources | nindent 10 }} diff --git a/charts/dim/templates/secret-external-db.yaml b/charts/dim/templates/secret-external-db.yaml new file mode 100644 index 0000000..8447fce --- /dev/null +++ b/charts/dim/templates/secret-external-db.yaml @@ -0,0 +1,40 @@ +{{- /* +* 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 +*/}} + +{{- if not .Values.postgresql.enabled -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.externalDatabase.existingSecret }} + namespace: {{ .Release.Namespace }} +type: Opaque +# use lookup function to check if secret exists +{{- $secret := (lookup "v1" "Secret" .Release.Namespace .Values.externalDatabase.existingSecret) }} +{{ if $secret -}} +data: + # if secret exists, use value provided from values file (to cover update scenario) or existing value from secret + # use data map instead of stringData to prevent base64 encoding of already base64-encoded existing value from secret + password: {{ ( .Values.externalDatabase.password | b64enc ) | default $secret.data.password | quote }} +{{ else -}} +stringData: + # if secret doesn't exist, use provided value from values file or generate a random one + password: {{ .Values.externalDatabase.password | default ( randAlphaNum 32 ) | quote }} +{{ end }} +{{- end -}} diff --git a/charts/dim/templates/secret-postgres.yaml b/charts/dim/templates/secret-postgres.yaml new file mode 100644 index 0000000..9998703 --- /dev/null +++ b/charts/dim/templates/secret-postgres.yaml @@ -0,0 +1,46 @@ +{{- /* +* 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 +*/}} + +{{- if .Values.postgresql.enabled -}} +{{- $secretName := include "dim.postgresSecretName" . -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $secretName }} + namespace: {{ .Release.Namespace }} +type: Opaque +# use lookup function to check if secret exists +{{- $secret := (lookup "v1" "Secret" .Release.Namespace $secretName) }} +{{ if $secret -}} +data: + # if secret exists, use value provided from values file (to cover update scenario) or existing value from secret + # use data map instead of stringData to prevent base64 encoding of already base64-encoded existing value from secret + # use index function for secret keys with hyphen otherwise '$secret.data.secretKey' works too + postgres-password: {{ ( .Values.postgresql.auth.postgrespassword | b64enc ) | default ( index $secret.data "postgres-password" ) | quote }} + password: {{ ( .Values.postgresql.auth.password | b64enc ) | default $secret.data.password | quote }} + replication-password: {{ ( .Values.postgresql.auth.replicationPassword | b64enc ) | default ( index $secret.data "replication-password" ) | quote}} +{{ else -}} +stringData: + # if secret doesn't exist, use provided value from values file or generate a random one + postgres-password: {{ .Values.postgresql.auth.postgrespassword | default ( randAlphaNum 32 ) | quote }} + password: {{ .Values.postgresql.auth.password | default ( randAlphaNum 32 ) | quote }} + replication-password: {{ .Values.postgresql.auth.replicationPassword | default ( randAlphaNum 32 ) | quote }} +{{ end }} +{{- end -}} diff --git a/charts/dim/templates/secret.yaml b/charts/dim/templates/secret.yaml new file mode 100644 index 0000000..996d04b --- /dev/null +++ b/charts/dim/templates/secret.yaml @@ -0,0 +1,48 @@ +{{- /* +* 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 +*/}} + +{{- if not .Values.existingSecret }} +{{- $secretName := include "dim.secretName" . -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "dim.secretName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "dim.labels" . | nindent 4 }} +type: Opaque +# use lookup function to check if secret exists +{{- $secret := (lookup "v1" "Secret" .Release.Namespace $secretName) }} +{{ if $secret -}} +data: + # if secret exists, use value provided from values file (to cover update scenario) or existing value from secret or generate a random one (if keys are added later on) + # use data map instead of stringData to prevent base64 encoding of already base64-encoded existing value from secret + # use index function for secret keys with hyphen otherwise '$secret.data.secretKey' works too + client-secret-cis-central: {{ coalesce ( .Values.processesworker.dim.clientSecretCisCentral | b64enc ) ( index $secret.data "client-secret-cis-central" ) | default ( randAlphaNum 32 ) | quote }} + client-secret-cf: {{ coalesce ( .Values.processesworker.cf.clientSecret | b64enc ) ( index $secret.data "client-secret-cf" ) | default ( randAlphaNum 32 ) | quote }} + client-secret-callback: {{ coalesce ( .Values.processesworker.callback.clientSecret | b64enc ) ( index $secret.data "client-secret-callback" ) | default ( randAlphaNum 32 ) | quote }} +{{ else -}} +stringData: + # if secret doesn't exist, use provided value from values file or generate a random one + client-secret-cis-central: {{ .Values.processesworker.dim.clientSecretCisCentral | default ( randAlphaNum 32 ) | quote }} + client-secret-cf: {{ .Values.processesworker.cf.clientSecret | default ( randAlphaNum 32 ) | quote }} + client-secret-callback: {{ .Values.processesworker.callback.clientSecret | default ( randAlphaNum 32 ) | quote }} +{{ end }} +{{- end -}} diff --git a/charts/dim/templates/service.yaml b/charts/dim/templates/service.yaml new file mode 100644 index 0000000..8906892 --- /dev/null +++ b/charts/dim/templates/service.yaml @@ -0,0 +1,35 @@ +############################################################### +# 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: v1 +kind: Service +metadata: + name: {{ include "dim.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "dim.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: {{ .Values.portService }} + targetPort: {{ .Values.portContainer }} + selector: + {{- include "dim.selectorLabels" . | nindent 4 }} + diff --git a/charts/dim/values.yaml b/charts/dim/values.yaml new file mode 100644 index 0000000..b1b82ed --- /dev/null +++ b/charts/dim/values.yaml @@ -0,0 +1,273 @@ +############################################################### +# 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 +############################################################### + +dim: + image: + name: "ghcr.io/sap/dim-client_dim-service" + tag: "" + imagePullPolicy: "IfNotPresent" + # -- We recommend to review the default resource limits as this should a conscious choice. + resources: + requests: + cpu: 15m + memory: 300M + limits: + cpu: 45m + memory: 300M + healthChecks: + startup: + path: "/health/startup" + tags: + - name: "HEALTHCHECKS__0__TAGS__1" + value: "dimdb" + liveness: + path: "/healthz" + readyness: + path: "/ready" + swaggerEnabled: false + rootDirectoryId: "00000000-0000-0000-0000-000000000000" + operatorId: "00000000-0000-0000-0000-000000000000" + +migrations: + name: "migrations" + image: + name: "ghcr.io/sap/dim-client_dim-migrations" + tag: "" + imagePullPolicy: "IfNotPresent" + # -- We recommend to review the default resource limits as this should a conscious choice. + resources: + requests: + cpu: 15m + memory: 200M + limits: + cpu: 45m + memory: 200M + seeding: + testDataEnvironments: "" + testDataPaths: "Seeder/Data" + logging: + default: "Information" + +processesworker: + name: "processesworker" + image: + name: "ghcr.io/sap/dim-client_dim-processes-worker" + tag: "" + imagePullPolicy: "IfNotPresent" + # -- We recommend to review the default resource limits as this should a conscious choice. + resources: + requests: + cpu: 15m + memory: 300M + limits: + cpu: 45m + memory: 300M + dim: + adminMail: "mail@example.org" + clientIdCisCentral: "" + clientSecretCisCentral: "" + authUrl: "" + subaccount: + # -- Url to the subaccount service api + baseUrl: "" + entitlement: + # -- Url to the entitlement service api + baseUrl: "" + cf: + clientId: "" + clientSecret: "" + tokenAddress: "" + # -- Url to the cf service api + baseUrl: "" + 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: "" + # -- Url to the cf service api + baseAddress: "" + +# -- Secret containing "client-secret-cis-central", "client-secret-cf" and "client-secret-callback" +existingSecret: "" + +dotnetEnvironment: "Production" + +dbConnection: + schema: "dim" + sslMode: "Disable" + +postgresql: + # -- PostgreSQL chart configuration; + # default configurations: + # host: "dim-postgresql-primary", + # port: 5432; + # Switch to enable or disable the PostgreSQL helm chart. + enabled: true + # -- Setting image tag to major to get latest minor updates + image: + tag: "15-debian-12" + commonLabels: + app.kubernetes.io/version: "15" + auth: + # -- Non-root username. + username: dim + # -- Database name. + database: dim + # -- Secret containing the passwords for root usernames postgres and non-root username dim. + # Should not be changed without changing the "dim-postgresSecretName" template as well. + existingSecret: "{{ .Release.Name }}-dim-postgres" + # -- Password for the root username 'postgres'. Secret-key 'postgres-password'. + postgrespassword: "" + # -- Password for the non-root username 'dim'. Secret-key 'password'. + password: "" + # -- Password for the non-root username 'repl_user'. Secret-key 'replication-password'. + replicationPassword: "" + architecture: replication + audit: + pgAuditLog: "write, ddl" + logLinePrefix: "%m %u %d " + primary: + # -- Extended PostgreSQL Primary configuration (increase of max_connections recommended - default is 100) + extendedConfiguration: "" + initdb: + scriptsConfigMap: "{{ .Release.Name }}-dim-cm-postgres" + readReplicas: + # -- Extended PostgreSQL read only replicas configuration (increase of max_connections recommended - default is 100) + extendedConfiguration: "" + +externalDatabase: + # -- External PostgreSQL configuration + # IMPORTANT: non-root db user needs to be created beforehand on external database. + # And the init script (02-init-db.sql) available in templates/configmap-postgres-init.yaml + # needs to be executed beforehand. + # Database host ('-primary' is added as postfix). + host: "dim-postgres-ext" + # -- Database port number. + port: 5432 + # -- Non-root username for dim. + username: "dim" + # -- Database name. + database: "dim" + # -- Password for the non-root username (default 'dim'). Secret-key 'password'. + password: "" + # -- Secret containing the password non-root username, (default 'dim'). + existingSecret: "dim-external-db" + +# -- Provide details about idp instance. +idp: + # -- Provide idp base address, without trailing '/auth'. + address: "https://centralidp.example.org" + authRealm: "CX-Central" + jwtBearerOptions: + requireHttpsMetadata: "true" + metadataPath: "/auth/realms/CX-Central/.well-known/openid-configuration" + tokenValidationParameters: + validIssuerPath: "/auth/realms/CX-Central" + validAudience: "DIM-Middle-Layer" + refreshInterval: "00:00:30" + tokenPath: "/auth/realms/CX-Central/protocol/openid-connect/token" + # -- Flag if the api should be used with an leading /auth path + useAuthTrail: true + +ingress: + # -- DIM ingress parameters, + # enable ingress record generation for dim. + enabled: false + # className: "nginx" + ## Optional annotations when using the nginx ingress class + # annotations: + # nginx.ingress.kubernetes.io/use-regex: "true" + # nginx.ingress.kubernetes.io/enable-cors: "true" + # nginx.ingress.kubernetes.io/proxy-body-size: "8m" + # # -- Provide CORS allowed origin. + # nginx.ingress.kubernetes.io/cors-allow-origin: "https://*.example.org" + tls: + # -- Provide tls secret. + - secretName: "" + # -- Provide host for tls secret. + hosts: + - "" + hosts: + # -- Provide default path for the ingress record. + - host: "" + paths: + - path: "/api/dim" + pathType: "Prefix" + backend: + port: 8080 + +portContainer: 8080 + +portService: 8080 + +replicaCount: 3 + +# -- Node labels for pod assignment +nodeSelector: {} + +# -- Tolerations for pod assignment +tolerations: [] + +affinity: +# -- Following Catena-X Helm Best Practices, +# [reference](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity). + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: DoesNotExist + topologyKey: kubernetes.io/hostname + +updateStrategy: +# -- Update strategy type, +# rolling update configuration parameters, +# [reference](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies). + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + +# -- Following Catena-X Helm Best Practices, +# [reference](https://github.com/helm/charts/blob/master/stable/nginx-ingress/values.yaml#L210). +startupProbe: + failureThreshold: 30 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 +livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 10 +readinessProbe: + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 diff --git a/consortia/argocd-app-templates/appsetup-dev.yaml b/consortia/argocd-app-templates/appsetup-dev.yaml new file mode 100644 index 0000000..56d22ad --- /dev/null +++ b/consortia/argocd-app-templates/appsetup-dev.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/dim-client.git' + targetRevision: main + plugin: + env: + - name: AVP_SECRET + value: vault-secret + - name: helm_args + value: '-f values.yaml -f ../../consortia/environments/values-dev.yaml' + project: project-portal + syncPolicy: + automated: + prune: true diff --git a/consortia/argocd-app-templates/appsetup-int.yaml b/consortia/argocd-app-templates/appsetup-int.yaml new file mode 100644 index 0000000..14304c1 --- /dev/null +++ b/consortia/argocd-app-templates/appsetup-int.yaml @@ -0,0 +1,39 @@ +############################################################### +# 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/dim-client.git' + targetRevision: dim-1.0.0 + plugin: + env: + - name: AVP_SECRET + value: vault-secret + - name: helm_args + value: '-f values.yaml -f ../../consortia/environments/values-int.yaml' + project: project-portal diff --git a/consortia/environments/values-dev.yaml b/consortia/environments/values-dev.yaml new file mode 100644 index 0000000..acfb6c9 --- /dev/null +++ b/consortia/environments/values-dev.yaml @@ -0,0 +1,101 @@ +############################################################### +# 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.dev.demo.catena-x.net" + hosts: + - host: "dim.dev.demo.catena-x.net" + paths: + - path: "/api/dim" + pathType: "Prefix" + backend: + port: 8080 + +dim: + image: + tag: "main" + imagePullPolicy: "Always" + swaggerEnabled: true + rootDirectoryId: "27fee02a-e265-4cfc-af70-4f217a33840b" + 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-int-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.dev.demo.catena-x.net/auth/realms/CX-Central/protocol/openid-connect/token" + # -- Url to the cf service api + baseAddress: "https://portal-backend.dev.demo.catena-x.net/api/administration/registration/dim/" + +idp: + address: "https://centralidp.dev.demo.catena-x.net" + jwtBearerOptions: + tokenValidationParameters: + validAudience: "DIM-Middle-Layer" + +postgresql: + auth: + postgrespassword: "" + password: "" + replicationPassword: "" diff --git a/consortia/environments/values-int.yaml b/consortia/environments/values-int.yaml new file mode 100644 index 0000000..bd667bb --- /dev/null +++ b/consortia/environments/values-int.yaml @@ -0,0 +1,92 @@ +############################################################### +# 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://*.int.demo.catena-x.net" + tls: + - secretName: "tls-secret" + hosts: + - "dim.int.demo.catena-x.net" + hosts: + - host: "dim.int.demo.catena-x.net" + paths: + - path: "/api/dim" + pathType: "Prefix" + backend: + port: 8080 + +dim: + swaggerEnabled: true + rootDirectoryId: "27fee02a-e265-4cfc-af70-4f217a33840b" + operatorId: "27fee02a-e265-4cfc-af70-4f217a33840b" + +migrations: + logging: + default: "Debug" + +processesworker: + logging: + default: "Debug" + dim: + adminMail: "phil.schneider@digitalnativesolutions.de" + clientIdCisCentral: "" + clientSecretCisCentral: "" + authUrl: "https://catena-x-int-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.int.demo.catena-x.net/auth/realms/CX-Central/protocol/openid-connect/token" + # -- Url to the cf service api + baseAddress: "https://portal-backend.dev.demo.catena-x.net/api/administration/registration/dim/" + +idp: + address: "https://centralidp.int.demo.catena-x.net" + jwtBearerOptions: + tokenValidationParameters: + validAudience: "DIM-Middle-Layer" + +postgresql: + auth: + postgrespassword: "" + password: "" + replicationPassword: "" diff --git a/docker/Dockerfile-dim-migrations b/docker/Dockerfile-dim-migrations new file mode 100644 index 0000000..0e5ec7e --- /dev/null +++ b/docker/Dockerfile-dim-migrations @@ -0,0 +1,36 @@ +############################################################### +# 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 +############################################################### + +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS publish +ARG TARGETARCH +WORKDIR / +COPY LICENSE / +COPY /src/database /src/database +COPY /src/processes/Processes.Worker.Library /src/processes/Processes.Worker.Library +WORKDIR /src/database/Dim.Migrations +RUN dotnet publish "Dim.Migrations.csproj" -c Release -o /migrations/publish + +FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine +ENV COMPlus_EnableDiagnostics=0 +WORKDIR /migrations +COPY --from=publish /migrations/publish . +RUN chown -R 1000:3000 /migrations +USER 1000:3000 +ENTRYPOINT ["dotnet", "Dim.Migrations.dll"] diff --git a/docker/Dockerfile-dim-processes-worker b/docker/Dockerfile-dim-processes-worker new file mode 100644 index 0000000..6703460 --- /dev/null +++ b/docker/Dockerfile-dim-processes-worker @@ -0,0 +1,36 @@ +īģŋ############################################################### +# 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 +############################################################### + +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS publish +ARG TARGETARCH +WORKDIR / +COPY LICENSE / +COPY src/ src/ +RUN dotnet restore "src/processes/Processes.Worker/Processes.Worker.csproj" +WORKDIR /src/processes/Processes.Worker +RUN dotnet publish "Processes.Worker.csproj" -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine +ENV COMPlus_EnableDiagnostics=0 +WORKDIR /app +COPY --from=publish /app/publish . +RUN chown -R 1000:3000 /app +USER 1000:3000 +ENTRYPOINT ["dotnet", "Processes.Worker.dll"] diff --git a/docker/Dockerfile-dim-service b/docker/Dockerfile-dim-service new file mode 100644 index 0000000..21ab67b --- /dev/null +++ b/docker/Dockerfile-dim-service @@ -0,0 +1,38 @@ +############################################################### +# 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 +############################################################### + +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS publish +ARG TARGETARCH +WORKDIR / +COPY LICENSE / +COPY src/ src/ +WORKDIR /src/web/Dim.Web +RUN dotnet publish "Dim.Web.csproj" -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine +ENV \ + COMPlus_EnableDiagnostics=0 \ + ASPNETCORE_URLS=http://+:8080 +WORKDIR /app +COPY --from=publish /app/publish . +EXPOSE 8080 +RUN chown -R 1000:3000 /app +USER 1000:3000 +ENTRYPOINT ["dotnet", "Dim.Web.dll"] diff --git a/src/Dim.sln b/src/Dim.sln new file mode 100644 index 0000000..90ca4c2 --- /dev/null +++ b/src/Dim.sln @@ -0,0 +1,127 @@ +īģŋ +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "processes", "processes", "{30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "clients", "clients", "{B84A3CAB-AC86-4B2D-A490-79E1002350FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.Clients", "clients\Dim.Clients\Dim.Clients.csproj", "{8356C7AF-6F88-4A62-B3E9-5656634A6FEA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Library", "processes\Processes.Library\Processes.Library.csproj", "{BFDDFCF5-8F35-4348-A568-57D8E34DF289}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Worker", "processes\Processes.Worker\Processes.Worker.csproj", "{E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Worker.Library", "processes\Processes.Worker.Library\Processes.Worker.Library.csproj", "{4F40B8F2-12D4-403B-B666-8F0540354455}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "database", "database", "{4A305F87-23B4-4929-B138-4B7D2710526F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.DbAccess", "database\Dim.DbAccess\Dim.DbAccess.csproj", "{6984539B-7B5A-481E-A955-3AE537D43166}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.Entities", "database\Dim.Entities\Dim.Entities.csproj", "{0A85DB84-41A0-4EE0-815B-D922329A8BCA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DimProcess.Executor", "processes\DimProcess.Executor\DimProcess.Executor.csproj", "{41AA384E-E433-440E-A02E-83F03AC98A63}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DimProcess.Library", "processes\DimProcess.Library\DimProcess.Library.csproj", "{F14C5E02-DC54-4A27-B2DE-513A644E6473}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.Migrations", "database\Dim.Migrations\Dim.Migrations.csproj", "{1621A06B-9AF2-421C-BB48-08028560E108}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "web", "web", "{D20B95EF-D7D9-45BA-B5A4-C8D3B64AC745}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.Web", "web\Dim.Web\Dim.Web.csproj", "{1B50DC13-DB8A-41CF-9D40-A8740B068AC2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{CB1B7D43-9AFC-47EF-8915-A547F7F553AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Library.Tests", "..\tests\processes\Processes.Library.Tests\Processes.Library.Tests.csproj", "{AA29EECC-CD77-4452-B48E-CC038E4FBB69}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Worker.Library.Tests", "..\tests\processes\Processes.Worker.Library.Tests\Processes.Worker.Library.Tests.csproj", "{9888C703-CC13-47EB-89F0-B8A34D64BD07}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Shared", "..\tests\shared\Tests.Shared\Tests.Shared.csproj", "{0D288AF0-1CE5-4B2B-9F80-532040F24BCF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DimProcess.Executor.Tests", "..\tests\processes\DimProcess.Executor.Tests\DimProcess.Executor.Tests.csproj", "{A44447B0-794D-451A-A571-E3B761174B48}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DimProcess.Library.Tests", "..\tests\processes\DimProcess.Library.Tests\DimProcess.Library.Tests.csproj", "{85D316A0-17BE-4983-AB06-5C72365ABD9B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8356C7AF-6F88-4A62-B3E9-5656634A6FEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8356C7AF-6F88-4A62-B3E9-5656634A6FEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8356C7AF-6F88-4A62-B3E9-5656634A6FEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8356C7AF-6F88-4A62-B3E9-5656634A6FEA}.Release|Any CPU.Build.0 = Release|Any CPU + {BFDDFCF5-8F35-4348-A568-57D8E34DF289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFDDFCF5-8F35-4348-A568-57D8E34DF289}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFDDFCF5-8F35-4348-A568-57D8E34DF289}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFDDFCF5-8F35-4348-A568-57D8E34DF289}.Release|Any CPU.Build.0 = Release|Any CPU + {E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2}.Release|Any CPU.Build.0 = Release|Any CPU + {4F40B8F2-12D4-403B-B666-8F0540354455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F40B8F2-12D4-403B-B666-8F0540354455}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F40B8F2-12D4-403B-B666-8F0540354455}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F40B8F2-12D4-403B-B666-8F0540354455}.Release|Any CPU.Build.0 = Release|Any CPU + {6984539B-7B5A-481E-A955-3AE537D43166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6984539B-7B5A-481E-A955-3AE537D43166}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6984539B-7B5A-481E-A955-3AE537D43166}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6984539B-7B5A-481E-A955-3AE537D43166}.Release|Any CPU.Build.0 = Release|Any CPU + {0A85DB84-41A0-4EE0-815B-D922329A8BCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A85DB84-41A0-4EE0-815B-D922329A8BCA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A85DB84-41A0-4EE0-815B-D922329A8BCA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A85DB84-41A0-4EE0-815B-D922329A8BCA}.Release|Any CPU.Build.0 = Release|Any CPU + {41AA384E-E433-440E-A02E-83F03AC98A63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41AA384E-E433-440E-A02E-83F03AC98A63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41AA384E-E433-440E-A02E-83F03AC98A63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41AA384E-E433-440E-A02E-83F03AC98A63}.Release|Any CPU.Build.0 = Release|Any CPU + {F14C5E02-DC54-4A27-B2DE-513A644E6473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F14C5E02-DC54-4A27-B2DE-513A644E6473}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F14C5E02-DC54-4A27-B2DE-513A644E6473}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F14C5E02-DC54-4A27-B2DE-513A644E6473}.Release|Any CPU.Build.0 = Release|Any CPU + {1621A06B-9AF2-421C-BB48-08028560E108}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1621A06B-9AF2-421C-BB48-08028560E108}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1621A06B-9AF2-421C-BB48-08028560E108}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1621A06B-9AF2-421C-BB48-08028560E108}.Release|Any CPU.Build.0 = Release|Any CPU + {1B50DC13-DB8A-41CF-9D40-A8740B068AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B50DC13-DB8A-41CF-9D40-A8740B068AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B50DC13-DB8A-41CF-9D40-A8740B068AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B50DC13-DB8A-41CF-9D40-A8740B068AC2}.Release|Any CPU.Build.0 = Release|Any CPU + {AA29EECC-CD77-4452-B48E-CC038E4FBB69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA29EECC-CD77-4452-B48E-CC038E4FBB69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA29EECC-CD77-4452-B48E-CC038E4FBB69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA29EECC-CD77-4452-B48E-CC038E4FBB69}.Release|Any CPU.Build.0 = Release|Any CPU + {9888C703-CC13-47EB-89F0-B8A34D64BD07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9888C703-CC13-47EB-89F0-B8A34D64BD07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9888C703-CC13-47EB-89F0-B8A34D64BD07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9888C703-CC13-47EB-89F0-B8A34D64BD07}.Release|Any CPU.Build.0 = Release|Any CPU + {0D288AF0-1CE5-4B2B-9F80-532040F24BCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D288AF0-1CE5-4B2B-9F80-532040F24BCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D288AF0-1CE5-4B2B-9F80-532040F24BCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D288AF0-1CE5-4B2B-9F80-532040F24BCF}.Release|Any CPU.Build.0 = Release|Any CPU + {A44447B0-794D-451A-A571-E3B761174B48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A44447B0-794D-451A-A571-E3B761174B48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A44447B0-794D-451A-A571-E3B761174B48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A44447B0-794D-451A-A571-E3B761174B48}.Release|Any CPU.Build.0 = Release|Any CPU + {85D316A0-17BE-4983-AB06-5C72365ABD9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85D316A0-17BE-4983-AB06-5C72365ABD9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85D316A0-17BE-4983-AB06-5C72365ABD9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85D316A0-17BE-4983-AB06-5C72365ABD9B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8356C7AF-6F88-4A62-B3E9-5656634A6FEA} = {B84A3CAB-AC86-4B2D-A490-79E1002350FF} + {BFDDFCF5-8F35-4348-A568-57D8E34DF289} = {30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2} + {E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2} = {30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2} + {4F40B8F2-12D4-403B-B666-8F0540354455} = {30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2} + {6984539B-7B5A-481E-A955-3AE537D43166} = {4A305F87-23B4-4929-B138-4B7D2710526F} + {0A85DB84-41A0-4EE0-815B-D922329A8BCA} = {4A305F87-23B4-4929-B138-4B7D2710526F} + {41AA384E-E433-440E-A02E-83F03AC98A63} = {30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2} + {F14C5E02-DC54-4A27-B2DE-513A644E6473} = {30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2} + {1621A06B-9AF2-421C-BB48-08028560E108} = {4A305F87-23B4-4929-B138-4B7D2710526F} + {1B50DC13-DB8A-41CF-9D40-A8740B068AC2} = {D20B95EF-D7D9-45BA-B5A4-C8D3B64AC745} + {AA29EECC-CD77-4452-B48E-CC038E4FBB69} = {CB1B7D43-9AFC-47EF-8915-A547F7F553AB} + {9888C703-CC13-47EB-89F0-B8A34D64BD07} = {CB1B7D43-9AFC-47EF-8915-A547F7F553AB} + {0D288AF0-1CE5-4B2B-9F80-532040F24BCF} = {CB1B7D43-9AFC-47EF-8915-A547F7F553AB} + {A44447B0-794D-451A-A571-E3B761174B48} = {CB1B7D43-9AFC-47EF-8915-A547F7F553AB} + {85D316A0-17BE-4983-AB06-5C72365ABD9B} = {CB1B7D43-9AFC-47EF-8915-A547F7F553AB} + EndGlobalSection +EndGlobal diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..b753620 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,26 @@ + + + + + 1.0.0 + + + diff --git a/src/clients/Dim.Clients/Api/Cf/AddSpaceRoleToUserRequest.cs b/src/clients/Dim.Clients/Api/Cf/AddSpaceRoleToUserRequest.cs new file mode 100644 index 0000000..b165b63 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/AddSpaceRoleToUserRequest.cs @@ -0,0 +1,50 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Cf; + +public record AddSpaceRoleToUserRequest( + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("relationships")] SpaceRoleRelationship Relationship +); + +public record SpaceRoleRelationship( + [property: JsonPropertyName("user")] RelationshipUser User, + [property: JsonPropertyName("space")] SpaceRoleSpace Space +); + +public record RelationshipUser( + [property: JsonPropertyName("data")] UserData Data +); + +public record UserData( + [property: JsonPropertyName("username")] string Username, + [property: JsonPropertyName("origin")] string Origin +); + +public record SpaceRoleSpace( + [property: JsonPropertyName("data")] SpaceRoleData Data +); + +public record SpaceRoleData( + [property: JsonPropertyName("guid")] Guid Id +); diff --git a/src/clients/Dim.Clients/Api/Cf/CfClient.cs b/src/clients/Dim.Clients/Api/Cf/CfClient.cs new file mode 100644 index 0000000..c77dc3a --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/CfClient.cs @@ -0,0 +1,279 @@ +/******************************************************************************** + * 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.DependencyInjection; +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Dim.Clients.Api.Cf; + +public class CfClient : ICfClient +{ + private readonly CfSettings _settings; + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public CfClient(IBasicAuthTokenService basicAuthTokenService, IOptions settings) + { + _basicAuthTokenService = basicAuthTokenService; + _settings = settings.Value; + } + + public async Task CreateCloudFoundrySpace(string tenantName, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var cfEnvironmentId = await GetEnvironmentId(tenantName, cancellationToken, client).ConfigureAwait(false); + var data = new CreateSpaceRequest( + $"{tenantName}-space", + new SpaceRelationship(new SpaceOrganization(new SpaceRelationshipData(cfEnvironmentId))) + ); + + var result = await client.PostAsJsonAsync("/v3/spaces", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-cfe", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + return response.Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + private static async Task GetEnvironmentId(string tenantName, CancellationToken cancellationToken, HttpClient client) + { + var environmentsResponse = await client.GetAsync("/v3/organizations", cancellationToken) + .CatchingIntoServiceExceptionFor("get-organizations", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE); + var environments = await environmentsResponse.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + var tenantEnvironment = environments.Resources.Where(x => x.Name == tenantName); + if (tenantEnvironment.Count() > 1) + { + throw new ConflictException($"There should only be one cf environment for tenant {tenantName}"); + } + + return tenantEnvironment.Single().EnvironmentId; + } + + public async Task AddSpaceRoleToUser(string type, string user, Guid spaceId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var data = new AddSpaceRoleToUserRequest( + type, + new SpaceRoleRelationship( + new RelationshipUser(new UserData(user, "sap.ids")), + new SpaceRoleSpace(new SpaceRoleData(spaceId)) + ) + ); + + await client.PostAsJsonAsync("/v3/roles", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("add-space-roles", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + } + + public async Task GetServicePlan(string servicePlanName, string servicePlanType, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync("/v3/service_plans", cancellationToken) + .CatchingIntoServiceExceptionFor("get-service-plan", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException("response should never be null here"); + } + + var servicePlans = response.Resources.Where(x => x.Name == servicePlanType && + x.BrokerCatalog?.BrokerCatalogMetadata?.AutoSubscription?.AppName == servicePlanName); + if (servicePlans.Count() != 1) + { + throw new ServiceException($"There must be exactly one service plan with name {servicePlanName} and type {servicePlanType}", isRecoverable: !servicePlans.Any()); + } + + return servicePlans.Single().Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task GetSpace(string tenantName, CancellationToken cancellationToken) + { + var spaceName = $"{tenantName}-space"; + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync("/v3/spaces", cancellationToken) + .CatchingIntoServiceExceptionFor("get-space", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException("response should never be null here"); + } + + var spaces = response.Resources.Where(x => x.Name == spaceName); + if (spaces.Count() != 1) + { + throw new ServiceException($"There must be exactly one space with name {spaceName}", isRecoverable: !spaces.Any()); + } + + return spaces.Single().Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task CreateDimServiceInstance(string tenantName, Guid spaceId, Guid servicePlanId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var data = new CreateDimServiceInstance( + "managed", + $"{tenantName}-dim-instance", + new DimRelationships( + new DimSpace(new DimData(spaceId)), + new DimServicePlan(new DimData(servicePlanId))) + ); + + await client.PostAsJsonAsync("/v3/service_instances", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-dim-si", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + } + + private async Task GetServiceInstances(string tenantName, Guid? spaceId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync("/v3/service_instances", cancellationToken) + .CatchingIntoServiceExceptionFor("get-si", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + var name = $"{tenantName}-dim-instance"; + var resources = response.Resources.Where(x => x.Name == name && x.Type == "managed" && (spaceId == null || x.Relationships.Space.Data.Id == spaceId.Value) && x.LastOperation.State == "succeeded"); + if (resources.Count() != 1) + { + throw new ServiceException("There must be exactly one service instance", isRecoverable: !resources.Any()); + } + + return resources.Single().Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task CreateServiceInstanceBindings(string tenantName, Guid spaceId, CancellationToken cancellationToken) + { + var serviceInstanceId = await GetServiceInstances(tenantName, spaceId, cancellationToken).ConfigureAwait(false); + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var data = new CreateServiceCredentialBindingRequest( + "key", + $"{tenantName}-dim-key01", + new ServiceCredentialRelationships( + new DimServiceInstance(new DimData(serviceInstanceId))) + ); + await client.PostAsJsonAsync("/v3/service_credential_bindings", data, JsonSerializerOptions.Default, cancellationToken) + .CatchingIntoServiceExceptionFor("create-si-bindings", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS); + } + + public async Task GetServiceBinding(string tenantName, Guid spaceId, string bindingName, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var serviceInstanceId = await GetServiceInstances(tenantName, spaceId, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync($"/v3/service_credential_bindings?names={bindingName}", cancellationToken) + .CatchingIntoServiceExceptionFor("get-credential-binding", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + var resources = response.Resources.Where(x => x.Relationships.ServiceInstance.Data.Id == serviceInstanceId); + if (resources.Count() != 1) + { + throw new ServiceException("There must be exactly one service credential binding", isRecoverable: !resources.Any()); + } + + return resources.Single().Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task GetServiceBindingDetails(Guid id, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync($"/v3/service_credential_bindings/{id}/details", cancellationToken) + .CatchingIntoServiceExceptionFor("get-credential-binding-name", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException("There must be exactly one service instance", isRecoverable: true); + } + + return response; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } +} diff --git a/src/clients/Dim.Clients/Api/Cf/CreateCfeRequest.cs b/src/clients/Dim.Clients/Api/Cf/CreateCfeRequest.cs new file mode 100644 index 0000000..d832e4e --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/CreateCfeRequest.cs @@ -0,0 +1,53 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Cf; + +public record CreateSpaceRequest( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("relationships")] SpaceRelationship Relationship +); + +public record SpaceRelationship( + [property: JsonPropertyName("organization")] SpaceOrganization Organization +); + +public record SpaceOrganization( + [property: JsonPropertyName("data")] SpaceRelationshipData Data +); + +public record SpaceRelationshipData( + [property: JsonPropertyName("guid")] Guid Id +); + +public record CreateSpaceResponse( + [property: JsonPropertyName("guid")] Guid Id +); + +public record GetEnvironmentsResponse( + [property: JsonPropertyName("resources")] IEnumerable Resources +); + +public record EnvironmentResource( + [property: JsonPropertyName("guid")] Guid EnvironmentId, + [property: JsonPropertyName("name")] string Name +); diff --git a/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfClientServiceExtensions.cs new file mode 100644 index 0000000..dba6419 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfClientServiceExtensions.cs @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Dim.Clients.Api.Cf.DependencyInjection; + +public static class CfClientServiceExtensions +{ + public static IServiceCollection AddCfClient(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + services + .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, settings.Value.TokenAddress) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfSettings.cs b/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfSettings.cs new file mode 100644 index 0000000..ebe728d --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfSettings.cs @@ -0,0 +1,30 @@ +/******************************************************************************** + * 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.Token; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Api.Cf.DependencyInjection; + +public class CfSettings : BasicAuthSettings +{ + [Required] + public string BaseUrl { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Api/Cf/ICfClient.cs b/src/clients/Dim.Clients/Api/Cf/ICfClient.cs new file mode 100644 index 0000000..f11a4bf --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/ICfClient.cs @@ -0,0 +1,33 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Clients.Api.Cf; + +public interface ICfClient +{ + Task CreateCloudFoundrySpace(string tenantName, CancellationToken cancellationToken); + Task AddSpaceRoleToUser(string type, string user, Guid spaceId, CancellationToken cancellationToken); + Task GetServicePlan(string servicePlanName, string servicePlanType, CancellationToken cancellationToken); + Task GetSpace(string tenantName, CancellationToken cancellationToken); + Task CreateDimServiceInstance(string tenantName, Guid spaceId, Guid servicePlanId, CancellationToken cancellationToken); + Task CreateServiceInstanceBindings(string tenantName, Guid spaceId, CancellationToken cancellationToken); + Task GetServiceBinding(string tenantName, Guid spaceId, string bindingName, CancellationToken cancellationToken); + Task GetServiceBindingDetails(Guid id, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Cf/ServicePlanResponse.cs b/src/clients/Dim.Clients/Api/Cf/ServicePlanResponse.cs new file mode 100644 index 0000000..956834b --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/ServicePlanResponse.cs @@ -0,0 +1,140 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Cf; + +public record ServicePlanResponse( + [property: JsonPropertyName("resources")] IEnumerable Resources +); + +public record ServicePlanResources( + [property: JsonPropertyName("guid")] Guid Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("broker_catalog")] BrokerCatalog? BrokerCatalog +); + +public record BrokerCatalog( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("metadata")] BrokerCatalogMetadata? BrokerCatalogMetadata +); + +public record BrokerCatalogMetadata( + [property: JsonPropertyName("auto_subscription")] AutoSupscription? AutoSubscription +); + +public record AutoSupscription( + [property: JsonPropertyName("app_name")] string? AppName +); + +public record ServiceInstanceResponse( + [property: JsonPropertyName("resources")] IEnumerable Resources +); + +public record ServiceInstanceResource( + [property: JsonPropertyName("guid")] Guid Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("relationships")] ServiceInstanceRelationship Relationships, + [property: JsonPropertyName("last_operation")] LastOperation LastOperation +); + +public record ServiceInstanceRelationship( + [property: JsonPropertyName("space")] ServiceInstanceRelationshipSpace Space +); + +public record LastOperation( + [property: JsonPropertyName("state")] string State +); + +public record ServiceInstanceRelationshipSpace( + [property: JsonPropertyName("data")] DimData Data +); + +public record CreateServiceCredentialBindingRequest( + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("relationships")] ServiceCredentialRelationships Relationships +); + +public record ServiceCredentialRelationships( + [property: JsonPropertyName("service_instance")] DimServiceInstance ServiceInstance +); + +public record DimServiceInstance( + [property: JsonPropertyName("data")] DimData Data +); + +public record CreateDimServiceInstance( + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("relationships")] DimRelationships Relationships +); + +public record DimRelationships( + [property: JsonPropertyName("space")] DimSpace Space, + [property: JsonPropertyName("service_plan")] DimServicePlan ServicePlan +); + +public record DimServicePlan( + [property: JsonPropertyName("data")] DimData Data +); + +public record DimSpace( + [property: JsonPropertyName("data")] DimData Data +); + +public record DimData( + [property: JsonPropertyName("guid")] Guid Id +); + +public record ServiceCredentialBindingResponse( + [property: JsonPropertyName("resources")] IEnumerable Resources +); + +public record ServiceCredentialBindingRelationships( + [property: JsonPropertyName("service_instance")] ScbServiceInstnace ServiceInstance +); + +public record ScbServiceInstnace( + [property: JsonPropertyName("data")] DimData Data +); + +public record ServiceCredentialBindingResource( + [property: JsonPropertyName("guid")] Guid Id, + [property: JsonPropertyName("relationships")] ServiceCredentialBindingRelationships Relationships +); + +public record ServiceCredentialBindingDetailResponse( + [property: JsonPropertyName("credentials")] Credentials Credentials +); + +public record Credentials( + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("uaa")] Uaa Uaa +); + +public record Uaa( + [property: JsonPropertyName("clientid")] string ClientId, + [property: JsonPropertyName("clientsecret")] string ClientSecret, + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("apiurl")] string ApiUrl +); diff --git a/src/clients/Dim.Clients/Api/Cf/SpaceResponse.cs b/src/clients/Dim.Clients/Api/Cf/SpaceResponse.cs new file mode 100644 index 0000000..21f9234 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/SpaceResponse.cs @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Cf; + +public record SpaceResponse( + [property: JsonPropertyName("resources")] IEnumerable Resources +); + +public record Space( + [property: JsonPropertyName("guid")] Guid Id, + [property: JsonPropertyName("name")] string Name +); diff --git a/src/clients/Dim.Clients/Api/Dim/ApplicationResponse.cs b/src/clients/Dim.Clients/Api/Dim/ApplicationResponse.cs new file mode 100644 index 0000000..9db87fc --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/ApplicationResponse.cs @@ -0,0 +1,31 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Dim; + +public record ApplicationResponse( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("application")] string Application, + [property: JsonPropertyName("applicationKey")] string ApplicaitonKey, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("colorAccent")] int ColorAccent +); diff --git a/src/clients/Dim.Clients/Api/Dim/CompanyIdentityPatch.cs b/src/clients/Dim.Clients/Api/Dim/CompanyIdentityPatch.cs new file mode 100644 index 0000000..f66264e --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/CompanyIdentityPatch.cs @@ -0,0 +1,31 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Dim; + +public record CompanyIdentityPatch( + [property: JsonPropertyName("applicationUpdates")] ApplicationUpdates ApplicationUpdates +); + +public record ApplicationUpdates( + [property: JsonPropertyName("assignApplications")] IEnumerable AssignApplications +); diff --git a/src/clients/Dim.Clients/Api/Dim/CreateApplicationRequest.cs b/src/clients/Dim.Clients/Api/Dim/CreateApplicationRequest.cs new file mode 100644 index 0000000..5ffbecc --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/CreateApplicationRequest.cs @@ -0,0 +1,37 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Dim; + +public record CreateApplicationRequest( + [property: JsonPropertyName("payload")] ApplicationPayload Payload +); + +public record ApplicationPayload( + [property: JsonPropertyName("application")] string Application, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("colorAccent")] int ColorAccent +); + +public record CreateApplicationResponse( + [property: JsonPropertyName("id")] string Id +); diff --git a/src/clients/Dim.Clients/Api/Dim/CreateCompanyIdentityRequest.cs b/src/clients/Dim.Clients/Api/Dim/CreateCompanyIdentityRequest.cs new file mode 100644 index 0000000..03a7175 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/CreateCompanyIdentityRequest.cs @@ -0,0 +1,59 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Dim; + +public record CreateCompanyIdentityRequest( + [property: JsonPropertyName("payload")] Payload Payload +); + +public record Payload( + [property: JsonPropertyName("hostingUrl")] string HostingUrl, + [property: JsonPropertyName("bootstrap")] Bootstrap Bootstrap, + [property: JsonPropertyName("keys")] IEnumerable Keys +); + +public record Service( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("type")] string Type +); + +public record Bootstrap( + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("protocols")] IEnumerable Protocols +); + +public record Key( + [property: JsonPropertyName("type")] string Type +); + +public record Network( + [property: JsonPropertyName("didMethod")] string DidMethod, + [property: JsonPropertyName("type")] string Type +); + +public record CreateCompanyIdentityResponse( + [property: JsonPropertyName("did")] string Did, + [property: JsonPropertyName("companyId")] Guid CompanyId, + [property: JsonPropertyName("downloadURL")] string DownloadUrl +); diff --git a/src/clients/Dim.Clients/Api/Dim/DependencyInjection/DimClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Dim/DependencyInjection/DimClientServiceExtensions.cs new file mode 100644 index 0000000..7dcc39f --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/DependencyInjection/DimClientServiceExtensions.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.Clients.Api.Dim.DependencyInjection; + +public static class DimClientServiceExtensions +{ + public static IServiceCollection AddDimClient(this IServiceCollection services) + { + services + .AddCustomHttpClientWithAuthentication(null, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Dim/DimClient.cs b/src/clients/Dim.Clients/Api/Dim/DimClient.cs new file mode 100644 index 0000000..dccd8a1 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/DimClient.cs @@ -0,0 +1,215 @@ +/******************************************************************************** + * 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.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Dim.Clients.Api.Dim; + +public class DimClient : IDimClient +{ + private readonly IBasicAuthTokenService _basicAuthTokenService; + private readonly IHttpClientFactory _clientFactory; + + public DimClient(IBasicAuthTokenService basicAuthTokenService, IHttpClientFactory clientFactory) + { + _basicAuthTokenService = basicAuthTokenService; + _clientFactory = clientFactory; + } + + public async Task CreateCompanyIdentity(BasicAuthSettings dimBasicAuth, string hostingUrl, string baseUrl, string tenantName, bool isIssuer, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(dimBasicAuth, cancellationToken).ConfigureAwait(false); + var data = new CreateCompanyIdentityRequest(new Payload( + hostingUrl, + new Bootstrap("Holder with IATP", "Holder IATP", Enumerable.Repeat("IATP", 1)), + isIssuer ? + Enumerable.Empty() : + new Key[] + { + new("SIGNING"), + new("SIGNING_VC"), + })); + var result = await client.PostAsJsonAsync($"{baseUrl}/api/v2.0.0/companyIdentities", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-company-identity", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async m => + { + var message = await m.Content.ReadAsStringAsync().ConfigureAwait(false); + return (false, message); + }).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response was empty", true); + } + + return response; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task GetDidDocument(string url, CancellationToken cancellationToken) + { + var client = _clientFactory.CreateClient("didDocumentDownload"); + using var result = await client.GetStreamAsync(url, cancellationToken).ConfigureAwait(false); + var document = await JsonDocument.ParseAsync(result, cancellationToken: cancellationToken).ConfigureAwait(false); + return document; + } + + public async Task CreateApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string tenantName, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(dimAuth, cancellationToken).ConfigureAwait(false); + var data = new CreateApplicationRequest(new ApplicationPayload( + "catena-x-portal", + $"Catena-X Portal MIW for {tenantName}", + 6)); + var result = await client.PostAsJsonAsync($"{dimBaseUrl}/api/v2.0.0/applications", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async m => + { + var message = await m.Content.ReadAsStringAsync().ConfigureAwait(false); + return (false, message); + }).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response was empty", true); + } + + return response.Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task GetApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(dimAuth, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync($"{dimBaseUrl}/api/v2.0.0/applications/{applicationId}", cancellationToken) + .CatchingIntoServiceExceptionFor("get-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async m => + { + var message = await m.Content.ReadAsStringAsync().ConfigureAwait(false); + return (false, message); + }).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + return response.ApplicaitonKey; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task AssignApplicationToCompany(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationKey, Guid companyId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(dimAuth, cancellationToken).ConfigureAwait(false); + var data = new CompanyIdentityPatch(new ApplicationUpdates(Enumerable.Repeat(applicationKey, 1))); + await client.PatchAsJsonAsync($"{dimBaseUrl}/api/v2.0.0/companyIdentities/{companyId}", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("assign-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async m => + { + var message = await m.Content.ReadAsStringAsync().ConfigureAwait(false); + return (false, message); + }).ConfigureAwait(false); + } + + public async Task GetStatusList(BasicAuthSettings dimAuth, string dimBaseUrl, Guid companyId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(dimAuth, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync($"{dimBaseUrl}/api/v2.0.0/companyIdentities/{companyId}/revocationLists", cancellationToken); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + if (!response.Data.Any(x => x.RemainingSpace > 0)) + { + throw new ConflictException("There is no status list with remaining space, please create a new one."); + } + + return response.Data.First(x => x.RemainingSpace > 0).StatusListCredential; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task CreateStatusList(BasicAuthSettings dimAuth, string dimBaseUrl, Guid companyId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(dimAuth, cancellationToken).ConfigureAwait(false); + var data = new CreateStatusListRequest(new CreateStatusListPaypload(new CreateStatusList("StatusList2021", DateTimeOffset.UtcNow.ToString("yyyyMMdd"), "New revocation list", 2097152))); + var result = await client.PostAsJsonAsync($"{dimBaseUrl}/api/v2.0.0/companyIdentities/{companyId}/revocationLists", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("assign-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async m => + { + var message = await m.Content.ReadAsStringAsync().ConfigureAwait(false); + return (false, message); + }).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + return response.RevocationVc.Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } +} diff --git a/src/clients/Dim.Clients/Api/Dim/IDimClient.cs b/src/clients/Dim.Clients/Api/Dim/IDimClient.cs new file mode 100644 index 0000000..151969d --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/IDimClient.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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.Token; +using System.Text.Json; + +namespace Dim.Clients.Api.Dim; + +public interface IDimClient +{ + Task CreateCompanyIdentity(BasicAuthSettings dimBasicAuth, string hostingUrl, string baseUrl, string tenantName, bool isIssuer, CancellationToken cancellationToken); + Task GetDidDocument(string url, CancellationToken cancellationToken); + Task CreateApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string tenantName, CancellationToken cancellationToken); + + Task GetApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationId, CancellationToken cancellationToken); + Task AssignApplicationToCompany(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationKey, Guid companyId, CancellationToken cancellationToken); + Task GetStatusList(BasicAuthSettings dimAuth, string dimBaseUrl, Guid companyId, CancellationToken cancellationToken); + Task CreateStatusList(BasicAuthSettings dimAuth, string dimBaseUrl, Guid companyId, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Dim/StatusListResponse.cs b/src/clients/Dim.Clients/Api/Dim/StatusListResponse.cs new file mode 100644 index 0000000..fa3eb40 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/StatusListResponse.cs @@ -0,0 +1,61 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Dim; + +public record CreateStatusListRequest( + [property: JsonPropertyName("payload")] CreateStatusListPaypload Payload +); + +public record CreateStatusListPaypload( + [property: JsonPropertyName("create")] CreateStatusList Create +); + +public record CreateStatusList( + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("length")] int Length +); + +public record CreateStatusListResponse( + [property: JsonPropertyName("id")] Guid Id, + [property: JsonPropertyName("revocationVc")] RevocationVc RevocationVc +); + +public record RevocationVc( + [property: JsonPropertyName("id")] string Id +); + +public record StatusListListResponse( + [property: JsonPropertyName("count")] int Count, + [property: JsonPropertyName("data")] IEnumerable Data +); + +public record StatusListResponse( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("statusListCredential")] string StatusListCredential, + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("length")] int Length, + [property: JsonPropertyName("remainingSpace")] int RemainingSpace +); diff --git a/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectoryClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectoryClientServiceExtensions.cs new file mode 100644 index 0000000..3d5a9e1 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectoryClientServiceExtensions.cs @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Dim.Clients.Api.Directories.DependencyInjection; + +public static class DirectoryClientServiceExtensions +{ + public static IServiceCollection AddDirectoryClient(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + services + .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, settings.Value.TokenAddress) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectorySettings.cs b/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectorySettings.cs new file mode 100644 index 0000000..f4ce443 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectorySettings.cs @@ -0,0 +1,30 @@ +/******************************************************************************** + * 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.Token; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Api.Directories.DependencyInjection; + +public class DirectorySettings : BasicAuthSettings +{ + [Required] + public string BaseUrl { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Api/Directories/DirectoryClient.cs b/src/clients/Dim.Clients/Api/Directories/DirectoryClient.cs new file mode 100644 index 0000000..fd50052 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Directories/DirectoryClient.cs @@ -0,0 +1,76 @@ +/******************************************************************************** + * 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.Directories.DependencyInjection; +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Dim.Clients.Api.Directories; + +public class DirectoryClient : IDirectoryClient +{ + private readonly DirectorySettings _settings; + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public DirectoryClient(IBasicAuthTokenService basicAuthTokenService, IOptions settings) + { + _basicAuthTokenService = basicAuthTokenService; + _settings = settings.Value; + } + + public async Task CreateDirectory(string description, string bpnl, Guid parentId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(_settings, cancellationToken).ConfigureAwait(false); + var directory = new DirectoryRequest( + description, + Enumerable.Repeat("phil.schneider@digitalnativesolutions.de", 1), + bpnl, + new Dictionary>() + { + { "cloud_management_service", new[] { "Created by API - Don't change it" } } + } + ); + + var result = await client.PostAsJsonAsync($"/accounts/v1/directories?parentId={parentId}", directory, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-directory", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException("Directory response must not be null"); + } + + return response.Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } +} diff --git a/src/clients/Dim.Clients/Api/Directories/DirectoryRequest.cs b/src/clients/Dim.Clients/Api/Directories/DirectoryRequest.cs new file mode 100644 index 0000000..64cdd7a --- /dev/null +++ b/src/clients/Dim.Clients/Api/Directories/DirectoryRequest.cs @@ -0,0 +1,35 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Directories; + +public record DirectoryRequest( + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("directoryAdmins")] IEnumerable DirectoryAdmins, + [property: JsonPropertyName("displayName")] string DisplayName, + [property: JsonPropertyName("labels")] Dictionary> Labels +); + +public record DirectoryResponse( + [property: JsonPropertyName("guid")] Guid Id, + [property: JsonPropertyName("subdomain")] string Subdomain +); diff --git a/src/clients/Dim.Clients/Api/Directories/IDirectoryClient.cs b/src/clients/Dim.Clients/Api/Directories/IDirectoryClient.cs new file mode 100644 index 0000000..a145d93 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Directories/IDirectoryClient.cs @@ -0,0 +1,26 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Clients.Api.Directories; + +public interface IDirectoryClient +{ + Task CreateDirectory(string description, string bpnl, Guid parentId, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Entitlements/CreateSubAccountRequest.cs b/src/clients/Dim.Clients/Api/Entitlements/CreateSubAccountRequest.cs new file mode 100644 index 0000000..ce4d5c2 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Entitlements/CreateSubAccountRequest.cs @@ -0,0 +1,39 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Entitlements; + +public record CreateSubAccountRequest( + [property: JsonPropertyName("subaccountServicePlans")] IEnumerable SubaccountServicePlans +); + +public record SubaccountServicePlan( + [property: JsonPropertyName("assignmentInfo")] IEnumerable AssignmentInfo, + [property: JsonPropertyName("serviceName")] string ServiceName, + [property: JsonPropertyName("servicePlanName")] string ServicePlanName +); + +public record AssignmentInfo( + [property: JsonPropertyName("enable")] bool? Enabled, + [property: JsonPropertyName("amount")] int? Amount, + [property: JsonPropertyName("subaccountGUID")] Guid SubaccountId +); diff --git a/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementClientServiceExtensions.cs new file mode 100644 index 0000000..751b551 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementClientServiceExtensions.cs @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Dim.Clients.Api.Entitlements.DependencyInjection; + +public static class EntitlementClientServiceExtensions +{ + public static IServiceCollection AddEntitlementClient(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + services + .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementSettings.cs b/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementSettings.cs new file mode 100644 index 0000000..6fdd346 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementSettings.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * 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.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Api.Entitlements.DependencyInjection; + +public class EntitlementSettings +{ + [Required] + public string BaseUrl { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Api/Entitlements/EntitlementClient.cs b/src/clients/Dim.Clients/Api/Entitlements/EntitlementClient.cs new file mode 100644 index 0000000..8103474 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Entitlements/EntitlementClient.cs @@ -0,0 +1,53 @@ +/******************************************************************************** + * 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.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; + +namespace Dim.Clients.Api.Entitlements; + +public class EntitlementClient : IEntitlementClient +{ + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public EntitlementClient(IBasicAuthTokenService basicAuthTokenService) + { + _basicAuthTokenService = basicAuthTokenService; + } + + public async Task AssignEntitlements(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(basicAuthSettings, cancellationToken).ConfigureAwait(false); + var data = new CreateSubAccountRequest( + new List + { + new(Enumerable.Repeat(new AssignmentInfo(true, null, subAccountId), 1), "cis", "local"), + new(Enumerable.Repeat(new AssignmentInfo(true, null, subAccountId), 1), "decentralized-identity-management-app", "standard"), + new(Enumerable.Repeat(new AssignmentInfo(null, 1, subAccountId), 1), "decentralized-identity-management", "standard"), + new(Enumerable.Repeat(new AssignmentInfo(true, null, subAccountId), 1), "auditlog-viewer", "free") + } + ); + + await client.PutAsJsonAsync("/entitlements/v1/subaccountServicePlans", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("assign-entitlements", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + } +} diff --git a/src/clients/Dim.Clients/Api/Entitlements/IEntitlementClient.cs b/src/clients/Dim.Clients/Api/Entitlements/IEntitlementClient.cs new file mode 100644 index 0000000..da46df6 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Entitlements/IEntitlementClient.cs @@ -0,0 +1,28 @@ +/******************************************************************************** + * 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.Token; + +namespace Dim.Clients.Api.Entitlements; + +public interface IEntitlementClient +{ + Task AssignEntitlements(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Provisioning/CreateCfeRequest.cs b/src/clients/Dim.Clients/Api/Provisioning/CreateCfeRequest.cs new file mode 100644 index 0000000..692da27 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Provisioning/CreateCfeRequest.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Provisioning; + +public record CreateCfeRequest( + [property: JsonPropertyName("environmentType")] string EnvironmentType, + [property: JsonPropertyName("parameters")] Dictionary Parameters, + [property: JsonPropertyName("landscapeLabel")] string LandscapeLabel, + [property: JsonPropertyName("planName")] string PlanName, + [property: JsonPropertyName("serviceName")] string ServiceName, + [property: JsonPropertyName("user")] string User +); + +public record CreateCfeResponse( + [property: JsonPropertyName("id")] Guid Id +); diff --git a/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningClientServiceExtensions.cs new file mode 100644 index 0000000..c46310d --- /dev/null +++ b/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningClientServiceExtensions.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.Clients.Api.Provisioning.DependencyInjection; + +public static class ProvisioningClientServiceExtensions +{ + public static IServiceCollection AddProvisioningClient(this IServiceCollection services) + { + services + .AddCustomHttpClientWithAuthentication(null, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningSettings.cs b/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningSettings.cs new file mode 100644 index 0000000..48fd5a3 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningSettings.cs @@ -0,0 +1,30 @@ +/******************************************************************************** + * 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.Token; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Api.Provisioning.DependencyInjection; + +public class ProvisioningSettings : BasicAuthSettings +{ + [Required] + public string BaseUrl { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Api/Provisioning/IProvisioningClient.cs b/src/clients/Dim.Clients/Api/Provisioning/IProvisioningClient.cs new file mode 100644 index 0000000..41e3825 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Provisioning/IProvisioningClient.cs @@ -0,0 +1,28 @@ +/******************************************************************************** + * 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.Services; + +namespace Dim.Clients.Api.Provisioning; + +public interface IProvisioningClient +{ + Task CreateCloudFoundryEnvironment(string authUrl, BindingItem bindingData, string tenantName, string user, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Provisioning/ProvisioningClient.cs b/src/clients/Dim.Clients/Api/Provisioning/ProvisioningClient.cs new file mode 100644 index 0000000..a986b87 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Provisioning/ProvisioningClient.cs @@ -0,0 +1,62 @@ +/******************************************************************************** + * 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.Services; +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; + +namespace Dim.Clients.Api.Provisioning; + +public class ProvisioningClient : IProvisioningClient +{ + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public ProvisioningClient(IBasicAuthTokenService basicAuthTokenService) + { + _basicAuthTokenService = basicAuthTokenService; + } + + public async Task CreateCloudFoundryEnvironment(string authUrl, BindingItem bindingData, string tenantName, string user, CancellationToken cancellationToken) + { + var authSettings = new BasicAuthSettings + { + TokenAddress = $"{authUrl}/oauth/token", + ClientId = bindingData.Credentials.Uaa.ClientId, + ClientSecret = bindingData.Credentials.Uaa.ClientSecret + }; + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(authSettings, cancellationToken).ConfigureAwait(false); + var data = new CreateCfeRequest( + "cloudfoundry", + new Dictionary + { + { "instance_name", tenantName } + }, + "cf-eu10", + "standard", + "cloudfoundry", + user + ); + + await client.PostAsJsonAsync($"{bindingData.Credentials.Endpoints.ProvisioningServiceUrl}/provisioning/v1/environments", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-cf-env", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + } +} diff --git a/src/clients/Dim.Clients/Api/Services/CreateServiceInstanceRequest.cs b/src/clients/Dim.Clients/Api/Services/CreateServiceInstanceRequest.cs new file mode 100644 index 0000000..e311673 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Services/CreateServiceInstanceRequest.cs @@ -0,0 +1,71 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.Services; + +public record CreateServiceInstanceRequest( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("service_offering_name")] string ServiceOfferingName, + [property: JsonPropertyName("service_plan_name")] string ServicePlanName, + [property: JsonPropertyName("parameters")] Dictionary Parameters +); + +public record CreateServiceInstanceResponse( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("name")] string Name +); + +public record CreateServiceBindingRequest( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("service_instance_id")] string ServiceInstanceId +); + +public record CreateServiceBindingResponse( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("name")] string Name +); + +public record GetBindingResponse( + [property: JsonPropertyName("items")] IEnumerable Items +); + +public record BindingItem( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("service_instance_id")] Guid ServiceInstanceId, + [property: JsonPropertyName("credentials")] BindingCredentials Credentials +); + +public record BindingCredentials( + [property: JsonPropertyName("endpoints")] GetBindingEndpoints Endpoints, + [property: JsonPropertyName("grant_type")] string GrantType, + [property: JsonPropertyName("uaa")] GetBindingUaa Uaa +); + +public record GetBindingUaa( + [property: JsonPropertyName("clientid")] string ClientId, + [property: JsonPropertyName("clientsecret")] string ClientSecret +); + +public record GetBindingEndpoints( + [property: JsonPropertyName("provisioning_service_url")] string ProvisioningServiceUrl, + [property: JsonPropertyName("saas_registry_service_url")] string SaasRegistryServiceUrl +); diff --git a/src/clients/Dim.Clients/Api/Services/DependencyInjection/ServiceClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Services/DependencyInjection/ServiceClientServiceExtensions.cs new file mode 100644 index 0000000..ac75302 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Services/DependencyInjection/ServiceClientServiceExtensions.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.Clients.Api.Services.DependencyInjection; + +public static class ServiceClientServiceExtensions +{ + public static IServiceCollection AddServiceClient(this IServiceCollection services) + { + services + .AddCustomHttpClientWithAuthentication(null, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Services/IServiceClient.cs b/src/clients/Dim.Clients/Api/Services/IServiceClient.cs new file mode 100644 index 0000000..230c7ce --- /dev/null +++ b/src/clients/Dim.Clients/Api/Services/IServiceClient.cs @@ -0,0 +1,30 @@ +/******************************************************************************** + * 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.SubAccounts; + +namespace Dim.Clients.Api.Services; + +public interface IServiceClient +{ + Task CreateServiceInstance(ServiceManagementBindingItem saBinding, CancellationToken cancellationToken); + Task CreateServiceBinding(ServiceManagementBindingItem saBinding, string serviceInstanceId, CancellationToken cancellationToken); + Task GetServiceBinding(ServiceManagementBindingItem saBinding, string serviceBindingName, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Services/ServiceClient.cs b/src/clients/Dim.Clients/Api/Services/ServiceClient.cs new file mode 100644 index 0000000..d6da273 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Services/ServiceClient.cs @@ -0,0 +1,146 @@ +/******************************************************************************** + * 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.SubAccounts; +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Dim.Clients.Api.Services; + +public class ServiceClient : IServiceClient +{ + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public ServiceClient(IBasicAuthTokenService basicAuthTokenService) + { + _basicAuthTokenService = basicAuthTokenService; + } + + public async Task CreateServiceInstance(ServiceManagementBindingItem saBinding, CancellationToken cancellationToken) + { + var serviceAuth = new BasicAuthSettings + { + TokenAddress = $"{saBinding.Url}/oauth/token", + ClientId = saBinding.ClientId, + ClientSecret = saBinding.ClientSecret + }; + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(serviceAuth, cancellationToken).ConfigureAwait(false); + var directory = new CreateServiceInstanceRequest( + "cis-local-instance", + "cis", + "local", + new Dictionary + { + { "grantType", "clientCredentials" } + } + ); + + var result = await client.PostAsJsonAsync($"{saBinding.SmUrl}/v1/service_instances?async=false", directory, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-service-instance", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response was empty", true); + } + + return response; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task CreateServiceBinding(ServiceManagementBindingItem saBinding, string serviceInstanceId, CancellationToken cancellationToken) + { + var serviceAuth = new BasicAuthSettings + { + TokenAddress = $"{saBinding.Url}/oauth/token", + ClientId = saBinding.ClientId, + ClientSecret = saBinding.ClientSecret + }; + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(serviceAuth, cancellationToken).ConfigureAwait(false); + var data = new CreateServiceBindingRequest( + "cis-local-binding", + serviceInstanceId + ); + + var result = await client.PostAsJsonAsync($"{saBinding.SmUrl}/v1/service_bindings?async=false", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-service-binding", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response was empty", true); + } + + return response; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task GetServiceBinding(ServiceManagementBindingItem saBinding, string serviceBindingName, CancellationToken cancellationToken) + { + var serviceAuth = new BasicAuthSettings + { + TokenAddress = $"{saBinding.Url}/oauth/token", + ClientId = saBinding.ClientId, + ClientSecret = saBinding.ClientSecret + }; + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(serviceAuth, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync($"{saBinding.SmUrl}/v1/service_bindings?fieldQuery=name eq '{serviceBindingName}'", cancellationToken) + .CatchingIntoServiceExceptionFor("get-service-binding", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response was empty", true); + } + + if (response.Items.Count() != 1) + { + throw new ServiceException($"There must be exactly one binding for {serviceBindingName}"); + } + + return response.Items.Single(); + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } +} diff --git a/src/clients/Dim.Clients/Api/SubAccounts/CreateSubAccountRequest.cs b/src/clients/Dim.Clients/Api/SubAccounts/CreateSubAccountRequest.cs new file mode 100644 index 0000000..d7a7ccb --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/CreateSubAccountRequest.cs @@ -0,0 +1,51 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Api.SubAccounts; + +public record CreateSubAccountRequest( + [property: JsonPropertyName("betaEnabled")] bool BetaEnabled, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("displayName")] string DisplayName, + [property: JsonPropertyName("labels")] Dictionary> Labels, + [property: JsonPropertyName("origin")] string Origin, + [property: JsonPropertyName("parentGUID")] Guid ParentId, + [property: JsonPropertyName("region")] string Region, + [property: JsonPropertyName("subaccountAdmins")] IEnumerable SubaccountAdmins, + [property: JsonPropertyName("subdomain")] string Subdomain, + [property: JsonPropertyName("usedForProduction")] UsedForProduction UsedForProduction +); + +public record CreateSubaccountResponse( + [property: JsonPropertyName("guid")] Guid Id +); + +public record ServiceManagementBindingResponse( + [property: JsonPropertyName("items")] IEnumerable Items +); + +public record ServiceManagementBindingItem( + [property: JsonPropertyName("clientid")] string ClientId, + [property: JsonPropertyName("clientsecret")] string ClientSecret, + [property: JsonPropertyName("sm_url")] string SmUrl, + [property: JsonPropertyName("url")] string Url +); diff --git a/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountClientServiceExtensions.cs new file mode 100644 index 0000000..77be287 --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountClientServiceExtensions.cs @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Dim.Clients.Api.SubAccounts.DependencyInjection; + +public static class SubAccountClientServiceExtensions +{ + public static IServiceCollection AddSubAccountClient(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + services + .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountSettings.cs b/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountSettings.cs new file mode 100644 index 0000000..aec5c64 --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountSettings.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * 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.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Api.SubAccounts.DependencyInjection; + +public class SubAccountSettings +{ + [Required] + public string BaseUrl { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Api/SubAccounts/ISubAccountClient.cs b/src/clients/Dim.Clients/Api/SubAccounts/ISubAccountClient.cs new file mode 100644 index 0000000..e95ce33 --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/ISubAccountClient.cs @@ -0,0 +1,30 @@ +/******************************************************************************** + * 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.Token; + +namespace Dim.Clients.Api.SubAccounts; + +public interface ISubAccountClient +{ + Task CreateSubaccount(BasicAuthSettings basicAuthSettings, string adminMail, string tenantName, Guid directoryId, CancellationToken cancellationToken); + Task CreateServiceManagerBindings(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken); + Task GetServiceManagerBindings(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/SubAccounts/SubAccountClient.cs b/src/clients/Dim.Clients/Api/SubAccounts/SubAccountClient.cs new file mode 100644 index 0000000..3150304 --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/SubAccountClient.cs @@ -0,0 +1,123 @@ +/******************************************************************************** + * 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.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Dim.Clients.Api.SubAccounts; + +public class SubAccountClient : ISubAccountClient +{ + private static readonly Regex TenantName = new(@"(?<=[^\w-])|(?<=[^-])[\W_]+|(?<=[^-])$", RegexOptions.Compiled, new TimeSpan(0, 0, 0, 1)); + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public SubAccountClient(IBasicAuthTokenService basicAuthTokenService) + { + _basicAuthTokenService = basicAuthTokenService; + } + + public async Task CreateSubaccount(BasicAuthSettings basicAuthSettings, string adminMail, string tenantName, Guid directoryId, CancellationToken cancellationToken) + { + var subdomain = TenantName.Replace(tenantName, "-").ToLower().TrimStart('-').TrimEnd('-'); + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(basicAuthSettings, cancellationToken).ConfigureAwait(false); + var directory = new CreateSubAccountRequest( + false, + $"CX customer sub-account {tenantName}", + tenantName, + new Dictionary> + { + { "cloud_management_service", new[] { "Created by API - Don't change it" } }, + { "tenantName", new[] { tenantName } } + }, + "API", + directoryId, + "eu10", + Enumerable.Repeat(adminMail, 1), + subdomain, + UsedForProduction.USED_FOR_PRODUCTION + ); + + var result = await client.PostAsJsonAsync("/accounts/v1/subaccounts", directory, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-subaccount", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, async message => + { + var errorMessage = await message.Content.ReadAsStringAsync().ConfigureAwait(false); + return new(false, errorMessage); + }).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + return response.Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task CreateServiceManagerBindings(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(basicAuthSettings, cancellationToken).ConfigureAwait(false); + var data = new + { + name = "accessServiceManager" + }; + + await client.PostAsJsonAsync($"/accounts/v2/subaccounts/{subAccountId}/serviceManagerBindings", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-subaccount", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + } + + public async Task GetServiceManagerBindings(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(basicAuthSettings, cancellationToken).ConfigureAwait(false); + + var result = await client.GetAsync($"/accounts/v2/subaccounts/{subAccountId}/serviceManagerBindings", cancellationToken) + .CatchingIntoServiceExceptionFor("create-subaccount", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null || response.Items.Count() != 1) + { + throw new ServiceException("Response must not be null and contain exactly 1 item"); + } + + return response.Items.Single(); + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } +} diff --git a/src/clients/Dim.Clients/Api/SubAccounts/UsedForProduction.cs b/src/clients/Dim.Clients/Api/SubAccounts/UsedForProduction.cs new file mode 100644 index 0000000..619234a --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/UsedForProduction.cs @@ -0,0 +1,28 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Clients.Api.SubAccounts; + +public enum UsedForProduction +{ + USED_FOR_PRODUCTION = 1, + NOT_USED_FOR_PRODUCTION = 2, + UNSET = 3 +} diff --git a/src/clients/Dim.Clients/Api/Subscriptions/DependencyInjection/SubscriptionClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Subscriptions/DependencyInjection/SubscriptionClientServiceExtensions.cs new file mode 100644 index 0000000..6a6e846 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Subscriptions/DependencyInjection/SubscriptionClientServiceExtensions.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.Clients.Api.Subscriptions.DependencyInjection; + +public static class SubscriptionClientServiceExtensions +{ + public static IServiceCollection AddSubscriptionClient(this IServiceCollection services) + { + services + .AddCustomHttpClientWithAuthentication(null, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Subscriptions/ISubscriptionClient.cs b/src/clients/Dim.Clients/Api/Subscriptions/ISubscriptionClient.cs new file mode 100644 index 0000000..35fbd9a --- /dev/null +++ b/src/clients/Dim.Clients/Api/Subscriptions/ISubscriptionClient.cs @@ -0,0 +1,28 @@ +/******************************************************************************** + * 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.Services; + +namespace Dim.Clients.Api.Subscriptions; + +public interface ISubscriptionClient +{ + Task SubscribeApplication(string authUrl, BindingItem bindingData, string applicationName, string planName, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Subscriptions/SubscriptionClient.cs b/src/clients/Dim.Clients/Api/Subscriptions/SubscriptionClient.cs new file mode 100644 index 0000000..04bb6b8 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Subscriptions/SubscriptionClient.cs @@ -0,0 +1,55 @@ +/******************************************************************************** + * 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.Services; +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; + +namespace Dim.Clients.Api.Subscriptions; + +public class SubscriptionClient : ISubscriptionClient +{ + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public SubscriptionClient(IBasicAuthTokenService basicAuthTokenService) + { + _basicAuthTokenService = basicAuthTokenService; + } + + public async Task SubscribeApplication(string authUrl, BindingItem bindingData, string applicationName, string planName, CancellationToken cancellationToken) + { + var authSettings = new BasicAuthSettings + { + TokenAddress = $"{authUrl}/oauth/token", + ClientId = bindingData.Credentials.Uaa.ClientId, + ClientSecret = bindingData.Credentials.Uaa.ClientSecret + }; + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(authSettings, cancellationToken).ConfigureAwait(false); + var data = new + { + planName = planName + }; + + await client.PostAsJsonAsync($"{bindingData.Credentials.Endpoints.SaasRegistryServiceUrl}/saas-manager/v1/applications/{applicationName}/subscription", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("subscribe-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + } +} diff --git a/src/clients/Dim.Clients/Dim.Clients.csproj b/src/clients/Dim.Clients/Dim.Clients.csproj new file mode 100644 index 0000000..598ed58 --- /dev/null +++ b/src/clients/Dim.Clients/Dim.Clients.csproj @@ -0,0 +1,44 @@ +īģŋ + + + + + net8.0 + enable + enable + Dim.Clients + Dim.Clients + b2a7407a-26de-4833-b207-bb823d67ca22 + + + + + + + + + + + + + + + diff --git a/src/clients/Dim.Clients/Extensions/HttpClientExtensions.cs b/src/clients/Dim.Clients/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000..475f0cf --- /dev/null +++ b/src/clients/Dim.Clients/Extensions/HttpClientExtensions.cs @@ -0,0 +1,46 @@ +/******************************************************************************** + * 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.Extensions.DependencyInjection; + +namespace Dim.Clients.Extensions; + +public static class HttpClientExtensions +{ + public static IServiceCollection AddCustomHttpClientWithAuthentication(this IServiceCollection services, string? baseAddress, string? authAddress) where T : class + { + services.AddHttpClient(typeof(T).Name, c => + { + if (baseAddress != null) + { + c.BaseAddress = new Uri(baseAddress); + } + }); + + services.AddHttpClient($"{typeof(T).Name}Auth", c => + { + if (authAddress != null) + { + c.BaseAddress = new Uri(authAddress); + } + }); + return services; + } +} diff --git a/src/clients/Dim.Clients/Extensions/JsonSerializerExtensions.cs b/src/clients/Dim.Clients/Extensions/JsonSerializerExtensions.cs new file mode 100644 index 0000000..515261a --- /dev/null +++ b/src/clients/Dim.Clients/Extensions/JsonSerializerExtensions.cs @@ -0,0 +1,37 @@ +/******************************************************************************** + * 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.Text.Json; +using System.Text.Json.Serialization; + +namespace Dim.Clients.Extensions; + +public static class JsonSerializerExtensions +{ + public static readonly JsonSerializerOptions Options = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = + { + new JsonStringEnumConverter(allowIntegerValues: false) + }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; +} diff --git a/src/clients/Dim.Clients/Token/AuthResponse.cs b/src/clients/Dim.Clients/Token/AuthResponse.cs new file mode 100644 index 0000000..ac64494 --- /dev/null +++ b/src/clients/Dim.Clients/Token/AuthResponse.cs @@ -0,0 +1,41 @@ +/******************************************************************************** + * 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.Text.Json.Serialization; + +namespace Dim.Clients.Token; + +public record AuthResponse( + [property: JsonPropertyName("access_token")] string? AccessToken, + [property: JsonPropertyName("expires_in")] int ExpiresIn, + [property: JsonPropertyName("token_type")] string? TokenType, + [property: JsonPropertyName("jti")] string? Jti, + [property: JsonPropertyName("scope")] string? Scope +); + +public record LegacyAuthResponse( + [property: JsonPropertyName("access_token")] string? AccessToken, + [property: JsonPropertyName("token_type")] string? TokenType, + [property: JsonPropertyName("id_token")] string? IdToken, + [property: JsonPropertyName("refresh_token")] string? RefreshToken, + [property: JsonPropertyName("expires_in")] int ExpiresIn, + [property: JsonPropertyName("scope")] string? Scope, + [property: JsonPropertyName("jti")] string? Jti +); diff --git a/src/clients/Dim.Clients/Token/BasicAuthSettings.cs b/src/clients/Dim.Clients/Token/BasicAuthSettings.cs new file mode 100644 index 0000000..8bae847 --- /dev/null +++ b/src/clients/Dim.Clients/Token/BasicAuthSettings.cs @@ -0,0 +1,35 @@ +/******************************************************************************** + * 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.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Token; + +public class BasicAuthSettings +{ + [Required(AllowEmptyStrings = false)] + public string ClientId { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string ClientSecret { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string TokenAddress { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Token/BasicAuthTokenService.cs b/src/clients/Dim.Clients/Token/BasicAuthTokenService.cs new file mode 100644 index 0000000..d817fbb --- /dev/null +++ b/src/clients/Dim.Clients/Token/BasicAuthTokenService.cs @@ -0,0 +1,108 @@ +/******************************************************************************** + * 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.Extensions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Headers; +using System.Net.Http.Json; + +namespace Dim.Clients.Token; + +public class BasicAuthTokenService : IBasicAuthTokenService +{ + private readonly IHttpClientFactory _httpClientFactory; + + public BasicAuthTokenService(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + public async Task GetBasicAuthorizedClient(BasicAuthSettings settings, CancellationToken cancellationToken) + { + var tokenParameters = new GetBasicTokenSettings( + $"{typeof(T).Name}Auth", + settings.ClientId, + settings.ClientSecret, + settings.TokenAddress); + + var token = await this.GetBasicTokenAsync(tokenParameters, cancellationToken).ConfigureAwait(false); + + var httpClient = _httpClientFactory.CreateClient(typeof(T).Name); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + return httpClient; + } + + public async Task GetBasicAuthorizedLegacyClient(BasicAuthSettings settings, CancellationToken cancellationToken) + { + var tokenParameters = new GetBasicTokenSettings( + $"{typeof(T).Name}Auth", + settings.ClientId, + settings.ClientSecret, + settings.TokenAddress); + + var token = await this.GetBasicLegacyToken(tokenParameters, cancellationToken).ConfigureAwait(false); + + var httpClient = _httpClientFactory.CreateClient(typeof(T).Name); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + return httpClient; + } + + private async Task GetBasicTokenAsync(GetBasicTokenSettings settings, CancellationToken cancellationToken) + { + var formParameters = new Dictionary + { + { "grant_type", "client_credentials" } + }; + var content = new FormUrlEncodedContent(formParameters); + var authClient = _httpClientFactory.CreateClient(settings.HttpClientName); + var authenticationString = $"{settings.ClientId}:{settings.ClientSecret}"; + var base64String = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(authenticationString)); + + authClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64String); + + var response = await authClient.PostAsync(settings.TokenAddress, content, cancellationToken) + .CatchingIntoServiceExceptionFor("token-post", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + + var responseObject = await response.Content.ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken).ConfigureAwait(false); + return responseObject?.AccessToken; + } + + private async Task GetBasicLegacyToken(GetBasicTokenSettings settings, CancellationToken cancellationToken) + { + var formParameters = new Dictionary + { + { "username", settings.ClientId }, + { "password", settings.ClientSecret }, + { "client_id", "cf" }, + { "grant_type", "password" }, + { "response_type", "token" } + }; + var content = new FormUrlEncodedContent(formParameters); + var authClient = _httpClientFactory.CreateClient(settings.HttpClientName); + var base64String = Convert.ToBase64String("cf:"u8.ToArray()); + authClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64String); + + var response = await authClient.PostAsync(settings.TokenAddress, content, cancellationToken) + .CatchingIntoServiceExceptionFor("token-post", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + + var responseObject = await response.Content.ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken).ConfigureAwait(false); + return responseObject?.AccessToken; + } +} diff --git a/src/clients/Dim.Clients/Token/GetBasicTokenSettings.cs b/src/clients/Dim.Clients/Token/GetBasicTokenSettings.cs new file mode 100644 index 0000000..54d8f03 --- /dev/null +++ b/src/clients/Dim.Clients/Token/GetBasicTokenSettings.cs @@ -0,0 +1,33 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Clients.Token; + +public record GetBasicTokenSettings(string HttpClientName, string ClientId, string ClientSecret, string TokenAddress); + +public record GetTokenSettings( + string HttpClientName, + string Username, + string Password, + string ClientId, + string GrantType, + string ClientSecret, + string Scope, + string TokenUrl); diff --git a/src/clients/Dim.Clients/Token/IBasicAuthTokenService.cs b/src/clients/Dim.Clients/Token/IBasicAuthTokenService.cs new file mode 100644 index 0000000..9c37f2c --- /dev/null +++ b/src/clients/Dim.Clients/Token/IBasicAuthTokenService.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Clients.Token; + +public interface IBasicAuthTokenService +{ + Task GetBasicAuthorizedClient(BasicAuthSettings settings, CancellationToken cancellationToken); + Task GetBasicAuthorizedLegacyClient(BasicAuthSettings settings, CancellationToken cancellationToken); +} diff --git a/src/database/Dim.DbAccess/DependencyInjection/DimRepositoriesServiceExtensions.cs b/src/database/Dim.DbAccess/DependencyInjection/DimRepositoriesServiceExtensions.cs new file mode 100644 index 0000000..1bc98f4 --- /dev/null +++ b/src/database/Dim.DbAccess/DependencyInjection/DimRepositoriesServiceExtensions.cs @@ -0,0 +1,40 @@ +/******************************************************************************** + * 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.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.DbAccess.DependencyInjection; + +public static class DimRepositoriesServiceExtensions +{ + public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration) + { + services + .AddScoped() + .AddDbContext(o => o + .UseNpgsql(configuration.GetConnectionString("DimDb"))) + .AddHealthChecks() + .AddDbContextCheck("DimDb", tags: new[] { "dimdb" }); + return services; + } +} diff --git a/src/database/Dim.DbAccess/Dim.DbAccess.csproj b/src/database/Dim.DbAccess/Dim.DbAccess.csproj new file mode 100644 index 0000000..f63bb72 --- /dev/null +++ b/src/database/Dim.DbAccess/Dim.DbAccess.csproj @@ -0,0 +1,41 @@ + + + + + + net8.0 + enable + enable + Dim.DbAccess + Dim.DbAccess + + + + + + + + + + + + + diff --git a/src/database/Dim.DbAccess/DimRepositories.cs b/src/database/Dim.DbAccess/DimRepositories.cs new file mode 100644 index 0000000..542b173 --- /dev/null +++ b/src/database/Dim.DbAccess/DimRepositories.cs @@ -0,0 +1,101 @@ +/******************************************************************************** + * 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.DbAccess.Repositories; +using Dim.Entities; +using Microsoft.EntityFrameworkCore; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Collections.Immutable; + +namespace Dim.DbAccess; + +public class DimRepositories : IDimRepositories +{ + private readonly DimDbContext _dbContext; + + private static readonly IReadOnlyDictionary> _types = new Dictionary> { + { typeof(IProcessStepRepository), context => new ProcessStepRepository(context) }, + { typeof(ITenantRepository), context => new TenantRepository(context) } + }.ToImmutableDictionary(); + + public DimRepositories(DimDbContext dimDbContext) + { + _dbContext = dimDbContext; + } + + public RepositoryType GetInstance() + { + Object? repository = default; + + if (_types.TryGetValue(typeof(RepositoryType), out var createFunc)) + { + repository = createFunc(_dbContext); + } + + return (RepositoryType)(repository ?? throw new ArgumentException($"unexpected type {typeof(RepositoryType).Name}", nameof(RepositoryType))); + } + + /// + public TEntity Attach(TEntity entity, Action? setOptionalParameters = null) where TEntity : class + { + var attachedEntity = _dbContext.Attach(entity).Entity; + setOptionalParameters?.Invoke(attachedEntity); + + return attachedEntity; + } + + public void AttachRange(IEnumerable entities, Action setOptionalParameters) where TEntity : class + { + foreach (var entity in entities) + { + var attachedEntity = _dbContext.Attach(entity).Entity; + setOptionalParameters.Invoke(attachedEntity); + } + } + + public IEnumerable AttachRange(IEnumerable entities) where TEntity : class + { + foreach (var entity in entities) + { + yield return _dbContext.Attach(entity).Entity; + } + } + + /// + public TEntity Remove(TEntity entity) where TEntity : class + => _dbContext.Remove(entity).Entity; + + public void RemoveRange(IEnumerable entities) where TEntity : class + => _dbContext.RemoveRange(entities); + + public Task SaveAsync() + { + try + { + return _dbContext.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException e) + { + throw new ConflictException("while processing a concurrent update was saved to the database (reason could also be data to be deleted is no longer existing)", e); + } + } + + public void Clear() => _dbContext.ChangeTracker.Clear(); +} diff --git a/src/database/Dim.DbAccess/IDimRepositories.cs b/src/database/Dim.DbAccess/IDimRepositories.cs new file mode 100644 index 0000000..b054bf8 --- /dev/null +++ b/src/database/Dim.DbAccess/IDimRepositories.cs @@ -0,0 +1,39 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.DbAccess; + +public interface IDimRepositories +{ + RepositoryType GetInstance(); + + /// + TEntity Attach(TEntity entity, Action? setOptionalParameters = null) where TEntity : class; + + void AttachRange(IEnumerable entities, Action setOptionalParameters) where TEntity : class; + IEnumerable AttachRange(IEnumerable entities) where TEntity : class; + + /// + TEntity Remove(TEntity entity) where TEntity : class; + + void RemoveRange(IEnumerable entities) where TEntity : class; + Task SaveAsync(); + void Clear(); +} diff --git a/src/database/Dim.DbAccess/Repositories/IProcessStepRepository.cs b/src/database/Dim.DbAccess/Repositories/IProcessStepRepository.cs new file mode 100644 index 0000000..f9b26df --- /dev/null +++ b/src/database/Dim.DbAccess/Repositories/IProcessStepRepository.cs @@ -0,0 +1,38 @@ +/******************************************************************************** + * 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.Entities.Entities; +using Dim.Entities.Enums; + +namespace Dim.DbAccess.Repositories; + +/// +/// Repository for accessing and creating processSteps on persistence layer. +/// +public interface IProcessStepRepository +{ + Process CreateProcess(ProcessTypeId processTypeId); + ProcessStep CreateProcessStep(ProcessStepTypeId processStepTypeId, ProcessStepStatusId processStepStatusId, Guid processId); + IEnumerable CreateProcessStepRange(IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatus); + void AttachAndModifyProcessStep(Guid processStepId, Action? initialize, Action modify); + void AttachAndModifyProcessSteps(IEnumerable<(Guid ProcessStepId, Action? Initialize, Action Modify)> processStepIdsInitializeModifyData); + IAsyncEnumerable GetActiveProcesses(IEnumerable processTypeIds, IEnumerable processStepTypeIds, DateTimeOffset lockExpiryDate); + IAsyncEnumerable<(Guid ProcessStepId, ProcessStepTypeId ProcessStepTypeId)> GetProcessStepData(Guid processId); +} diff --git a/src/database/Dim.DbAccess/Repositories/ITenantRepository.cs b/src/database/Dim.DbAccess/Repositories/ITenantRepository.cs new file mode 100644 index 0000000..2f6ea81 --- /dev/null +++ b/src/database/Dim.DbAccess/Repositories/ITenantRepository.cs @@ -0,0 +1,39 @@ +/******************************************************************************** + * 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.Entities.Entities; + +namespace Dim.DbAccess.Repositories; + +public interface ITenantRepository +{ + Tenant CreateTenant(string companyName, string bpn, string didDocumentLocation, bool isIssuer, Guid processId, Guid operatorId); + Task<(bool Exists, Guid TenantId, string CompanyName, string Bpn)> GetTenantDataForProcessId(Guid processId); + void AttachAndModifyTenant(Guid tenantId, Action? initialize, Action modify); + Task GetSubAccountIdByTenantId(Guid tenantId); + Task<(Guid? SubAccountId, string? ServiceInstanceId)> GetSubAccountAndServiceInstanceIdsByTenantId(Guid tenantId); + Task<(Guid? SubAccountId, string? ServiceBindingName)> GetSubAccountIdAndServiceBindingNameByTenantId(Guid tenantId); + Task GetSpaceId(Guid tenantId); + Task GetDimInstanceId(Guid tenantId); + Task<(string bpn, string? DownloadUrl, string? Did, Guid? DimInstanceId)> GetCallbackData(Guid tenantId); + Task<(Guid? DimInstanceId, string HostingUrl, bool IsIssuer)> GetDimInstanceIdAndHostingUrl(Guid tenantId); + Task<(string? ApplicationId, Guid? CompanyId, Guid? DimInstanceId, bool IsIssuer)> GetApplicationAndCompanyId(Guid tenantId); + Task<(bool Exists, Guid? CompanyId, Guid? InstanceId)> GetCompanyAndInstanceIdForBpn(string bpn); +} diff --git a/src/database/Dim.DbAccess/Repositories/ProcessStepRepository.cs b/src/database/Dim.DbAccess/Repositories/ProcessStepRepository.cs new file mode 100644 index 0000000..cc66c45 --- /dev/null +++ b/src/database/Dim.DbAccess/Repositories/ProcessStepRepository.cs @@ -0,0 +1,101 @@ +/******************************************************************************** + * 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.Entities; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Microsoft.EntityFrameworkCore; +using ProcessTypeId = Dim.Entities.Enums.ProcessTypeId; + +namespace Dim.DbAccess.Repositories; + +public class ProcessStepRepository : IProcessStepRepository +{ + private readonly DimDbContext _context; + + /// + /// Constructor + /// + /// DimDb context. + public ProcessStepRepository(DimDbContext dimDbContext) + { + _context = dimDbContext; + } + + public Process CreateProcess(ProcessTypeId processTypeId) => + _context.Add(new Process(Guid.NewGuid(), processTypeId, Guid.NewGuid())).Entity; + + public ProcessStep CreateProcessStep(Dim.Entities.Enums.ProcessStepTypeId processStepTypeId, Dim.Entities.Enums.ProcessStepStatusId processStepStatusId, Guid processId) => + _context.Add(new ProcessStep(Guid.NewGuid(), processStepTypeId, processStepStatusId, processId, DateTimeOffset.UtcNow)).Entity; + + public IEnumerable CreateProcessStepRange(IEnumerable<(Dim.Entities.Enums.ProcessStepTypeId ProcessStepTypeId, Dim.Entities.Enums.ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatus) + { + var processSteps = processStepTypeStatus.Select(x => new ProcessStep(Guid.NewGuid(), x.ProcessStepTypeId, x.ProcessStepStatusId, x.ProcessId, DateTimeOffset.UtcNow)).ToList(); + _context.AddRange(processSteps); + return processSteps; + } + + public void AttachAndModifyProcessStep(Guid processStepId, Action? initialize, Action modify) + { + var step = new ProcessStep(processStepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + _context.Attach(step); + step.DateLastChanged = DateTimeOffset.UtcNow; + modify(step); + } + + public void AttachAndModifyProcessSteps(IEnumerable<(Guid ProcessStepId, Action? Initialize, Action Modify)> processStepIdsInitializeModifyData) + { + var stepModifyData = processStepIdsInitializeModifyData.Select(data => + { + var step = new ProcessStep(data.ProcessStepId, default, default, Guid.Empty, default); + data.Initialize?.Invoke(step); + return (Step: step, data.Modify); + }).ToList(); + _context.AttachRange(stepModifyData.Select(data => data.Step)); + stepModifyData.ForEach(data => + { + data.Step.DateLastChanged = DateTimeOffset.UtcNow; + data.Modify(data.Step); + }); + } + + public IAsyncEnumerable GetActiveProcesses(IEnumerable processTypeIds, IEnumerable processStepTypeIds, DateTimeOffset lockExpiryDate) => + _context.Processes + .AsNoTracking() + .Where(process => + processTypeIds.Contains(process.ProcessTypeId) && + process.ProcessSteps.Any(step => processStepTypeIds.Contains(step.ProcessStepTypeId) && step.ProcessStepStatusId == ProcessStepStatusId.TODO) && + (process.LockExpiryDate == null || process.LockExpiryDate < lockExpiryDate)) + .AsAsyncEnumerable(); + + public IAsyncEnumerable<(Guid ProcessStepId, ProcessStepTypeId ProcessStepTypeId)> GetProcessStepData(Guid processId) => + _context.ProcessSteps + .AsNoTracking() + .Where(step => + step.ProcessId == processId && + step.ProcessStepStatusId == ProcessStepStatusId.TODO) + .OrderBy(step => step.ProcessStepTypeId) + .Select(step => + new ValueTuple( + step.Id, + step.ProcessStepTypeId)) + .AsAsyncEnumerable(); +} diff --git a/src/database/Dim.DbAccess/Repositories/TenantRepository.cs b/src/database/Dim.DbAccess/Repositories/TenantRepository.cs new file mode 100644 index 0000000..82abb60 --- /dev/null +++ b/src/database/Dim.DbAccess/Repositories/TenantRepository.cs @@ -0,0 +1,109 @@ +/******************************************************************************** + * 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.Entities; +using Dim.Entities.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Dim.DbAccess.Repositories; + +public class TenantRepository : ITenantRepository +{ + private readonly DimDbContext _context; + + public TenantRepository(DimDbContext context) + { + _context = context; + } + + public Tenant CreateTenant(string companyName, string bpn, string didDocumentLocation, bool isIssuer, Guid processId, Guid operatorId) => + _context.Tenants.Add(new Tenant(Guid.NewGuid(), companyName, bpn, didDocumentLocation, isIssuer, processId, operatorId)).Entity; + + public Task<(bool Exists, Guid TenantId, string CompanyName, string Bpn)> GetTenantDataForProcessId(Guid processId) => + _context.Tenants + .Where(x => x.ProcessId == processId) + .Select(x => new ValueTuple(true, x.Id, x.CompanyName, x.Bpn)) + .SingleOrDefaultAsync(); + + public void AttachAndModifyTenant(Guid tenantId, Action? initialize, Action modify) + { + var tenant = new Tenant(tenantId, null!, null!, null!, default, Guid.Empty, Guid.Empty); + initialize?.Invoke(tenant); + _context.Tenants.Attach(tenant); + modify(tenant); + } + + public Task GetSubAccountIdByTenantId(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => x.SubAccountId) + .SingleOrDefaultAsync(); + + public Task<(Guid? SubAccountId, string? ServiceInstanceId)> GetSubAccountAndServiceInstanceIdsByTenantId(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => new ValueTuple(x.SubAccountId, x.ServiceInstanceId)) + .SingleOrDefaultAsync(); + + public Task<(Guid? SubAccountId, string? ServiceBindingName)> GetSubAccountIdAndServiceBindingNameByTenantId(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => new ValueTuple(x.SubAccountId, x.ServiceBindingName)) + .SingleOrDefaultAsync(); + + public Task GetSpaceId(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => x.SpaceId) + .SingleOrDefaultAsync(); + + public Task GetDimInstanceId(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => x.DimInstanceId) + .SingleOrDefaultAsync(); + + public Task<(string bpn, string? DownloadUrl, string? Did, Guid? DimInstanceId)> GetCallbackData(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => new ValueTuple(x.Bpn, x.DidDownloadUrl, x.Did, x.DimInstanceId)) + .SingleOrDefaultAsync(); + + public Task<(Guid? DimInstanceId, string HostingUrl, bool IsIssuer)> GetDimInstanceIdAndHostingUrl(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => new ValueTuple(x.DimInstanceId, x.DidDocumentLocation, x.IsIssuer)) + .SingleOrDefaultAsync(); + + public Task<(string? ApplicationId, Guid? CompanyId, Guid? DimInstanceId, bool IsIssuer)> GetApplicationAndCompanyId(Guid tenantId) => + _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => new ValueTuple( + x.ApplicationId, + x.CompanyId, + x.DimInstanceId, + x.IsIssuer)) + .SingleOrDefaultAsync(); + + public Task<(bool Exists, Guid? CompanyId, Guid? InstanceId)> GetCompanyAndInstanceIdForBpn(string bpn) => + _context.Tenants.Where(x => x.Bpn == bpn) + .Select(x => new ValueTuple(true, x.CompanyId, x.DimInstanceId)) + .SingleOrDefaultAsync(); +} diff --git a/src/database/Dim.Entities/Dim.Entities.csproj b/src/database/Dim.Entities/Dim.Entities.csproj new file mode 100644 index 0000000..65247d8 --- /dev/null +++ b/src/database/Dim.Entities/Dim.Entities.csproj @@ -0,0 +1,39 @@ + + + + + + net8.0 + enable + enable + Dim.Entities + Dim.Entities + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/src/database/Dim.Entities/DimDbContext.cs b/src/database/Dim.Entities/DimDbContext.cs new file mode 100644 index 0000000..b480859 --- /dev/null +++ b/src/database/Dim.Entities/DimDbContext.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 Dim.Entities.Entities; +using Dim.Entities.Enums; +using Microsoft.EntityFrameworkCore; + +namespace Dim.Entities; + +public class DimDbContext : DbContext +{ + protected DimDbContext() + { + } + + public DimDbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Processes { get; set; } = default!; + public virtual DbSet ProcessSteps { get; set; } = default!; + public virtual DbSet ProcessStepStatuses { get; set; } = default!; + public virtual DbSet ProcessStepTypes { get; set; } = default!; + public virtual DbSet ProcessTypes { get; set; } = default!; + public virtual DbSet Tenants { get; set; } = default!; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSnakeCaseNamingConvention(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasAnnotation("Relational:Collation", "en_US.utf8"); + modelBuilder.HasDefaultSchema("dim"); + + modelBuilder.Entity() + .HasOne(d => d.ProcessType) + .WithMany(p => p.Processes) + .HasForeignKey(d => d.ProcessTypeId) + .OnDelete(DeleteBehavior.ClientSetNull); + + modelBuilder.Entity() + .HasOne(d => d.Process) + .WithMany(p => p.ProcessSteps) + .HasForeignKey(d => d.ProcessId) + .OnDelete(DeleteBehavior.ClientSetNull); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(ProcessTypeId)) + .Cast() + .Select(e => new ProcessType(e)) + ); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(ProcessStepStatusId)) + .Cast() + .Select(e => new ProcessStepStatus(e)) + ); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(ProcessStepTypeId)) + .Cast() + .Select(e => new ProcessStepType(e)) + ); + + modelBuilder.Entity() + .HasOne(d => d.Process) + .WithMany(p => p.Tenants) + .HasForeignKey(d => d.ProcessId) + .OnDelete(DeleteBehavior.ClientSetNull); + } +} diff --git a/src/database/Dim.Entities/Entities/Process.cs b/src/database/Dim.Entities/Entities/Process.cs new file mode 100644 index 0000000..b76dc64 --- /dev/null +++ b/src/database/Dim.Entities/Entities/Process.cs @@ -0,0 +1,55 @@ +/******************************************************************************** + * 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.Entities.Enums; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Entities.Entities; + +public class Process : ILockableEntity +{ + private Process() + { + ProcessSteps = new HashSet(); + Tenants = new HashSet(); + } + + public Process(Guid id, ProcessTypeId processTypeId, Guid version) : this() + { + Id = id; + ProcessTypeId = processTypeId; + Version = version; + } + + public Guid Id { get; private set; } + + public ProcessTypeId ProcessTypeId { get; set; } + + public DateTimeOffset? LockExpiryDate { get; set; } + + [ConcurrencyCheck] + public Guid Version { get; set; } + + // Navigation properties + public virtual ProcessType? ProcessType { get; set; } + public virtual ICollection ProcessSteps { get; private set; } + public virtual ICollection Tenants { get; private set; } +} diff --git a/src/database/Dim.Entities/Entities/ProcessStep.cs b/src/database/Dim.Entities/Entities/ProcessStep.cs new file mode 100644 index 0000000..09b1dd4 --- /dev/null +++ b/src/database/Dim.Entities/Entities/ProcessStep.cs @@ -0,0 +1,54 @@ +/******************************************************************************** + * 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.Entities.Enums; + +namespace Dim.Entities.Entities; + +public class ProcessStep +{ + public ProcessStep(Guid id, ProcessStepTypeId processStepTypeId, ProcessStepStatusId processStepStatusId, Guid processId, DateTimeOffset dateCreated) + { + Id = id; + ProcessStepTypeId = processStepTypeId; + ProcessStepStatusId = processStepStatusId; + ProcessId = processId; + DateCreated = dateCreated; + } + + public Guid Id { get; private set; } + + public ProcessStepTypeId ProcessStepTypeId { get; private set; } + + public ProcessStepStatusId ProcessStepStatusId { get; set; } + + public Guid ProcessId { get; private set; } + + public DateTimeOffset DateCreated { get; private set; } + + public DateTimeOffset? DateLastChanged { get; set; } + + public string? Message { get; set; } + + // Navigation properties + public virtual ProcessStepType? ProcessStepType { get; private set; } + public virtual ProcessStepStatus? ProcessStepStatus { get; set; } + public virtual Process? Process { get; private set; } +} diff --git a/src/database/Dim.Entities/Entities/ProcessStepStatus.cs b/src/database/Dim.Entities/Entities/ProcessStepStatus.cs new file mode 100644 index 0000000..ef7619e --- /dev/null +++ b/src/database/Dim.Entities/Entities/ProcessStepStatus.cs @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Entities.Entities; + +public class ProcessStepStatus +{ + private ProcessStepStatus() + { + this.Label = null!; + this.ProcessSteps = new HashSet(); + } + + public ProcessStepStatus(ProcessStepStatusId processStepStatusId) : this() + { + Id = processStepStatusId; + Label = processStepStatusId.ToString(); + } + + public ProcessStepStatusId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection ProcessSteps { get; private set; } +} diff --git a/src/database/Dim.Entities/Entities/ProcessStepType.cs b/src/database/Dim.Entities/Entities/ProcessStepType.cs new file mode 100644 index 0000000..87d1ccc --- /dev/null +++ b/src/database/Dim.Entities/Entities/ProcessStepType.cs @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Entities.Entities; + +public class ProcessStepType +{ + private ProcessStepType() + { + this.Label = null!; + this.ProcessSteps = new HashSet(); + } + + public ProcessStepType(ProcessStepTypeId processStepTypeId) : this() + { + Id = processStepTypeId; + Label = processStepTypeId.ToString(); + } + + public ProcessStepTypeId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection ProcessSteps { get; private set; } +} diff --git a/src/database/Dim.Entities/Entities/ProcessType.cs b/src/database/Dim.Entities/Entities/ProcessType.cs new file mode 100644 index 0000000..b8ff4cc --- /dev/null +++ b/src/database/Dim.Entities/Entities/ProcessType.cs @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Entities.Entities; + +public class ProcessType +{ + private ProcessType() + { + this.Label = null!; + this.Processes = new HashSet(); + } + + public ProcessType(ProcessTypeId processTypeId) : this() + { + Id = processTypeId; + Label = processTypeId.ToString(); + } + + public ProcessTypeId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection Processes { get; private set; } +} diff --git a/src/database/Dim.Entities/Entities/Tenant.cs b/src/database/Dim.Entities/Entities/Tenant.cs new file mode 100644 index 0000000..e26f96c --- /dev/null +++ b/src/database/Dim.Entities/Entities/Tenant.cs @@ -0,0 +1,62 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Entities.Entities; + +public class Tenant +{ + public Tenant(Guid id, string companyName, string bpn, string didDocumentLocation, bool isIssuer, Guid processId, Guid operatorId) + { + Id = id; + CompanyName = companyName; + Bpn = bpn; + DidDocumentLocation = didDocumentLocation; + IsIssuer = isIssuer; + ProcessId = processId; + OperatorId = operatorId; + } + + public Guid Id { get; set; } + public string CompanyName { get; set; } + public string Bpn { get; set; } + + public string DidDocumentLocation { get; set; } + + public bool IsIssuer { get; set; } + + public Guid ProcessId { get; set; } + + public Guid? SubAccountId { get; set; } + + public string? ServiceInstanceId { get; set; } + + public string? ServiceBindingName { get; set; } + + public Guid? SpaceId { get; set; } + + public Guid? DimInstanceId { get; set; } + public string? DidDownloadUrl { get; set; } + public string? Did { get; set; } + public string? ApplicationId { get; set; } + public Guid? CompanyId { get; set; } + public string? ApplicationKey { get; set; } + public Guid OperatorId { get; set; } + public virtual Process? Process { get; set; } +} diff --git a/src/database/Dim.Entities/Entities/VerifyProcessData.cs b/src/database/Dim.Entities/Entities/VerifyProcessData.cs new file mode 100644 index 0000000..01805b3 --- /dev/null +++ b/src/database/Dim.Entities/Entities/VerifyProcessData.cs @@ -0,0 +1,26 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Entities.Entities; + +public record VerifyProcessData( + Process? Process, + IEnumerable? ProcessSteps +); diff --git a/src/database/Dim.Entities/Enums/ProcessStepStatusId.cs b/src/database/Dim.Entities/Enums/ProcessStepStatusId.cs new file mode 100644 index 0000000..061e605 --- /dev/null +++ b/src/database/Dim.Entities/Enums/ProcessStepStatusId.cs @@ -0,0 +1,30 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Entities.Enums; + +public enum ProcessStepStatusId +{ + TODO = 1, + DONE = 2, + SKIPPED = 3, + FAILED = 4, + DUPLICATE = 5 +} diff --git a/src/database/Dim.Entities/Enums/ProcessStepTypeId.cs b/src/database/Dim.Entities/Enums/ProcessStepTypeId.cs new file mode 100644 index 0000000..918e0c7 --- /dev/null +++ b/src/database/Dim.Entities/Enums/ProcessStepTypeId.cs @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Entities.Enums; + +public enum ProcessStepTypeId +{ + // Setup Dim Process + CREATE_SUBACCOUNT = 1, + CREATE_SERVICEMANAGER_BINDINGS = 2, + ASSIGN_ENTITLEMENTS = 3, + CREATE_SERVICE_INSTANCE = 4, + CREATE_SERVICE_BINDING = 5, + SUBSCRIBE_APPLICATION = 6, + CREATE_CLOUD_FOUNDRY_ENVIRONMENT = 7, + CREATE_CLOUD_FOUNDRY_SPACE = 8, + ADD_SPACE_MANAGER_ROLE = 9, + ADD_SPACE_DEVELOPER_ROLE = 10, + CREATE_DIM_SERVICE_INSTANCE = 11, + CREATE_SERVICE_INSTANCE_BINDING = 12, + GET_DIM_DETAILS = 13, + CREATE_APPLICATION = 14, + CREATE_COMPANY_IDENTITY = 15, + ASSIGN_COMPANY_APPLICATION = 16, + CREATE_STATUS_LIST = 17, + SEND_CALLBACK = 18 +} diff --git a/src/database/Dim.Entities/Enums/ProcessTypeId.cs b/src/database/Dim.Entities/Enums/ProcessTypeId.cs new file mode 100644 index 0000000..094f435 --- /dev/null +++ b/src/database/Dim.Entities/Enums/ProcessTypeId.cs @@ -0,0 +1,26 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Entities.Enums; + +public enum ProcessTypeId +{ + SETUP_DIM = 1 +} diff --git a/src/database/Dim.Migrations/Dim.Migrations.csproj b/src/database/Dim.Migrations/Dim.Migrations.csproj new file mode 100644 index 0000000..240e8b3 --- /dev/null +++ b/src/database/Dim.Migrations/Dim.Migrations.csproj @@ -0,0 +1,68 @@ + + + + + + Dim.Migrations + Dim.Migrations + net8.0 + enable + enable + Linux + ..\..\.. + True + Exe + + true + 2f0a8ca8-0f29-4d24-a9cc-792453cf4114 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + Always + + + + + + + + + + + + diff --git a/src/database/Dim.Migrations/Migrations/20240403114716_Initial.Designer.cs b/src/database/Dim.Migrations/Migrations/20240403114716_Initial.Designer.cs new file mode 100644 index 0000000..58dcfef --- /dev/null +++ b/src/database/Dim.Migrations/Migrations/20240403114716_Initial.Designer.cs @@ -0,0 +1,463 @@ +/******************************************************************************** + * 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("20240403114716_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("dim") + .UseCollation("en_US.utf8") + .HasAnnotation("ProductVersion", "7.0.13") + .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" + }); + }); + + 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" + }); + }); + + 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.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("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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/database/Dim.Migrations/Migrations/20240403114716_Initial.cs b/src/database/Dim.Migrations/Migrations/20240403114716_Initial.cs new file mode 100644 index 0000000..c8a1ea6 --- /dev/null +++ b/src/database/Dim.Migrations/Migrations/20240403114716_Initial.cs @@ -0,0 +1,275 @@ +/******************************************************************************** + * 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; +using System; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Dim.Migrations.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "dim"); + + migrationBuilder.CreateTable( + name: "process_step_statuses", + schema: "dim", + columns: table => new + { + id = table.Column(type: "integer", nullable: false), + label = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_process_step_statuses", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "process_step_types", + schema: "dim", + columns: table => new + { + id = table.Column(type: "integer", nullable: false), + label = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_process_step_types", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "process_types", + schema: "dim", + columns: table => new + { + id = table.Column(type: "integer", nullable: false), + label = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_process_types", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "processes", + schema: "dim", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + process_type_id = table.Column(type: "integer", nullable: false), + lock_expiry_date = table.Column(type: "timestamp with time zone", nullable: true), + version = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_processes", x => x.id); + table.ForeignKey( + name: "fk_processes_process_types_process_type_id", + column: x => x.process_type_id, + principalSchema: "dim", + principalTable: "process_types", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "process_steps", + schema: "dim", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + process_step_type_id = table.Column(type: "integer", nullable: false), + process_step_status_id = table.Column(type: "integer", nullable: false), + process_id = table.Column(type: "uuid", nullable: false), + date_created = table.Column(type: "timestamp with time zone", nullable: false), + date_last_changed = table.Column(type: "timestamp with time zone", nullable: true), + message = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_process_steps", x => x.id); + table.ForeignKey( + name: "fk_process_steps_process_step_statuses_process_step_status_id", + column: x => x.process_step_status_id, + principalSchema: "dim", + principalTable: "process_step_statuses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_process_steps_process_step_types_process_step_type_id", + column: x => x.process_step_type_id, + principalSchema: "dim", + principalTable: "process_step_types", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_process_steps_processes_process_id", + column: x => x.process_id, + principalSchema: "dim", + principalTable: "processes", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "tenants", + schema: "dim", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + company_name = table.Column(type: "text", nullable: false), + bpn = table.Column(type: "text", nullable: false), + did_document_location = table.Column(type: "text", nullable: false), + is_issuer = table.Column(type: "boolean", nullable: false), + process_id = table.Column(type: "uuid", nullable: false), + sub_account_id = table.Column(type: "uuid", nullable: true), + service_instance_id = table.Column(type: "text", nullable: true), + service_binding_name = table.Column(type: "text", nullable: true), + space_id = table.Column(type: "uuid", nullable: true), + dim_instance_id = table.Column(type: "uuid", nullable: true), + did_download_url = table.Column(type: "text", nullable: true), + did = table.Column(type: "text", nullable: true), + application_id = table.Column(type: "text", nullable: true), + company_id = table.Column(type: "uuid", nullable: true), + application_key = table.Column(type: "text", nullable: true), + operator_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tenants", x => x.id); + table.ForeignKey( + name: "fk_tenants_processes_process_id", + column: x => x.process_id, + principalSchema: "dim", + principalTable: "processes", + principalColumn: "id"); + }); + + migrationBuilder.InsertData( + schema: "dim", + table: "process_step_statuses", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "TODO" }, + { 2, "DONE" }, + { 3, "SKIPPED" }, + { 4, "FAILED" }, + { 5, "DUPLICATE" } + }); + + migrationBuilder.InsertData( + schema: "dim", + table: "process_step_types", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "CREATE_SUBACCOUNT" }, + { 2, "CREATE_SERVICEMANAGER_BINDINGS" }, + { 3, "ASSIGN_ENTITLEMENTS" }, + { 4, "CREATE_SERVICE_INSTANCE" }, + { 5, "CREATE_SERVICE_BINDING" }, + { 6, "SUBSCRIBE_APPLICATION" }, + { 7, "CREATE_CLOUD_FOUNDRY_ENVIRONMENT" }, + { 8, "CREATE_CLOUD_FOUNDRY_SPACE" }, + { 9, "ADD_SPACE_MANAGER_ROLE" }, + { 10, "ADD_SPACE_DEVELOPER_ROLE" }, + { 11, "CREATE_DIM_SERVICE_INSTANCE" }, + { 12, "CREATE_SERVICE_INSTANCE_BINDING" }, + { 13, "GET_DIM_DETAILS" }, + { 14, "CREATE_APPLICATION" }, + { 15, "CREATE_COMPANY_IDENTITY" }, + { 16, "ASSIGN_COMPANY_APPLICATION" }, + { 17, "CREATE_STATUS_LIST" }, + { 18, "SEND_CALLBACK" } + }); + + migrationBuilder.InsertData( + schema: "dim", + table: "process_types", + columns: new[] { "id", "label" }, + values: new object[] { 1, "SETUP_DIM" }); + + migrationBuilder.CreateIndex( + name: "ix_process_steps_process_id", + schema: "dim", + table: "process_steps", + column: "process_id"); + + migrationBuilder.CreateIndex( + name: "ix_process_steps_process_step_status_id", + schema: "dim", + table: "process_steps", + column: "process_step_status_id"); + + migrationBuilder.CreateIndex( + name: "ix_process_steps_process_step_type_id", + schema: "dim", + table: "process_steps", + column: "process_step_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_processes_process_type_id", + schema: "dim", + table: "processes", + column: "process_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_tenants_process_id", + schema: "dim", + table: "tenants", + column: "process_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "process_steps", + schema: "dim"); + + migrationBuilder.DropTable( + name: "tenants", + schema: "dim"); + + migrationBuilder.DropTable( + name: "process_step_statuses", + schema: "dim"); + + migrationBuilder.DropTable( + name: "process_step_types", + schema: "dim"); + + migrationBuilder.DropTable( + name: "processes", + schema: "dim"); + + migrationBuilder.DropTable( + name: "process_types", + schema: "dim"); + } + } +} diff --git a/src/database/Dim.Migrations/Migrations/DimDbContextModelSnapshot.cs b/src/database/Dim.Migrations/Migrations/DimDbContextModelSnapshot.cs new file mode 100644 index 0000000..b38f7fc --- /dev/null +++ b/src/database/Dim.Migrations/Migrations/DimDbContextModelSnapshot.cs @@ -0,0 +1,456 @@ +/******************************************************************************** + * 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.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Dim.Migrations.Migrations +{ + [DbContext(typeof(DimDbContext))] + partial class DimDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("dim") + .UseCollation("en_US.utf8") + .HasAnnotation("ProductVersion", "7.0.13") + .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" + }); + }); + + 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" + }); + }); + + 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.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("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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/database/Dim.Migrations/Program.cs b/src/database/Dim.Migrations/Program.cs new file mode 100644 index 0000000..ccc4463 --- /dev/null +++ b/src/database/Dim.Migrations/Program.cs @@ -0,0 +1,59 @@ +/******************************************************************************** + * 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.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Seeding.DependencyInjection; +using Serilog; +using System.Reflection; + +LoggingExtensions.EnsureInitialized(); +Log.Information("Starting process"); +try +{ + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services + .AddDatabaseInitializer(hostContext.Configuration.GetSection("Seeding")) + .AddDbContext(o => + o.UseNpgsql(hostContext.Configuration.GetConnectionString("DimDb"), + x => x.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name) + .MigrationsHistoryTable("__efmigrations_history_dim"))); + }) + .AddLogging() + .Build(); + + await host.Services.InitializeDatabasesAsync(); // We don't actually run anything here. The magic happens in InitializeDatabasesAsync +} +catch (Exception ex) when (!ex.GetType().Name.Equals("StopTheHostException", StringComparison.Ordinal)) +{ + Log.Fatal("Unhandled exception {Exception}", ex); + throw; +} +finally +{ + Log.Information("Process Shutting down..."); + Log.CloseAndFlush(); +} diff --git a/src/database/Dim.Migrations/Properties/launchSettings.json b/src/database/Dim.Migrations/Properties/launchSettings.json new file mode 100644 index 0000000..f334184 --- /dev/null +++ b/src/database/Dim.Migrations/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Dim.Migrations": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/database/Dim.Migrations/appsettings.json b/src/database/Dim.Migrations/appsettings.json new file mode 100644 index 0000000..39a2b97 --- /dev/null +++ b/src/database/Dim.Migrations/appsettings.json @@ -0,0 +1,34 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "Dim.Migrations": "Warning" + } + }, + "WriteTo": [ + { "Name": "Console" } + ], + "Enrich": [ + "FromLogContext", + "WithMachineName", + "WithProcessId", + "WithThreadId" + ], + "Properties": { + "Application": "Dim.Migrations" + } + }, + "ConnectionStrings": { + "DimDb": "Server=placeholder;Database=placeholder;Port=5432;User Id=placeholder;Password=placeholder;Ssl Mode=Disable;" + }, + "Seeding": { + "DataPaths": [ + ], + "TestDataEnvironments": [] + } +} diff --git a/src/processes/DimProcess.Executor/DependencyInjection/DimProcessCollectionExtensions.cs b/src/processes/DimProcess.Executor/DependencyInjection/DimProcessCollectionExtensions.cs new file mode 100644 index 0000000..ffc5171 --- /dev/null +++ b/src/processes/DimProcess.Executor/DependencyInjection/DimProcessCollectionExtensions.cs @@ -0,0 +1,34 @@ +/******************************************************************************** + * 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 DimProcess.Library.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +namespace DimProcess.Executor.DependencyInjection; + +public static class DimProcessCollectionExtensions +{ + public static IServiceCollection AddDimProcessExecutor(this IServiceCollection services, IConfiguration config) => + services + .AddTransient() + .AddDimProcessHandler(config); +} diff --git a/src/processes/DimProcess.Executor/DimProcess.Executor.csproj b/src/processes/DimProcess.Executor/DimProcess.Executor.csproj new file mode 100644 index 0000000..0cf1f2c --- /dev/null +++ b/src/processes/DimProcess.Executor/DimProcess.Executor.csproj @@ -0,0 +1,41 @@ + + + + + + DimProcess.Executor + DimProcess.Executor + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/processes/DimProcess.Executor/DimProcessTypeExecutor.cs b/src/processes/DimProcess.Executor/DimProcessTypeExecutor.cs new file mode 100644 index 0000000..df7c612 --- /dev/null +++ b/src/processes/DimProcess.Executor/DimProcessTypeExecutor.cs @@ -0,0 +1,157 @@ +/******************************************************************************** + * 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.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Enums; +using DimProcess.Library; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; +using System.Collections.Immutable; + +namespace DimProcess.Executor; + +public class DimProcessTypeExecutor : IProcessTypeExecutor +{ + private readonly IDimRepositories _dimRepositories; + private readonly IDimProcessHandler _dimProcessHandler; + + private readonly IEnumerable _executableProcessSteps = ImmutableArray.Create( + ProcessStepTypeId.CREATE_SUBACCOUNT, + ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS, + ProcessStepTypeId.ASSIGN_ENTITLEMENTS, + ProcessStepTypeId.CREATE_SERVICE_INSTANCE, + ProcessStepTypeId.CREATE_SERVICE_BINDING, + ProcessStepTypeId.SUBSCRIBE_APPLICATION, + ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT, + ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE, + ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE, + ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE, + ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE, + ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING, + ProcessStepTypeId.GET_DIM_DETAILS, + ProcessStepTypeId.CREATE_APPLICATION, + ProcessStepTypeId.CREATE_COMPANY_IDENTITY, + ProcessStepTypeId.CREATE_STATUS_LIST, + ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION, + ProcessStepTypeId.SEND_CALLBACK); + + private Guid _tenantId; + private string? _tenantName; + + public DimProcessTypeExecutor( + IDimRepositories dimRepositories, + IDimProcessHandler dimProcessHandler) + { + _dimRepositories = dimRepositories; + _dimProcessHandler = dimProcessHandler; + } + + public ProcessTypeId GetProcessTypeId() => ProcessTypeId.SETUP_DIM; + public bool IsExecutableStepTypeId(ProcessStepTypeId processStepTypeId) => _executableProcessSteps.Contains(processStepTypeId); + public IEnumerable GetExecutableStepTypeIds() => _executableProcessSteps; + public ValueTask IsLockRequested(ProcessStepTypeId processStepTypeId) => new(false); + + public async ValueTask InitializeProcess(Guid processId, IEnumerable processStepTypeIds) + { + var (exists, tenantId, companyName, bpn) = await _dimRepositories.GetInstance().GetTenantDataForProcessId(processId).ConfigureAwait(false); + if (!exists) + { + throw new NotFoundException($"process {processId} does not exist or is not associated with an tenant"); + } + + _tenantId = tenantId; + _tenantName = $"{bpn}_{companyName}"; + return new IProcessTypeExecutor.InitializationResult(false, null); + } + + public async ValueTask ExecuteProcessStep(ProcessStepTypeId processStepTypeId, IEnumerable processStepTypeIds, CancellationToken cancellationToken) + { + if (_tenantId == Guid.Empty || _tenantName is null) + { + throw new UnexpectedConditionException("tenantId and tenantName should never be empty here"); + } + + IEnumerable? nextStepTypeIds; + ProcessStepStatusId stepStatusId; + bool modified; + string? processMessage; + + try + { + (nextStepTypeIds, stepStatusId, modified, processMessage) = processStepTypeId switch + { + ProcessStepTypeId.CREATE_SUBACCOUNT => await _dimProcessHandler.CreateSubaccount(_tenantId, _tenantName, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS => await _dimProcessHandler.CreateServiceManagerBindings(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.ASSIGN_ENTITLEMENTS => await _dimProcessHandler.AssignEntitlements(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_SERVICE_INSTANCE => await _dimProcessHandler.CreateServiceInstance(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_SERVICE_BINDING => await _dimProcessHandler.CreateServiceBindings(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.SUBSCRIBE_APPLICATION => await _dimProcessHandler.SubscribeApplication(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT => await _dimProcessHandler.CreateCloudFoundryEnvironment(_tenantId, _tenantName, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE => await _dimProcessHandler.CreateCloudFoundrySpace(_tenantId, _tenantName, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE => await _dimProcessHandler.AddSpaceManagerRole(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE => await _dimProcessHandler.AddSpaceDeveloperRole(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE => await _dimProcessHandler.CreateDimServiceInstance(_tenantName, _tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING => await _dimProcessHandler.CreateServiceInstanceBindings(_tenantName, _tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.GET_DIM_DETAILS => await _dimProcessHandler.GetDimDetails(_tenantName, _tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_APPLICATION => await _dimProcessHandler.CreateApplication(_tenantName, _tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_COMPANY_IDENTITY => await _dimProcessHandler.CreateCompanyIdentity(_tenantId, _tenantName, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION => await _dimProcessHandler.AssignCompanyApplication(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_STATUS_LIST => await _dimProcessHandler.CreateStatusList(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.SEND_CALLBACK => await _dimProcessHandler.SendCallback(_tenantId, cancellationToken) + .ConfigureAwait(false), + _ => (null, ProcessStepStatusId.TODO, false, null) + }; + } + catch (Exception ex) when (ex is not SystemException) + { + (stepStatusId, processMessage, nextStepTypeIds) = ProcessError(ex); + modified = true; + } + + return new IProcessTypeExecutor.StepExecutionResult(modified, stepStatusId, nextStepTypeIds, null, processMessage); + } + + private static (ProcessStepStatusId StatusId, string? ProcessMessage, IEnumerable? nextSteps) ProcessError(Exception ex) + { + return ex switch + { + ServiceException { IsRecoverable: true } => (ProcessStepStatusId.TODO, ex.Message, null), + _ => (ProcessStepStatusId.FAILED, ex.Message, null) + }; + } +} diff --git a/src/processes/DimProcess.Library/Callback/CallbackDataModel.cs b/src/processes/DimProcess.Library/Callback/CallbackDataModel.cs new file mode 100644 index 0000000..f3e5383 --- /dev/null +++ b/src/processes/DimProcess.Library/Callback/CallbackDataModel.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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.Text.Json; +using System.Text.Json.Serialization; + +namespace DimProcess.Library.Callback; + +public record CallbackDataModel( + [property: JsonPropertyName("did")] string Did, + [property: JsonPropertyName("didDocument")] JsonDocument DidDocument, + [property: JsonPropertyName("authenticationDetails")] AuthenticationDetail AuthenticationDetails +); + +public record AuthenticationDetail( + [property: JsonPropertyName("authenticationServiceUrl")] string AuthenticationServiceUrl, + [property: JsonPropertyName("clientID")] string ClientId, + [property: JsonPropertyName("clientSecret")] string ClientSecret +); diff --git a/src/processes/DimProcess.Library/Callback/CallbackService.cs b/src/processes/DimProcess.Library/Callback/CallbackService.cs new file mode 100644 index 0000000..e1dff18 --- /dev/null +++ b/src/processes/DimProcess.Library/Callback/CallbackService.cs @@ -0,0 +1,56 @@ +/******************************************************************************** + * 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.Clients.Extensions; +using DimProcess.Library.Callback.DependencyInjection; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Token; +using System.Net.Http.Json; +using System.Text.Json; + +namespace DimProcess.Library.Callback; + +public class CallbackService : ICallbackService +{ + private readonly ITokenService _tokenService; + private readonly CallbackSettings _settings; + + public CallbackService(ITokenService tokenService, IOptions options) + { + _tokenService = tokenService; + _settings = options.Value; + } + + public async Task SendCallback(string bpn, ServiceCredentialBindingDetailResponse dimDetails, JsonDocument didDocument, string did, CancellationToken cancellationToken) + { + var httpClient = await _tokenService.GetAuthorizedClient(_settings, cancellationToken) + .ConfigureAwait(false); + var data = new CallbackDataModel( + did, + didDocument, + new AuthenticationDetail( + dimDetails.Credentials.Uaa.Url, + dimDetails.Credentials.Uaa.ClientId, + dimDetails.Credentials.Uaa.ClientSecret) + ); + await httpClient.PostAsJsonAsync($"{bpn}", data, JsonSerializerExtensions.Options, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackServiceExtensions.cs b/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackServiceExtensions.cs new file mode 100644 index 0000000..1547b7a --- /dev/null +++ b/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackServiceExtensions.cs @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace DimProcess.Library.Callback.DependencyInjection; + +public static class CallbackServiceExtensions +{ + public static IServiceCollection AddCallbackClient(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + services + .AddCustomHttpClientWithAuthentication(settings.Value.BaseAddress, settings.Value.TokenAddress) + .AddTransient(); + + return services; + } +} diff --git a/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackSettings.cs b/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackSettings.cs new file mode 100644 index 0000000..a340ee7 --- /dev/null +++ b/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackSettings.cs @@ -0,0 +1,28 @@ +/******************************************************************************** + * 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 Org.Eclipse.TractusX.Portal.Backend.Framework.Token; + +namespace DimProcess.Library.Callback.DependencyInjection; + +public class CallbackSettings : KeyVaultAuthSettings +{ + public string BaseAddress { get; set; } = null!; +} diff --git a/src/processes/DimProcess.Library/Callback/ICallbackService.cs b/src/processes/DimProcess.Library/Callback/ICallbackService.cs new file mode 100644 index 0000000..48ac77c --- /dev/null +++ b/src/processes/DimProcess.Library/Callback/ICallbackService.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * 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 System.Text.Json; + +namespace DimProcess.Library.Callback; + +public interface ICallbackService +{ + Task SendCallback(string bpn, ServiceCredentialBindingDetailResponse dimDetails, JsonDocument didDocument, string did, CancellationToken cancellationToken); +} diff --git a/src/processes/DimProcess.Library/DependencyInjection/DimHandlerExtensions.cs b/src/processes/DimProcess.Library/DependencyInjection/DimHandlerExtensions.cs new file mode 100644 index 0000000..bb67cba --- /dev/null +++ b/src/processes/DimProcess.Library/DependencyInjection/DimHandlerExtensions.cs @@ -0,0 +1,57 @@ +/******************************************************************************** + * 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.DependencyInjection; +using Dim.Clients.Api.Dim.DependencyInjection; +using Dim.Clients.Api.Entitlements.DependencyInjection; +using Dim.Clients.Api.Provisioning.DependencyInjection; +using Dim.Clients.Api.Services.DependencyInjection; +using Dim.Clients.Api.SubAccounts.DependencyInjection; +using Dim.Clients.Api.Subscriptions.DependencyInjection; +using Dim.Clients.Token; +using DimProcess.Library.Callback.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace DimProcess.Library.DependencyInjection; + +public static class DimHandlerExtensions +{ + public static IServiceCollection AddDimProcessHandler(this IServiceCollection services, IConfiguration config) + { + services.AddOptions() + .Bind(config.GetSection("Dim")) + .ValidateOnStart(); + + services + .AddTransient() + .AddTransient() + .AddSubAccountClient(config.GetSection("SubAccount")) + .AddEntitlementClient(config.GetSection("Entitlement")) + .AddServiceClient() + .AddSubscriptionClient() + .AddProvisioningClient() + .AddCfClient(config.GetSection("Cf")) + .AddDimClient() + .AddCallbackClient(config.GetSection("Callback")); + + return services; + } +} diff --git a/src/processes/DimProcess.Library/DependencyInjection/DimHandlerSettings.cs b/src/processes/DimProcess.Library/DependencyInjection/DimHandlerSettings.cs new file mode 100644 index 0000000..6b09d1d --- /dev/null +++ b/src/processes/DimProcess.Library/DependencyInjection/DimHandlerSettings.cs @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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.ComponentModel.DataAnnotations; + +namespace DimProcess.Library.DependencyInjection; + +public class DimHandlerSettings +{ + [Required(AllowEmptyStrings = false)] + public string AdminMail { get; set; } = null!; + + [Required] + public Guid RootDirectoryId { get; set; } + + [Required(AllowEmptyStrings = false)] + public string AuthUrl { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string ClientidCisCentral { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string ClientsecretCisCentral { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string EncryptionKey { get; set; } = null!; +} diff --git a/src/processes/DimProcess.Library/DimProcess.Library.csproj b/src/processes/DimProcess.Library/DimProcess.Library.csproj new file mode 100644 index 0000000..f0d4e8b --- /dev/null +++ b/src/processes/DimProcess.Library/DimProcess.Library.csproj @@ -0,0 +1,44 @@ + + + + + + DimProcess.Library + DimProcess.Library + net8.0 + enable + enable + 415ef31b-ed90-4c9e-9e5f-5f3e673d4f91 + + + + + + + + + + + + + + + diff --git a/src/processes/DimProcess.Library/DimProcessHandler.cs b/src/processes/DimProcess.Library/DimProcessHandler.cs new file mode 100644 index 0000000..7453305 --- /dev/null +++ b/src/processes/DimProcess.Library/DimProcessHandler.cs @@ -0,0 +1,566 @@ +/******************************************************************************** + * 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.Clients.Api.Dim; +using Dim.Clients.Api.Entitlements; +using Dim.Clients.Api.Provisioning; +using Dim.Clients.Api.Services; +using Dim.Clients.Api.SubAccounts; +using Dim.Clients.Api.Subscriptions; +using Dim.Clients.Token; +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Enums; +using DimProcess.Library.Callback; +using DimProcess.Library.DependencyInjection; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace DimProcess.Library; + +public class DimProcessHandler : IDimProcessHandler +{ + private readonly IDimRepositories _dimRepositories; + private readonly ISubAccountClient _subAccountClient; + private readonly IServiceClient _serviceClient; + private readonly IEntitlementClient _entitlementClient; + private readonly ISubscriptionClient _subscriptionClient; + private readonly IProvisioningClient _provisioningClient; + private readonly ICfClient _cfClient; + private readonly IDimClient _dimClient; + private readonly ICallbackService _callbackService; + private readonly DimHandlerSettings _settings; + + public DimProcessHandler( + IDimRepositories dimRepositories, + ISubAccountClient subAccountClient, + IServiceClient serviceClient, + ISubscriptionClient subscriptionClient, + IEntitlementClient entitlementClient, + IProvisioningClient provisioningClient, + ICfClient cfClient, + IDimClient dimClient, + ICallbackService callbackService, + IOptions options) + { + _dimRepositories = dimRepositories; + _subAccountClient = subAccountClient; + _serviceClient = serviceClient; + _entitlementClient = entitlementClient; + _subscriptionClient = subscriptionClient; + _provisioningClient = provisioningClient; + _cfClient = cfClient; + _dimClient = dimClient; + _callbackService = callbackService; + _settings = options.Value; + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateSubaccount(Guid tenantId, string tenantName, CancellationToken cancellationToken) + { + var parentDirectoryId = _settings.RootDirectoryId; + var adminMail = _settings.AdminMail; + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + + var subAccountId = await _subAccountClient.CreateSubaccount(subAccountAuth, adminMail, tenantName, parentDirectoryId, cancellationToken).ConfigureAwait(false); + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.SubAccountId = null; + }, + tenant => + { + tenant.SubAccountId = subAccountId; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceManagerBindings(Guid tenantId, CancellationToken cancellationToken) + { + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + + var tenantRepository = _dimRepositories.GetInstance(); + var subAccountId = await tenantRepository.GetSubAccountIdByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + await _subAccountClient.CreateServiceManagerBindings(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.ASSIGN_ENTITLEMENTS, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AssignEntitlements(Guid tenantId, CancellationToken cancellationToken) + { + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + var subAccountId = await _dimRepositories.GetInstance().GetSubAccountIdByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + await _entitlementClient.AssignEntitlements(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_SERVICE_INSTANCE, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceInstance(Guid tenantId, CancellationToken cancellationToken) + { + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + var subAccountId = await _dimRepositories.GetInstance().GetSubAccountIdByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + var saBinding = await _subAccountClient.GetServiceManagerBindings(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + var serviceInstance = await _serviceClient.CreateServiceInstance(saBinding, cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.ServiceInstanceId = null; + }, + tenant => + { + tenant.ServiceInstanceId = serviceInstance.Id; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_SERVICE_BINDING, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceBindings(Guid tenantId, CancellationToken cancellationToken) + { + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + var (subAccountId, serviceInstanceId) = await _dimRepositories.GetInstance().GetSubAccountAndServiceInstanceIdsByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + if (string.IsNullOrEmpty(serviceInstanceId)) + { + throw new ConflictException("ServiceInstanceId must not be null."); + } + + var saBinding = await _subAccountClient.GetServiceManagerBindings(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + var serviceBinding = await _serviceClient.CreateServiceBinding(saBinding, serviceInstanceId, cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.ServiceBindingName = null; + }, + tenant => + { + tenant.ServiceBindingName = serviceBinding.Name; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.SUBSCRIBE_APPLICATION, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SubscribeApplication(Guid tenantId, CancellationToken cancellationToken) + { + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + var (subAccountId, serviceBindingName) = await _dimRepositories.GetInstance().GetSubAccountIdAndServiceBindingNameByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + if (string.IsNullOrEmpty(serviceBindingName)) + { + throw new ConflictException("ServiceBindingName must not be null."); + } + + var saBinding = await _subAccountClient.GetServiceManagerBindings(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + var bindingResponse = await _serviceClient.GetServiceBinding(saBinding, serviceBindingName, cancellationToken).ConfigureAwait(false); + await _subscriptionClient.SubscribeApplication(saBinding.Url, bindingResponse, "decentralized-identity-management-app", "standard", cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCloudFoundryEnvironment(Guid tenantId, string tenantName, CancellationToken cancellationToken) + { + var adminMail = _settings.AdminMail; + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + var (subAccountId, serviceBindingName) = await _dimRepositories.GetInstance().GetSubAccountIdAndServiceBindingNameByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + if (string.IsNullOrEmpty(serviceBindingName)) + { + throw new ConflictException("ServiceBindingName must not be null."); + } + + var saBinding = await _subAccountClient.GetServiceManagerBindings(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + var bindingResponse = await _serviceClient.GetServiceBinding(saBinding, serviceBindingName, cancellationToken).ConfigureAwait(false); + await _provisioningClient.CreateCloudFoundryEnvironment(saBinding.Url, bindingResponse, tenantName, adminMail, cancellationToken) + .ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCloudFoundrySpace(Guid tenantId, string tenantName, CancellationToken cancellationToken) + { + var spaceId = await _cfClient.CreateCloudFoundrySpace(tenantName, cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.SpaceId = null; + }, + tenant => + { + tenant.SpaceId = spaceId; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AddSpaceManagerRole(Guid tenantId, CancellationToken cancellationToken) + { + var adminMail = _settings.AdminMail; + var spaceId = await _dimRepositories.GetInstance().GetSpaceId(tenantId).ConfigureAwait(false); + if (spaceId == null) + { + throw new ConflictException("SpaceId must not be null."); + } + + await _cfClient.AddSpaceRoleToUser("space_manager", adminMail, spaceId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AddSpaceDeveloperRole(Guid tenantId, CancellationToken cancellationToken) + { + var adminMail = _settings.AdminMail; + var spaceId = await _dimRepositories.GetInstance().GetSpaceId(tenantId).ConfigureAwait(false); + if (spaceId == null) + { + throw new ConflictException("SpaceId must not be null."); + } + + await _cfClient.AddSpaceRoleToUser("space_developer", adminMail, spaceId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateDimServiceInstance(string tenantName, Guid tenantId, CancellationToken cancellationToken) + { + var servicePlanId = await _cfClient.GetServicePlan("decentralized-identity-management", "standard", cancellationToken).ConfigureAwait(false); + var spaceId = await _cfClient.GetSpace(tenantName, cancellationToken).ConfigureAwait(false); + await _cfClient.CreateDimServiceInstance(tenantName, spaceId, servicePlanId, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceInstanceBindings(string tenantName, Guid tenantId, CancellationToken cancellationToken) + { + var spaceId = await _dimRepositories.GetInstance().GetSpaceId(tenantId).ConfigureAwait(false); + if (spaceId == null) + { + throw new ConflictException("SpaceId must not be null."); + } + + await _cfClient.CreateServiceInstanceBindings(tenantName, spaceId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.GET_DIM_DETAILS, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> GetDimDetails(string tenantName, Guid tenantId, CancellationToken cancellationToken) + { + var spaceId = await _dimRepositories.GetInstance().GetSpaceId(tenantId).ConfigureAwait(false); + if (spaceId == null) + { + throw new ConflictException("SpaceId must not be null."); + } + + var dimInstanceId = await _cfClient.GetServiceBinding(tenantName, spaceId.Value, $"{tenantName}-dim-key01", cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.DimInstanceId = null; + }, + tenant => + { + tenant.DimInstanceId = dimInstanceId; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_APPLICATION, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateApplication(string tenantName, Guid tenantId, CancellationToken cancellationToken) + { + var (dimInstanceId, _, _) = await _dimRepositories.GetInstance().GetDimInstanceIdAndHostingUrl(tenantId).ConfigureAwait(false); + if (dimInstanceId == null) + { + throw new ConflictException("DimInstanceId must not be null."); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(dimInstanceId.Value, cancellationToken).ConfigureAwait(false); + + var dimAuth = new BasicAuthSettings + { + TokenAddress = $"{dimDetails.Credentials.Uaa.Url}/oauth/token", + ClientId = dimDetails.Credentials.Uaa.ClientId, + ClientSecret = dimDetails.Credentials.Uaa.ClientSecret + }; + var dimBaseUrl = dimDetails.Credentials.Url; + var applicationId = await _dimClient.CreateApplication(dimAuth, dimBaseUrl, tenantName, cancellationToken).ConfigureAwait(false); + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.ApplicationId = null; + }, + tenant => + { + tenant.ApplicationId = applicationId; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_COMPANY_IDENTITY, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCompanyIdentity(Guid tenantId, string tenantName, CancellationToken cancellationToken) + { + var (dimInstanceId, hostingUrl, isIssuer) = await _dimRepositories.GetInstance().GetDimInstanceIdAndHostingUrl(tenantId).ConfigureAwait(false); + if (dimInstanceId == null) + { + throw new ConflictException("DimInstanceId must not be null."); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(dimInstanceId.Value, cancellationToken).ConfigureAwait(false); + + var dimAuth = new BasicAuthSettings + { + TokenAddress = $"{dimDetails.Credentials.Uaa.Url}/oauth/token", + ClientId = dimDetails.Credentials.Uaa.ClientId, + ClientSecret = dimDetails.Credentials.Uaa.ClientSecret + }; + var dimBaseUrl = dimDetails.Credentials.Url; + var result = await _dimClient.CreateCompanyIdentity(dimAuth, hostingUrl, dimBaseUrl, tenantName, isIssuer, cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.DidDownloadUrl = null; + tenant.Did = null; + tenant.CompanyId = null; + }, + tenant => + { + tenant.DidDownloadUrl = result.DownloadUrl; + tenant.Did = result.Did; + tenant.CompanyId = result.CompanyId; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AssignCompanyApplication(Guid tenantId, CancellationToken cancellationToken) + { + var (applicationId, companyId, dimInstanceId, isIssuer) = await _dimRepositories.GetInstance().GetApplicationAndCompanyId(tenantId).ConfigureAwait(false); + if (applicationId == null) + { + throw new ConflictException("ApplicationId must always be set here"); + } + + if (companyId == null) + { + throw new ConflictException("CompanyId must always be set here"); + } + + if (dimInstanceId == null) + { + throw new ConflictException("DimInstanceId must not be null."); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(dimInstanceId.Value, cancellationToken).ConfigureAwait(false); + var dimAuth = new BasicAuthSettings + { + TokenAddress = $"{dimDetails.Credentials.Uaa.Url}/oauth/token", + ClientId = dimDetails.Credentials.Uaa.ClientId, + ClientSecret = dimDetails.Credentials.Uaa.ClientSecret + }; + var dimBaseUrl = dimDetails.Credentials.Url; + var applicationKey = await _dimClient.GetApplication(dimAuth, dimBaseUrl, applicationId, cancellationToken); + await _dimClient.AssignApplicationToCompany(dimAuth, dimBaseUrl, applicationKey, companyId.Value, cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.ApplicationKey = null; + }, + tenant => + { + tenant.ApplicationKey = applicationKey; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(isIssuer ? ProcessStepTypeId.CREATE_STATUS_LIST : ProcessStepTypeId.SEND_CALLBACK, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateStatusList(Guid tenantId, CancellationToken cancellationToken) + { + var (_, companyId, dimInstanceId, _) = await _dimRepositories.GetInstance().GetApplicationAndCompanyId(tenantId).ConfigureAwait(false); + if (companyId == null) + { + throw new ConflictException("CompanyId must always be set here"); + } + + if (dimInstanceId == null) + { + throw new ConflictException("DimInstanceId must not be null."); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(dimInstanceId.Value, cancellationToken).ConfigureAwait(false); + var dimAuth = new BasicAuthSettings + { + TokenAddress = $"{dimDetails.Credentials.Uaa.Url}/oauth/token", + ClientId = dimDetails.Credentials.Uaa.ClientId, + ClientSecret = dimDetails.Credentials.Uaa.ClientSecret + }; + var dimBaseUrl = dimDetails.Credentials.Url; + await _dimClient.CreateStatusList(dimAuth, dimBaseUrl, companyId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.SEND_CALLBACK, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SendCallback(Guid tenantId, CancellationToken cancellationToken) + { + var (bpn, downloadUrl, did, dimInstanceId) = await _dimRepositories.GetInstance().GetCallbackData(tenantId).ConfigureAwait(false); + if (downloadUrl == null) + { + throw new ConflictException("DownloadUrl must not be null."); + } + + if (did == null) + { + throw new ConflictException("Did must not be null."); + } + + if (dimInstanceId == null) + { + throw new ConflictException("DimInstanceId must not be null."); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(dimInstanceId.Value, cancellationToken).ConfigureAwait(false); + var didDocument = await _dimClient.GetDidDocument(downloadUrl, cancellationToken).ConfigureAwait(false); + + await _callbackService.SendCallback(bpn, dimDetails, didDocument, did, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + null, + ProcessStepStatusId.DONE, + false, + null); + } +} diff --git a/src/processes/DimProcess.Library/IDimProcessHandler.cs b/src/processes/DimProcess.Library/IDimProcessHandler.cs new file mode 100644 index 0000000..a9513b5 --- /dev/null +++ b/src/processes/DimProcess.Library/IDimProcessHandler.cs @@ -0,0 +1,45 @@ +/******************************************************************************** + * 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.Entities.Enums; + +namespace DimProcess.Library; + +public interface IDimProcessHandler +{ + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateSubaccount(Guid tenantId, string tenantName, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceManagerBindings(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AssignEntitlements(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceInstance(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceBindings(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SubscribeApplication(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCloudFoundryEnvironment(Guid tenantId, string tenantName, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCloudFoundrySpace(Guid tenantId, string tenantName, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AddSpaceManagerRole(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AddSpaceDeveloperRole(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateDimServiceInstance(string tenantName, Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceInstanceBindings(string tenantName, Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateApplication(string tenantName, Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> GetDimDetails(string tenantName, Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCompanyIdentity(Guid tenantId, string tenantName, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateStatusList(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AssignCompanyApplication(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SendCallback(Guid tenantId, CancellationToken cancellationToken); +} diff --git a/src/processes/Processes.Library/ManualProcessStepData.cs b/src/processes/Processes.Library/ManualProcessStepData.cs new file mode 100644 index 0000000..5cc0011 --- /dev/null +++ b/src/processes/Processes.Library/ManualProcessStepData.cs @@ -0,0 +1,32 @@ +/******************************************************************************** + * 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.DbAccess; +using Dim.Entities.Entities; +using Dim.Entities.Enums; + +namespace Dim.Processes.Library; + +public record ManualProcessStepData( + ProcessStepTypeId ProcessStepTypeId, + Process Process, + IEnumerable ProcessSteps, + IDimRepositories PortalRepositories +); diff --git a/src/processes/Processes.Library/ManualProcessStepDataExtensions.cs b/src/processes/Processes.Library/ManualProcessStepDataExtensions.cs new file mode 100644 index 0000000..9820578 --- /dev/null +++ b/src/processes/Processes.Library/ManualProcessStepDataExtensions.cs @@ -0,0 +1,139 @@ +/******************************************************************************** + * 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.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace Dim.Processes.Library; + +public static class VerifyProcessDataExtensions +{ + public static ManualProcessStepData CreateManualProcessData( + this VerifyProcessData? processData, + ProcessStepTypeId processStepTypeId, + IDimRepositories portalRepositories, + Func getProcessEntityName) + { + if (processData is null) + { + throw new NotFoundException($"{getProcessEntityName()} does not exist"); + } + + if (processData.Process == null) + { + throw new ConflictException($"{getProcessEntityName()} is not associated with any process"); + } + + if (processData.Process.IsLocked()) + { + throw new ConflictException($"process {processData.Process.Id} associated with {getProcessEntityName()} is locked, lock expiry is set to {processData.Process.LockExpiryDate}"); + } + + if (processData.ProcessSteps == null) + { + throw new UnexpectedConditionException("processSteps should never be null here"); + } + + if (processData.ProcessSteps.Any(step => step.ProcessStepStatusId != ProcessStepStatusId.TODO)) + { + throw new UnexpectedConditionException($"processSteps should never have any other status than TODO here"); + } + + if (processData.ProcessSteps.All(step => step.ProcessStepTypeId != processStepTypeId)) + { + throw new ConflictException($"{getProcessEntityName()}, process step {processStepTypeId} is not eligible to run"); + } + + return new(processStepTypeId, processData.Process, processData.ProcessSteps, portalRepositories); + } +} + +public static class ManualProcessStepDataExtensions +{ + public static void RequestLock(this ManualProcessStepData context, DateTimeOffset lockExpiryDate) + { + context.PortalRepositories.Attach(context.Process); + + var isLocked = context.Process.TryLock(lockExpiryDate); + if (!isLocked) + { + throw new UnexpectedConditionException("process TryLock should never fail here"); + } + } + + public static void SkipProcessSteps(this ManualProcessStepData context, IEnumerable processStepTypeIds) => + context.PortalRepositories.GetInstance() + .AttachAndModifyProcessSteps( + context.ProcessSteps + .Where(step => step.ProcessStepTypeId != context.ProcessStepTypeId) + .GroupBy(step => step.ProcessStepTypeId) + .IntersectBy(processStepTypeIds, group => group.Key) + .SelectMany(group => ModifyStepStatusRange(group, ProcessStepStatusId.SKIPPED))); + + public static void SkipProcessStepsExcept(this ManualProcessStepData context, IEnumerable processStepTypeIds) => + context.PortalRepositories.GetInstance() + .AttachAndModifyProcessSteps( + context.ProcessSteps + .Where(step => step.ProcessStepTypeId != context.ProcessStepTypeId) + .GroupBy(step => step.ProcessStepTypeId) + .ExceptBy(processStepTypeIds, group => group.Key) + .SelectMany(group => ModifyStepStatusRange(group, ProcessStepStatusId.SKIPPED))); + + public static void ScheduleProcessSteps(this ManualProcessStepData context, IEnumerable processStepTypeIds) => + context.PortalRepositories.GetInstance() + .CreateProcessStepRange( + processStepTypeIds + .Except(context.ProcessSteps.Select(step => step.ProcessStepTypeId)) + .Select(stepTypeId => (stepTypeId, ProcessStepStatusId.TODO, context.Process.Id))); + + public static void FinalizeProcessStep(this ManualProcessStepData context) + { + context.PortalRepositories.GetInstance().AttachAndModifyProcessSteps( + ModifyStepStatusRange(context.ProcessSteps.Where(step => step.ProcessStepTypeId == context.ProcessStepTypeId), ProcessStepStatusId.DONE)); + + context.PortalRepositories.Attach(context.Process); + if (!context.Process.ReleaseLock()) + { + context.Process.UpdateVersion(); + } + } + + private static IEnumerable<(Guid, Action?, Action)> ModifyStepStatusRange(IEnumerable steps, ProcessStepStatusId processStepStatusId) + { + var firstStep = steps.FirstOrDefault(); + + if (firstStep == null) + yield break; + + foreach (var step in steps) + { + yield return ( + step.Id, + null, + ps => ps.ProcessStepStatusId = ps.Id == firstStep.Id + ? processStepStatusId + : ProcessStepStatusId.DUPLICATE); + } + } +} diff --git a/src/processes/Processes.Library/Processes.Library.csproj b/src/processes/Processes.Library/Processes.Library.csproj new file mode 100644 index 0000000..5b2a8cc --- /dev/null +++ b/src/processes/Processes.Library/Processes.Library.csproj @@ -0,0 +1,41 @@ + + + + + + Dim.Processes.Library + Dim.Processes.Library + net8.0 + enable + enable + 76a1cf69-39e1-43a7-b6a7-fef83be5359f + + + + + + + + + + + + diff --git a/src/processes/Processes.Worker.Library/IProcessExecutor.cs b/src/processes/Processes.Worker.Library/IProcessExecutor.cs new file mode 100644 index 0000000..88a0b87 --- /dev/null +++ b/src/processes/Processes.Worker.Library/IProcessExecutor.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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.Entities.Enums; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +public interface IProcessExecutor +{ + enum ProcessExecutionResult + { + SaveRequested = 1, + LockRequested = 2, + Unmodified = 3 + } + IAsyncEnumerable ExecuteProcess(Guid processId, ProcessTypeId processTypeId, CancellationToken cancellationToken); + IEnumerable GetRegisteredProcessTypeIds(); + IEnumerable GetExecutableStepTypeIds(); +} diff --git a/src/processes/Processes.Worker.Library/IProcessTypeExecutor.cs b/src/processes/Processes.Worker.Library/IProcessTypeExecutor.cs new file mode 100644 index 0000000..051803a --- /dev/null +++ b/src/processes/Processes.Worker.Library/IProcessTypeExecutor.cs @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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.Entities.Enums; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +public interface IProcessTypeExecutor +{ + record InitializationResult(bool Modified, IEnumerable? ScheduleStepTypeIds); + record StepExecutionResult(bool Modified, ProcessStepStatusId ProcessStepStatusId, IEnumerable? ScheduleStepTypeIds, IEnumerable? SkipStepTypeIds, string? ProcessMessage); + + ValueTask InitializeProcess(Guid processId, IEnumerable processStepTypeIds); + ValueTask IsLockRequested(ProcessStepTypeId processStepTypeId); + + /// + /// tbd + /// + /// + /// + /// + /// Is thrown if entity is not found + /// Is thrown if ... + /// + ValueTask ExecuteProcessStep(ProcessStepTypeId processStepTypeId, IEnumerable processStepTypeIds, CancellationToken cancellationToken); + bool IsExecutableStepTypeId(ProcessStepTypeId processStepTypeId); + ProcessTypeId GetProcessTypeId(); + IEnumerable GetExecutableStepTypeIds(); +} diff --git a/src/processes/Processes.Worker.Library/ProcessExecutionService.cs b/src/processes/Processes.Worker.Library/ProcessExecutionService.cs new file mode 100644 index 0000000..d8644c2 --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutionService.cs @@ -0,0 +1,146 @@ +/******************************************************************************** + * 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.DbAccess; +using Dim.DbAccess.Repositories; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +/// +/// Service that reads all open/pending processSteps of a checklist and triggers their execution. +/// +public class ProcessExecutionService +{ + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly ProcessExecutionServiceSettings _settings; + private readonly ILogger _logger; + + /// + /// Creates a new instance of + /// + /// access to the services + /// date time provider + /// access to the options + /// the logger + public ProcessExecutionService( + IServiceScopeFactory serviceScopeFactory, + IDateTimeProvider dateTimeProvider, + IOptions options, + ILogger logger) + { + _serviceScopeFactory = serviceScopeFactory; + _dateTimeProvider = dateTimeProvider; + _settings = options.Value; + _logger = logger; + } + + /// + /// Handles the checklist processing + /// + /// Cancellation Token + public async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + using var processServiceScope = _serviceScopeFactory.CreateScope(); + var executorRepositories = processServiceScope.ServiceProvider.GetRequiredService(); + var processExecutor = processServiceScope.ServiceProvider.GetRequiredService(); + + using var outerLoopScope = _serviceScopeFactory.CreateScope(); + var outerLoopRepositories = outerLoopScope.ServiceProvider.GetRequiredService(); + + var lockExpiryTime = new TimeSpan(_settings.LockExpirySeconds * 10000000L); + var activeProcesses = outerLoopRepositories.GetInstance().GetActiveProcesses(processExecutor.GetRegisteredProcessTypeIds(), processExecutor.GetExecutableStepTypeIds(), _dateTimeProvider.OffsetNow); + await foreach (var process in activeProcesses.WithCancellation(stoppingToken).ConfigureAwait(false)) + { + try + { + if (process.IsLocked()) + { + _logger.LogInformation("skipping locked process {processId} type {processType}, lock expires at {lockExpireDate}", process.Id, process.ProcessTypeId, process.LockExpiryDate); + continue; + } + _logger.LogInformation("start processing process {processId} type {processType}", process.Id, process.ProcessTypeId); + + bool EnsureLock() + { + if (process.IsLocked()) + { + return false; + } + var isLocked = process.TryLock(_dateTimeProvider.OffsetNow.Add(lockExpiryTime)); + if (!isLocked) + { + throw new UnexpectedConditionException("process TryLock should never fail here"); + } + return true; + } + + bool UpdateVersion() + { + if (!process.IsLocked()) + { + process.UpdateVersion(); + } + return true; + } + + await foreach (var executionResult in processExecutor.ExecuteProcess(process.Id, process.ProcessTypeId, stoppingToken).WithCancellation(stoppingToken).ConfigureAwait(false)) + { + if (executionResult switch + { + IProcessExecutor.ProcessExecutionResult.LockRequested => EnsureLock(), + IProcessExecutor.ProcessExecutionResult.SaveRequested => UpdateVersion(), + _ => false + }) + { + await executorRepositories.SaveAsync().ConfigureAwait(false); + } + executorRepositories.Clear(); + } + + if (process.ReleaseLock()) + { + await executorRepositories.SaveAsync().ConfigureAwait(false); + executorRepositories.Clear(); + } + _logger.LogInformation("finished processing process {processId}", process.Id); + } + catch (Exception ex) when (ex is not SystemException) + { + _logger.LogInformation(ex, "error processing process {processId} type {processType}: {message}", process.Id, process.ProcessTypeId, ex.Message); + executorRepositories.Clear(); + } + } + } + catch (Exception ex) + { + Environment.ExitCode = 1; + _logger.LogError(ex, "processing failed with following Exception {ExceptionMessage}", ex.Message); + } + } +} diff --git a/src/processes/Processes.Worker.Library/ProcessExecutionServiceExtensions.cs b/src/processes/Processes.Worker.Library/ProcessExecutionServiceExtensions.cs new file mode 100644 index 0000000..4642c0c --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutionServiceExtensions.cs @@ -0,0 +1,38 @@ +/******************************************************************************** + * 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.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +public static class ProcessExecutionServiceExtensions +{ + public static IServiceCollection AddProcessExecutionService(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions().Bind(section); + services + .AddTransient() + .AddTransient() + .AddTransient(); + return services; + } +} diff --git a/src/processes/Processes.Worker.Library/ProcessExecutionServiceSettings.cs b/src/processes/Processes.Worker.Library/ProcessExecutionServiceSettings.cs new file mode 100644 index 0000000..1508a17 --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutionServiceSettings.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * 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.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +public class ProcessExecutionServiceSettings +{ + [Required] + public int LockExpirySeconds { get; set; } +} diff --git a/src/processes/Processes.Worker.Library/ProcessExecutor.cs b/src/processes/Processes.Worker.Library/ProcessExecutor.cs new file mode 100644 index 0000000..96905e0 --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutor.cs @@ -0,0 +1,214 @@ +/******************************************************************************** + * 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.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Enums; +using Microsoft.Extensions.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Async; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +public class ProcessExecutor : IProcessExecutor +{ + private readonly ImmutableDictionary _executors; + private readonly IProcessStepRepository _processStepRepository; + private readonly ILogger _logger; + + public ProcessExecutor(IEnumerable executors, IDimRepositories portalRepositories, ILogger logger) + { + _processStepRepository = portalRepositories.GetInstance(); + _executors = executors.ToImmutableDictionary(executor => executor.GetProcessTypeId()); + _logger = logger; + } + + public IEnumerable GetRegisteredProcessTypeIds() => _executors.Keys; + public IEnumerable GetExecutableStepTypeIds() => _executors.Values.SelectMany(executor => executor.GetExecutableStepTypeIds()); + + public async IAsyncEnumerable ExecuteProcess(Guid processId, ProcessTypeId processTypeId, [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (!_executors.TryGetValue(processTypeId, out var executor)) + { + throw new UnexpectedConditionException($"processType {processTypeId} is not a registered executable processType."); + } + + var allSteps = await _processStepRepository + .GetProcessStepData(processId) + .PreSortedGroupBy(x => x.ProcessStepTypeId, x => x.ProcessStepId) + .ToDictionaryAsync(g => g.Key, g => g.AsEnumerable(), cancellationToken) + .ConfigureAwait(false); + + var context = new ProcessContext( + processId, + allSteps, + new ProcessStepTypeSet(allSteps.Keys.Where(x => executor.IsExecutableStepTypeId(x))), + executor); + + var (modified, initialStepTypeIds) = await executor.InitializeProcess(processId, context.AllSteps.Keys).ConfigureAwait(false); + + modified |= ScheduleProcessStepTypeIds(initialStepTypeIds, context); + + yield return modified + ? IProcessExecutor.ProcessExecutionResult.SaveRequested + : IProcessExecutor.ProcessExecutionResult.Unmodified; + + while (context.ExecutableStepTypeIds.TryGetNext(out var stepTypeId)) + { + if (await executor.IsLockRequested(stepTypeId).ConfigureAwait(false)) + { + yield return IProcessExecutor.ProcessExecutionResult.LockRequested; + } + ProcessStepStatusId resultStepStatusId; + IEnumerable? scheduleStepTypeIds; + IEnumerable? skipStepTypeIds; + string? processMessage; + bool success; + try + { + (modified, resultStepStatusId, scheduleStepTypeIds, skipStepTypeIds, processMessage) = await executor.ExecuteProcessStep(stepTypeId, context.AllSteps.Keys, cancellationToken).ConfigureAwait(false); + success = true; + } + catch (Exception e) when (e is not SystemException) + { + resultStepStatusId = ProcessStepStatusId.FAILED; + processMessage = $"{e.GetType()}: {e.Message}"; + scheduleStepTypeIds = null; + skipStepTypeIds = null; + modified = false; + success = false; + } + if (!success) + { + yield return IProcessExecutor.ProcessExecutionResult.Unmodified; + } + modified |= SetProcessStepStatus(stepTypeId, resultStepStatusId, context, processMessage); + modified |= SkipProcessStepTypeIds(skipStepTypeIds, context); + modified |= ScheduleProcessStepTypeIds(scheduleStepTypeIds, context); + + yield return modified + ? IProcessExecutor.ProcessExecutionResult.SaveRequested + : IProcessExecutor.ProcessExecutionResult.Unmodified; + } + } + + private bool ScheduleProcessStepTypeIds(IEnumerable? scheduleStepTypeIds, ProcessContext context) + { + if (scheduleStepTypeIds == null || !scheduleStepTypeIds.Any()) + { + return false; + } + + var newStepTypeIds = scheduleStepTypeIds.Except(context.AllSteps.Keys).ToList(); + if (!newStepTypeIds.Any()) + { + return false; + } + foreach (var newStep in _processStepRepository.CreateProcessStepRange(newStepTypeIds.Select(stepTypeId => (stepTypeId, ProcessStepStatusId.TODO, context.ProcessId)))) + { + context.AllSteps.Add(newStep.ProcessStepTypeId, new[] { newStep.Id }); + if (context.Executor.IsExecutableStepTypeId(newStep.ProcessStepTypeId)) + { + context.ExecutableStepTypeIds.Add(newStep.ProcessStepTypeId); + } + } + return true; + } + + private bool SkipProcessStepTypeIds(IEnumerable? skipStepTypeIds, ProcessContext context) + { + if (skipStepTypeIds == null || !skipStepTypeIds.Any()) + { + return false; + } + var modified = false; + foreach (var skipStepTypeId in skipStepTypeIds) + { + var skippedStep = SetProcessStepStatus(skipStepTypeId, ProcessStepStatusId.SKIPPED, context, null); + if (skippedStep) + { + _logger.LogInformation("Skipped step {SkipStepTypeId} for process {ProcessId}", skipStepTypeId, context.ProcessId); + } + + modified |= skippedStep; + } + return modified; + } + + private bool SetProcessStepStatus(ProcessStepTypeId stepTypeId, ProcessStepStatusId stepStatusId, ProcessContext context, string? processMessage) + { + if ((stepStatusId == ProcessStepStatusId.TODO && processMessage == null) || !context.AllSteps.Remove(stepTypeId, out var stepIds)) + { + return false; + } + + var isFirst = true; + foreach (var stepId in stepIds) + { + _processStepRepository.AttachAndModifyProcessStep(stepId, null, step => + { + step.ProcessStepStatusId = isFirst ? stepStatusId : ProcessStepStatusId.DUPLICATE; + step.Message = processMessage; + }); + isFirst = false; + } + if (context.Executor.IsExecutableStepTypeId(stepTypeId)) + { + context.ExecutableStepTypeIds.Remove(stepTypeId); + } + return true; + } + + private sealed record ProcessContext( + Guid ProcessId, + IDictionary> AllSteps, + ProcessStepTypeSet ExecutableStepTypeIds, + IProcessTypeExecutor Executor + ); + + private sealed class ProcessStepTypeSet + { + private readonly HashSet _items; + + public ProcessStepTypeSet(IEnumerable items) + { + _items = new HashSet(items); + } + + public bool TryGetNext(out ProcessStepTypeId item) + { + using var enumerator = _items.GetEnumerator(); + if (!enumerator.MoveNext()) + { + item = default; + return false; + } + item = enumerator.Current; + _items.Remove(item); + return true; + } + + public void Add(ProcessStepTypeId item) => _items.Add(item); + + public void Remove(ProcessStepTypeId item) => _items.Remove(item); + } +} diff --git a/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj b/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj new file mode 100644 index 0000000..b1593ee --- /dev/null +++ b/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj @@ -0,0 +1,46 @@ + + + + + + Processes.Worker.Library + Processes.Worker.Library + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/src/processes/Processes.Worker/Processes.Worker.csproj b/src/processes/Processes.Worker/Processes.Worker.csproj new file mode 100644 index 0000000..8160173 --- /dev/null +++ b/src/processes/Processes.Worker/Processes.Worker.csproj @@ -0,0 +1,54 @@ + + + + + + Processes.Worker + Processes.Worker + net8.0 + enable + enable + Exe + Linux + ..\..\.. + True + f3fe97a1-10c5-4549-b468-dd1cb392247e + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/processes/Processes.Worker/Program.cs b/src/processes/Processes.Worker/Program.cs new file mode 100644 index 0000000..fade945 --- /dev/null +++ b/src/processes/Processes.Worker/Program.cs @@ -0,0 +1,69 @@ +/******************************************************************************** + * 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.DbAccess.DependencyInjection; +using DimProcess.Executor.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Token; +using Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; +using Serilog; + +LoggingExtensions.EnsureInitialized(); +Log.Information("Building worker"); +try +{ + var host = Host + .CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services + .AddTransient() + .AddDatabase(hostContext.Configuration) + .AddProcessExecutionService(hostContext.Configuration.GetSection("Processes")) + .AddDimProcessExecutor(hostContext.Configuration); + }) + .AddLogging() + .Build(); + Log.Information("Building worker completed"); + + var tokenSource = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => + { + Log.Information("Canceling..."); + tokenSource.Cancel(); + e.Cancel = true; + }; + + Log.Information("Start processing"); + var workerInstance = host.Services.GetRequiredService(); + await workerInstance.ExecuteAsync(tokenSource.Token).ConfigureAwait(false); + Log.Information("Execution finished shutting down"); +} +catch (Exception ex) when (!ex.GetType().Name.Equals("StopTheHostException", StringComparison.Ordinal)) +{ + Log.Fatal(ex, "Unhandled exception"); +} +finally +{ + Log.Information("Server Shutting down"); + Log.CloseAndFlush(); +} diff --git a/src/processes/Processes.Worker/Properties/launchSettings.json b/src/processes/Processes.Worker/Properties/launchSettings.json new file mode 100644 index 0000000..5de5fb5 --- /dev/null +++ b/src/processes/Processes.Worker/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Processes.Worker": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/processes/Processes.Worker/appsettings.json b/src/processes/Processes.Worker/appsettings.json new file mode 100644 index 0000000..5850051 --- /dev/null +++ b/src/processes/Processes.Worker/appsettings.json @@ -0,0 +1,56 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "Org.Eclipse.TractusX.Portal.Backend": "Information" + } + }, + "WriteTo": [ + { "Name": "Console" } + ], + "Enrich": [ + "FromLogContext" + ], + "Properties": { + "Application": "Dim.Process.Worker" + } + }, + "ConnectionStrings": { + "DimDb": "Server=placeholder;Database=placeholder;Port=5432;User Id=placeholder;Password=placeholder;Ssl Mode=Disable;", + }, + "Dim": { + "AdminMail": "", + "RootDirectoryId": "", + "ClientidCisCentral": "", + "ClientsecretCisCentral": "", + "AuthUrl": "" + }, + "SubAccount": { + "BaseUrl": "" + }, + "Entitlement": { + "BaseUrl": "" + }, + "Cf": { + "ClientId": "", + "ClientSecret": "", + "TokenAddress": "", + "BaseUrl": "", + "GrantType": "" + }, + "Callback": { + "Username": "", + "Password": "", + "ClientId": "", + "GrantType": "", + "ClientSecret": "", + "Scope": "", + "TokenAddress": "", + "BaseAddress": "" + } +} diff --git a/src/web/Dim.Web/Authentication/CustomClaimTypes.cs b/src/web/Dim.Web/Authentication/CustomClaimTypes.cs new file mode 100644 index 0000000..6491b6e --- /dev/null +++ b/src/web/Dim.Web/Authentication/CustomClaimTypes.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Web.Authentication; + +public static class CustomClaimTypes +{ + public const string Sub = "sub"; + public const string ClientId = "clientId"; + public const string PreferredUserName = "preferred_username"; + public const string ResourceAccess = "resource_access"; +} diff --git a/src/web/Dim.Web/Authentication/KeycloakClaimsTransformation.cs b/src/web/Dim.Web/Authentication/KeycloakClaimsTransformation.cs new file mode 100644 index 0000000..4aa4aaa --- /dev/null +++ b/src/web/Dim.Web/Authentication/KeycloakClaimsTransformation.cs @@ -0,0 +1,74 @@ +/******************************************************************************** + * 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.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; +using System.Json; +using System.Security.Claims; + +namespace Dim.Web.Authentication +{ + public class KeycloakClaimsTransformation : IClaimsTransformation + { + private readonly JwtBearerOptions _options; + + public KeycloakClaimsTransformation(IOptions options) + { + _options = options.Value; + } + + public Task TransformAsync(ClaimsPrincipal principal) + { + var claimsIdentity = new ClaimsIdentity(); + if (AddRoles(principal, claimsIdentity)) + { + principal.AddIdentity(claimsIdentity); + } + + return Task.FromResult(principal); + } + + private bool AddRoles(ClaimsPrincipal principal, ClaimsIdentity claimsIdentity) => + principal.Claims + .Where(claim => + claim.Type == CustomClaimTypes.ResourceAccess && + claim.ValueType == "JSON") + .SelectMany(claim => + JsonValue.Parse(claim.Value) is JsonObject jsonObject && + jsonObject.TryGetValue( + _options.TokenValidationParameters.ValidAudience, + out var audience) && + audience is JsonObject client && + client.TryGetValue("roles", out var jsonRoles) && + jsonRoles is JsonArray roles + ? roles.Where(x => x.JsonType == JsonType.String) + .Select(role => new Claim(ClaimTypes.Role, role)) + : Enumerable.Empty()) + .IfAny(claims => + { + foreach (var claim in claims) + { + claimsIdentity.AddClaim(claim); + } + }); + } +} diff --git a/src/web/Dim.Web/BusinessLogic/DimBusinessLogic.cs b/src/web/Dim.Web/BusinessLogic/DimBusinessLogic.cs new file mode 100644 index 0000000..5f5fa5c --- /dev/null +++ b/src/web/Dim.Web/BusinessLogic/DimBusinessLogic.cs @@ -0,0 +1,116 @@ +/******************************************************************************** + * 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.Clients.Api.Dim; +using Dim.Clients.Token; +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Enums; +using Dim.Web.ErrorHandling; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace Dim.Web.BusinessLogic; + +public class DimBusinessLogic : IDimBusinessLogic +{ + private readonly IDimRepositories _dimRepositories; + private readonly ICfClient _cfClient; + private readonly IDimClient _dimClient; + private readonly DimSettings _settings; + + public DimBusinessLogic(IDimRepositories dimRepositories, ICfClient cfClient, IDimClient dimClient, IOptions options) + { + _dimRepositories = dimRepositories; + _cfClient = cfClient; + _dimClient = dimClient; + _settings = options.Value; + } + + public async Task StartSetupDim(string companyName, string bpn, string didDocumentLocation, bool isIssuer) + { + var processStepRepository = _dimRepositories.GetInstance(); + var processId = processStepRepository.CreateProcess(ProcessTypeId.SETUP_DIM).Id; + processStepRepository.CreateProcessStep(ProcessStepTypeId.CREATE_SUBACCOUNT, ProcessStepStatusId.TODO, processId); + + _dimRepositories.GetInstance().CreateTenant(companyName, bpn, didDocumentLocation, isIssuer, processId, _settings.OperatorId); + + await _dimRepositories.SaveAsync().ConfigureAwait(false); + } + + public async Task GetStatusList(string bpn, CancellationToken cancellationToken) + { + var (exists, companyId, instanceId) = await _dimRepositories.GetInstance().GetCompanyAndInstanceIdForBpn(bpn).ConfigureAwait(false); + if (!exists) + { + throw NotFoundException.Create(DimErrors.NO_COMPANY_FOR_BPN, new ErrorParameter[] { new("bpn", bpn) }); + } + + if (companyId is null) + { + throw ConflictException.Create(DimErrors.NO_COMPANY_ID_SET); + } + + if (instanceId is null) + { + throw ConflictException.Create(DimErrors.NO_INSTANCE_ID_SET); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(instanceId.Value, cancellationToken).ConfigureAwait(false); + var dimAuth = new BasicAuthSettings + { + TokenAddress = $"{dimDetails.Credentials.Uaa.Url}/oauth/token", + ClientId = dimDetails.Credentials.Uaa.ClientId, + ClientSecret = dimDetails.Credentials.Uaa.ClientSecret + }; + var dimBaseUrl = dimDetails.Credentials.Url; + return await _dimClient.GetStatusList(dimAuth, dimBaseUrl, companyId.Value, cancellationToken).ConfigureAwait(false); + } + + public async Task CreateStatusList(string bpn, CancellationToken cancellationToken) + { + var (exists, companyId, instanceId) = await _dimRepositories.GetInstance().GetCompanyAndInstanceIdForBpn(bpn).ConfigureAwait(false); + if (!exists) + { + throw NotFoundException.Create(DimErrors.NO_COMPANY_FOR_BPN, new ErrorParameter[] { new("bpn", bpn) }); + } + + if (companyId is null) + { + throw ConflictException.Create(DimErrors.NO_COMPANY_ID_SET); + } + + if (instanceId is null) + { + throw ConflictException.Create(DimErrors.NO_INSTANCE_ID_SET); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(instanceId.Value, cancellationToken).ConfigureAwait(false); + var dimAuth = new BasicAuthSettings + { + TokenAddress = $"{dimDetails.Credentials.Uaa.Url}/oauth/token", + ClientId = dimDetails.Credentials.Uaa.ClientId, + ClientSecret = dimDetails.Credentials.Uaa.ClientSecret + }; + var dimBaseUrl = dimDetails.Credentials.Url; + return await _dimClient.CreateStatusList(dimAuth, dimBaseUrl, companyId.Value, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/web/Dim.Web/BusinessLogic/DimSettings.cs b/src/web/Dim.Web/BusinessLogic/DimSettings.cs new file mode 100644 index 0000000..2800a08 --- /dev/null +++ b/src/web/Dim.Web/BusinessLogic/DimSettings.cs @@ -0,0 +1,26 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Web.BusinessLogic; + +public class DimSettings +{ + public Guid OperatorId { get; set; } +} diff --git a/src/web/Dim.Web/BusinessLogic/IDimBusinessLogic.cs b/src/web/Dim.Web/BusinessLogic/IDimBusinessLogic.cs new file mode 100644 index 0000000..6a5117f --- /dev/null +++ b/src/web/Dim.Web/BusinessLogic/IDimBusinessLogic.cs @@ -0,0 +1,30 @@ +/******************************************************************************** + * 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 Org.Eclipse.TractusX.Portal.Backend.Framework.DependencyInjection; + +namespace Dim.Web.BusinessLogic; + +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); +} diff --git a/src/web/Dim.Web/Controllers/DimController.cs b/src/web/Dim.Web/Controllers/DimController.cs new file mode 100644 index 0000000..cda78fc --- /dev/null +++ b/src/web/Dim.Web/Controllers/DimController.cs @@ -0,0 +1,71 @@ +/******************************************************************************** + * 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.Web.BusinessLogic; +using Dim.Web.Extensions; +using Dim.Web.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Dim.Web.Controllers; + +/// +/// Creates a new instance of +/// +public static class DimController +{ + public static RouteGroupBuilder MapDimApi(this RouteGroupBuilder group) + { + var policyHub = group.MapGroup("/dim"); + + policyHub.MapPost("setup-dim", ([FromQuery] string companyName, [FromQuery] string bpn, [FromQuery] string didDocumentLocation, IDimBusinessLogic dimBusinessLogic) => dimBusinessLogic.StartSetupDim(companyName, bpn, didDocumentLocation, false)) + .WithSwaggerDescription("Gets the keys for the attributes", + "Example: Post: api/dim/setup-dim", + "the name of the company", + "bpn of the wallets company", + "The did document location") + .RequireAuthorization(r => r.RequireRole("setup_wallet")) + .Produces(StatusCodes.Status201Created); + + policyHub.MapPost("setup-issuer", ([FromQuery] string companyName, [FromQuery] string bpn, [FromQuery] string didDocumentLocation, IDimBusinessLogic dimBusinessLogic) => dimBusinessLogic.StartSetupDim(companyName, bpn, didDocumentLocation, true)) + .WithSwaggerDescription("Gets the keys for the attributes", + "Example: Post: api/dim/setup-issuer", + "the name of the company", + "bpn of the wallets company", + "The did document location") + .RequireAuthorization(r => r.RequireRole("setup_wallet")) + .Produces(StatusCodes.Status201Created); + + policyHub.MapGet("status-list", ([FromQuery] string bpn, CancellationToken cancellationToken, [FromServices] IDimBusinessLogic dimBusinessLogic) => dimBusinessLogic.GetStatusList(bpn, cancellationToken)) + .WithSwaggerDescription("Gets the status list for the given company", + "Example: GET: api/dim/status-list/{bpn}", + "id of the dim company") + .RequireAuthorization(r => r.RequireRole("view_status_list")) + .Produces(StatusCodes.Status200OK, responseType: typeof(string), contentType: Constants.JsonContentType); + + policyHub.MapPost("status-list", ([FromQuery] string bpn, CancellationToken cancellationToken, [FromServices] IDimBusinessLogic dimBusinessLogic) => dimBusinessLogic.CreateStatusList(bpn, cancellationToken)) + .WithSwaggerDescription("Creates a status list for the given company", + "Example: Post: api/dim/status-list/{bpn}", + "bpn of the company") + .RequireAuthorization(r => r.RequireRole("create_status_list")) + .Produces(StatusCodes.Status200OK, responseType: typeof(string), contentType: Constants.JsonContentType); + + return group; + } +} diff --git a/src/web/Dim.Web/Dim.Web.csproj b/src/web/Dim.Web/Dim.Web.csproj new file mode 100644 index 0000000..ce3759f --- /dev/null +++ b/src/web/Dim.Web/Dim.Web.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + true + Linux + Dim.Web + 732e6cca-c7bc-4ed7-a186-77d0c2a0d054 + True + CS1591 + + + + + + + + + + + + + + + diff --git a/src/web/Dim.Web/ErrorHandling/DimErrorMessageContainer.cs b/src/web/Dim.Web/ErrorHandling/DimErrorMessageContainer.cs new file mode 100644 index 0000000..d944a73 --- /dev/null +++ b/src/web/Dim.Web/ErrorHandling/DimErrorMessageContainer.cs @@ -0,0 +1,45 @@ +/******************************************************************************** + * 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 Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Service; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Dim.Web.ErrorHandling; + +[ExcludeFromCodeCoverage] +public class DimErrorMessageContainer : IErrorMessageContainer +{ + private static readonly IReadOnlyDictionary _messageContainer = new Dictionary { + { 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" }, + }.ToImmutableDictionary(x => (int)x.Key, x => x.Value); + + public Type Type { get => typeof(DimErrors); } + public IReadOnlyDictionary MessageContainer { get => _messageContainer; } +} + +public enum DimErrors +{ + NO_COMPANY_FOR_BPN, + NO_COMPANY_ID_SET, + NO_INSTANCE_ID_SET +} diff --git a/src/web/Dim.Web/Extensions/RouteHandlerBuilderExtensions.cs b/src/web/Dim.Web/Extensions/RouteHandlerBuilderExtensions.cs new file mode 100644 index 0000000..bff5c36 --- /dev/null +++ b/src/web/Dim.Web/Extensions/RouteHandlerBuilderExtensions.cs @@ -0,0 +1,40 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Web.Extensions; + +public static class RouteHandlerBuilderExtensions +{ + public static RouteHandlerBuilder WithSwaggerDescription(this RouteHandlerBuilder builder, string summary, string description, params string[] parameterDescriptions) => + builder.WithOpenApi(op => + { + op.Summary = summary; + op.Description = description; + for (var i = 0; i < parameterDescriptions.Length; i++) + { + if (i < op.Parameters.Count) + { + op.Parameters[i].Description = parameterDescriptions[i]; + } + } + + return op; + }); +} diff --git a/src/web/Dim.Web/Extensions/ServiceCollectionExtensions.cs b/src/web/Dim.Web/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..9eca2df --- /dev/null +++ b/src/web/Dim.Web/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +/******************************************************************************** + * 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.Web.BusinessLogic; + +namespace Dim.Web.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddDim(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + return services; + } +} diff --git a/src/web/Dim.Web/Models/Constants.cs b/src/web/Dim.Web/Models/Constants.cs new file mode 100644 index 0000000..6d2b832 --- /dev/null +++ b/src/web/Dim.Web/Models/Constants.cs @@ -0,0 +1,26 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Dim.Web.Models; + +public static class Constants +{ + public const string JsonContentType = "application/json"; +} diff --git a/src/web/Dim.Web/Program.cs b/src/web/Dim.Web/Program.cs new file mode 100644 index 0000000..ea55d10 --- /dev/null +++ b/src/web/Dim.Web/Program.cs @@ -0,0 +1,60 @@ +/******************************************************************************** + * 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.DependencyInjection; +using Dim.Clients.Api.Dim.DependencyInjection; +using Dim.Clients.Token; +using Dim.DbAccess.DependencyInjection; +using Dim.Web.Authentication; +using Dim.Web.Controllers; +using Dim.Web.Extensions; +using Microsoft.AspNetCore.Authentication; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Web; +using System.Text.Json.Serialization; + +const string Version = "v1"; + +WebApplicationBuildRunner + .BuildAndRunWebApplication(args, "dim", Version, "dim", + builder => + { + builder.Services + .AddTransient() + .AddTransient() + .AddDimClient() + .AddCfClient(builder.Configuration.GetSection("Cf")) + .AddDim(builder.Configuration.GetSection("Dim")) + .AddEndpointsApiExplorer() + .AddDatabase(builder.Configuration) + .ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }) + .Configure(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + }, + (app, env) => + { + app.MapGroup("/api") + .WithOpenApi() + .MapDimApi(); + }); diff --git a/src/web/Dim.Web/Properties/launchSettings.json b/src/web/Dim.Web/Properties/launchSettings.json new file mode 100644 index 0000000..3e6ab2e --- /dev/null +++ b/src/web/Dim.Web/Properties/launchSettings.json @@ -0,0 +1,40 @@ +īģŋ{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:30092", + "sslPort": 44344 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/dim/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Cors__AllowedOrigins__0": "http://localhost:3000" + } + }, + "DimWeb.Service": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/dim/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Cors__AllowedOrigins__0": "http://localhost:3000", + "Cors__AllowedOrigins__1": "https://portal.example.org" + }, + "applicationUrl": "https://localhost:7001;http://localhost:7000" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true, + "useSSL": true + } + } +} \ No newline at end of file diff --git a/src/web/Dim.Web/appsettings.json b/src/web/Dim.Web/appsettings.json new file mode 100644 index 0000000..ec589e9 --- /dev/null +++ b/src/web/Dim.Web/appsettings.json @@ -0,0 +1,38 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DimDb": "Server=placeholder;Database=placeholder;Port=5432;User Id=placeholder;Password=placeholder;Ssl Mode=Disable;" + }, + "Dim": { + "RootDirectoryId": "", + "OperatorId": "" + }, + "Cf": { + "ClientId": "", + "ClientSecret": "", + "TokenAddress": "", + "BaseUrl": "", + "GrantType": "" + }, + "SwaggerEnabled": false, + "JwtBearerOptions": { + "RequireHttpsMetadata": true, + "MetadataAddress": "", + "SaveToken": true, + "TokenValidationParameters": { + "ValidateIssuer": true, + "ValidIssuer": "", + "ValidateIssuerSigningKey": true, + "ValidAudience": "", + "ValidateAudience": true, + "ValidateLifetime": true, + "ClockSkew": 600000 + } + } +} \ No newline at end of file diff --git a/tests/processes/DimProcess.Executor.Tests/DimProcess.Executor.Tests.csproj b/tests/processes/DimProcess.Executor.Tests/DimProcess.Executor.Tests.csproj new file mode 100644 index 0000000..e97e276 --- /dev/null +++ b/tests/processes/DimProcess.Executor.Tests/DimProcess.Executor.Tests.csproj @@ -0,0 +1,31 @@ + + + net8.0 + enable + enable + false + DimProcess.Executor.Tests + DimProcess.Executor.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/processes/DimProcess.Executor.Tests/DimProcessTypeExecutorTests.cs b/tests/processes/DimProcess.Executor.Tests/DimProcessTypeExecutorTests.cs new file mode 100644 index 0000000..eeb2ee7 --- /dev/null +++ b/tests/processes/DimProcess.Executor.Tests/DimProcessTypeExecutorTests.cs @@ -0,0 +1,328 @@ +/******************************************************************************** + * 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.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Enums; +using DimProcess.Library; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace DimProcess.Executor.Tests; + +public class CredentialProcessTypeExecutorTests +{ + private readonly DimProcessTypeExecutor _sut; + private readonly IDimProcessHandler _dimProcessHandler; + private readonly ITenantRepository _tenantRepository; + + public CredentialProcessTypeExecutorTests() + { + var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + var repositories = A.Fake(); + _dimProcessHandler = A.Fake(); + + _tenantRepository = A.Fake(); + + A.CallTo(() => repositories.GetInstance()).Returns(_tenantRepository); + + _sut = new DimProcessTypeExecutor(repositories, _dimProcessHandler); + } + + [Fact] + public void GetProcessTypeId_ReturnsExpected() + { + // Assert + _sut.GetProcessTypeId().Should().Be(ProcessTypeId.SETUP_DIM); + } + + [Fact] + public void IsExecutableStepTypeId_WithValid_ReturnsExpected() + { + // Assert + _sut.IsExecutableStepTypeId(ProcessStepTypeId.SEND_CALLBACK).Should().BeTrue(); + } + + [Fact] + public void GetExecutableStepTypeIds_ReturnsExpected() + { + // Assert + _sut.GetExecutableStepTypeIds().Should().HaveCount(18).And.Satisfy( + x => x == ProcessStepTypeId.CREATE_SUBACCOUNT, + x => x == ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS, + x => x == ProcessStepTypeId.ASSIGN_ENTITLEMENTS, + x => x == ProcessStepTypeId.CREATE_SERVICE_INSTANCE, + x => x == ProcessStepTypeId.CREATE_SERVICE_BINDING, + x => x == ProcessStepTypeId.SUBSCRIBE_APPLICATION, + x => x == ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT, + x => x == ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE, + x => x == ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE, + x => x == ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE, + x => x == ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE, + x => x == ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING, + x => x == ProcessStepTypeId.GET_DIM_DETAILS, + x => x == ProcessStepTypeId.CREATE_APPLICATION, + x => x == ProcessStepTypeId.CREATE_COMPANY_IDENTITY, + x => x == ProcessStepTypeId.CREATE_STATUS_LIST, + x => x == ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION, + x => x == ProcessStepTypeId.SEND_CALLBACK); + } + + [Fact] + public async Task IsLockRequested_ReturnsExpected() + { + // Act + var result = await _sut.IsLockRequested(ProcessStepTypeId.SEND_CALLBACK); + + // Assert + result.Should().BeFalse(); + } + + #region InitializeProcess + + [Fact] + public async Task InitializeProcess_WithExistingProcess_ReturnsExpected() + { + // Arrange + var validProcessId = Guid.NewGuid(); + A.CallTo(() => _tenantRepository.GetTenantDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, Guid.NewGuid(), "test", "test1")); + + // Act + var result = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()); + + // Assert + result.Modified.Should().BeFalse(); + result.ScheduleStepTypeIds.Should().BeNull(); + } + + [Fact] + public async Task InitializeProcess_WithNotExistingProcess_ThrowsNotFoundException() + { + // Arrange + var validProcessId = Guid.NewGuid(); + A.CallTo(() => _tenantRepository.GetTenantDataForProcessId(validProcessId)) + .Returns(new ValueTuple(false, Guid.Empty, string.Empty, string.Empty)); + + // Act + async Task Act() => await _sut.InitializeProcess(validProcessId, Enumerable.Empty()).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be($"process {validProcessId} does not exist or is not associated with an tenant"); + } + + #endregion + + #region ExecuteProcessStep + + [Fact] + public async Task ExecuteProcessStep_WithoutRegistrationId_ThrowsUnexpectedConditionException() + { + // Act + async Task Act() => await _sut.ExecuteProcessStep(ProcessStepTypeId.SEND_CALLBACK, Enumerable.Empty(), CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be("tenantId and tenantName should never be empty here"); + } + + [Theory] + [InlineData(ProcessStepTypeId.CREATE_SUBACCOUNT)] + [InlineData(ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS)] + [InlineData(ProcessStepTypeId.ASSIGN_ENTITLEMENTS)] + [InlineData(ProcessStepTypeId.CREATE_SERVICE_INSTANCE)] + [InlineData(ProcessStepTypeId.CREATE_SERVICE_BINDING)] + [InlineData(ProcessStepTypeId.SUBSCRIBE_APPLICATION)] + [InlineData(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT)] + [InlineData(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE)] + [InlineData(ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE)] + [InlineData(ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE)] + [InlineData(ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE)] + [InlineData(ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING)] + [InlineData(ProcessStepTypeId.GET_DIM_DETAILS)] + [InlineData(ProcessStepTypeId.CREATE_APPLICATION)] + [InlineData(ProcessStepTypeId.CREATE_COMPANY_IDENTITY)] + [InlineData(ProcessStepTypeId.CREATE_STATUS_LIST)] + [InlineData(ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION)] + [InlineData(ProcessStepTypeId.SEND_CALLBACK)] + public async Task ExecuteProcessStep_WithValidData_CallsExpected(ProcessStepTypeId processStepTypeId) + { + // Arrange InitializeProcess + var validProcessId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + A.CallTo(() => _tenantRepository.GetTenantDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, tenantId, "test", "test1")); + + // Act InitializeProcess + var initializeResult = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()); + + // Assert InitializeProcess + initializeResult.Modified.Should().BeFalse(); + initializeResult.ScheduleStepTypeIds.Should().BeNull(); + + // Arrange + SetupMock(tenantId, "test1_test"); + + // Act + var result = await _sut.ExecuteProcessStep(processStepTypeId, Enumerable.Empty(), CancellationToken.None); + + // Assert + result.Modified.Should().BeFalse(); + result.ScheduleStepTypeIds.Should().BeNull(); + result.ProcessStepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.ProcessMessage.Should().BeNull(); + result.SkipStepTypeIds.Should().BeNull(); + } + + [Fact] + public async Task ExecuteProcessStep_WithRecoverableServiceException_ReturnsToDo() + { + // Arrange InitializeProcess + var validProcessId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + A.CallTo(() => _tenantRepository.GetTenantDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, tenantId, "test", "test1")); + + // Act InitializeProcess + var initializeResult = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()); + + // Assert InitializeProcess + initializeResult.Modified.Should().BeFalse(); + initializeResult.ScheduleStepTypeIds.Should().BeNull(); + + // Arrange + A.CallTo(() => _dimProcessHandler.CreateSubaccount(tenantId, "test1_test", A._)) + .Throws(new ServiceException("this is a test", true)); + + // Act + var result = await _sut.ExecuteProcessStep(ProcessStepTypeId.CREATE_SUBACCOUNT, Enumerable.Empty(), CancellationToken.None); + + // Assert + result.Modified.Should().BeTrue(); + result.ScheduleStepTypeIds.Should().BeNull(); + result.ProcessStepStatusId.Should().Be(ProcessStepStatusId.TODO); + result.ProcessMessage.Should().Be("this is a test"); + result.SkipStepTypeIds.Should().BeNull(); + } + + [Fact] + public async Task ExecuteProcessStep_WithServiceException_ReturnsFailedAndRetriggerStep() + { + // Arrange InitializeProcess + var validProcessId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + A.CallTo(() => _tenantRepository.GetTenantDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, tenantId, "test", "test1")); + + // Act InitializeProcess + var initializeResult = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()); + + // Assert InitializeProcess + initializeResult.Modified.Should().BeFalse(); + initializeResult.ScheduleStepTypeIds.Should().BeNull(); + + // Arrange + A.CallTo(() => _dimProcessHandler.CreateSubaccount(tenantId, "test1_test", A._)) + .Throws(new ServiceException("this is a test")); + + // Act + var result = await _sut.ExecuteProcessStep(ProcessStepTypeId.CREATE_SUBACCOUNT, Enumerable.Empty(), CancellationToken.None); + + // Assert + result.Modified.Should().BeTrue(); + result.ScheduleStepTypeIds.Should().BeNull(); + result.ProcessStepStatusId.Should().Be(ProcessStepStatusId.FAILED); + result.ProcessMessage.Should().Be("this is a test"); + result.SkipStepTypeIds.Should().BeNull(); + } + + #endregion + + #region Setup + + private void SetupMock(Guid tenantId, string tenantName) + { + A.CallTo(() => _dimProcessHandler.CreateSubaccount(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateServiceManagerBindings(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.AssignEntitlements(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateServiceInstance(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateServiceBindings(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.SubscribeApplication(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateCloudFoundryEnvironment(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateCloudFoundrySpace(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.AddSpaceManagerRole(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.AddSpaceDeveloperRole(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateSubaccount(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateDimServiceInstance(tenantName, tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateServiceInstanceBindings(tenantName, tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.GetDimDetails(tenantName, tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateApplication(tenantName, tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.GetDimDetails(tenantName, tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateCompanyIdentity(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateStatusList(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.AssignCompanyApplication(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.SendCallback(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + } + + #endregion +} diff --git a/tests/processes/DimProcess.Executor.Tests/Usings.cs b/tests/processes/DimProcess.Executor.Tests/Usings.cs new file mode 100644 index 0000000..ded99ae --- /dev/null +++ b/tests/processes/DimProcess.Executor.Tests/Usings.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +global using AutoFixture; +global using AutoFixture.AutoFakeItEasy; +global using FakeItEasy; +global using FluentAssertions; +global using Xunit; diff --git a/tests/processes/DimProcess.Library.Tests/DimProcess.Library.Tests.csproj b/tests/processes/DimProcess.Library.Tests/DimProcess.Library.Tests.csproj new file mode 100644 index 0000000..382251e --- /dev/null +++ b/tests/processes/DimProcess.Library.Tests/DimProcess.Library.Tests.csproj @@ -0,0 +1,29 @@ + + + net8.0 + enable + enable + false + DimProcess.Library.Tests + DimProcess.Library.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/tests/processes/DimProcess.Library.Tests/DimProcessHandlerTests.cs b/tests/processes/DimProcess.Library.Tests/DimProcessHandlerTests.cs new file mode 100644 index 0000000..f6e03b7 --- /dev/null +++ b/tests/processes/DimProcess.Library.Tests/DimProcessHandlerTests.cs @@ -0,0 +1,1041 @@ +/******************************************************************************** + * 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.Clients.Api.Dim; +using Dim.Clients.Api.Entitlements; +using Dim.Clients.Api.Provisioning; +using Dim.Clients.Api.Services; +using Dim.Clients.Api.SubAccounts; +using Dim.Clients.Api.Subscriptions; +using Dim.Clients.Token; +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using DimProcess.Library.Callback; +using DimProcess.Library.DependencyInjection; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Runtime.CompilerServices; +using System.Text.Json; + +namespace DimProcess.Library.Tests; + +public class DimProcessHandlerTests +{ + private readonly Guid _tenantId = Guid.NewGuid(); + private readonly Guid _processId = Guid.NewGuid(); + private readonly Guid _operatorId = Guid.NewGuid(); + private readonly string _tenantName = "testCorp"; + private readonly Guid _rootDirectoryId = Guid.NewGuid(); + + private readonly IDimRepositories _repositories; + private readonly ITenantRepository _tenantRepositories; + private readonly ISubAccountClient _subAccountClient; + private readonly IServiceClient _serviceClient; + private readonly ISubscriptionClient _subscriptionClient; + private readonly IEntitlementClient _entitlementClient; + private readonly IProvisioningClient _provisioningClient; + private readonly ICfClient _cfClient; + private readonly IDimClient _dimClient; + private readonly ICallbackService _callbackService; + private readonly IOptions _options; + + private readonly DimProcessHandler _sut; + private readonly IFixture _fixture; + + public DimProcessHandlerTests() + { + _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(); + _tenantRepositories = A.Fake(); + + A.CallTo(() => _repositories.GetInstance()).Returns(_tenantRepositories); + + _subAccountClient = A.Fake(); + _serviceClient = A.Fake(); + _subscriptionClient = A.Fake(); + _entitlementClient = A.Fake(); + _provisioningClient = A.Fake(); + _cfClient = A.Fake(); + _dimClient = A.Fake(); + _callbackService = A.Fake(); + _options = Options.Create(new DimHandlerSettings + { + AdminMail = "test@example.org", + AuthUrl = "https://example.org/auth", + ClientidCisCentral = "test123", + ClientsecretCisCentral = "test654", + EncryptionKey = "test123", + RootDirectoryId = _rootDirectoryId + }); + + _sut = new DimProcessHandler(_repositories, _subAccountClient, _serviceClient, _subscriptionClient, + _entitlementClient, _provisioningClient, _cfClient, _dimClient, _callbackService, _options); + } + + #region CreateSubaccount + + [Fact] + public async Task CreateSubaccount_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _subAccountClient.CreateSubaccount(A._, A._, _tenantName, A._, A._)) + .Returns(subAccountId); + + // Act + var result = await _sut.CreateSubaccount(_tenantId, _tenantName, 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.CREATE_SERVICEMANAGER_BINDINGS); + tenant.SubAccountId.Should().Be(subAccountId); + } + + #endregion + + #region CreateServiceManagerBindings + + [Fact] + public async Task CreateServiceManagerBindings_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.CreateServiceManagerBindings(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task CreateServiceManagerBindings_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns(subAccountId); + + // Act + var result = await _sut.CreateServiceManagerBindings(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _subAccountClient.CreateServiceManagerBindings(A._, subAccountId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.ASSIGN_ENTITLEMENTS); + } + + #endregion + + #region AssignEntitlements + + [Fact] + public async Task AssignEntitlements_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.AssignEntitlements(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task AssignEntitlements_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns(subAccountId); + + // Act + var result = await _sut.AssignEntitlements(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _entitlementClient.AssignEntitlements(A._, subAccountId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_SERVICE_INSTANCE); + } + + #endregion + + #region CreateServiceInstance + + [Fact] + public async Task CreateServiceInstance_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.CreateServiceInstance(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task CreateServiceInstance_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + var serviceInstance = new CreateServiceInstanceResponse(Guid.NewGuid().ToString(), "test"); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns(subAccountId); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(new ServiceManagementBindingItem("test", "test123", "https://example.org/sm", "https://example.org")); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _serviceClient.CreateServiceInstance(A._, A._)) + .Returns(serviceInstance); + + // Act + var result = await _sut.CreateServiceInstance(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _serviceClient.CreateServiceInstance(A._, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_SERVICE_BINDING); + tenant.ServiceInstanceId.Should().Be(serviceInstance.Id); + } + + #endregion + + #region CreateServiceBindings + + [Fact] + public async Task CreateServiceBindings_WithNotExistingSubAccount_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountAndServiceInstanceIdsByTenantId(_tenantId)) + .Returns((null, null)); + async Task Act() => await _sut.CreateServiceBindings(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task CreateServiceBindings_WithNotExistingServiceInstance_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountAndServiceInstanceIdsByTenantId(_tenantId)) + .Returns((Guid.NewGuid(), null)); + async Task Act() => await _sut.CreateServiceBindings(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("ServiceInstanceId must not be null."); + } + + [Fact] + public async Task CreateServiceBindings_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + var serviceInstanceId = Guid.NewGuid().ToString(); + var binding = new ServiceManagementBindingItem("cl1", "s1", "https://example.org/sm", "https://example.org"); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetSubAccountAndServiceInstanceIdsByTenantId(_tenantId)) + .Returns((subAccountId, serviceInstanceId)); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(new ServiceManagementBindingItem("test", "test123", "https://example.org/sm", "https://example.org")); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(binding); + A.CallTo(() => _serviceClient.CreateServiceBinding(binding, serviceInstanceId, A._)) + .Returns(new CreateServiceBindingResponse(Guid.NewGuid().ToString(), "expectedName")); + + // Act + var result = await _sut.CreateServiceBindings(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _serviceClient.CreateServiceBinding(binding, serviceInstanceId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.SUBSCRIBE_APPLICATION); + tenant.ServiceBindingName.Should().Be("expectedName"); + } + + #endregion + + #region SubscribeApplication + + [Fact] + public async Task SubscribeApplication_WithNotExistingSubAccount_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountAndServiceInstanceIdsByTenantId(_tenantId)) + .Returns((null, null)); + async Task Act() => await _sut.SubscribeApplication(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task SubscribeApplication_WithNotExistingServiceBindingName_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountAndServiceInstanceIdsByTenantId(_tenantId)) + .Returns((Guid.NewGuid(), null)); + async Task Act() => await _sut.SubscribeApplication(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task SubscribeApplication_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + var serviceInstanceId = Guid.NewGuid(); + var serviceBindingName = Guid.NewGuid().ToString(); + var serviceManagementBinding = new ServiceManagementBindingItem("c1", "cs1", "https://example.org/sm", "https://example.org/"); + var binding = new BindingItem("binding1", serviceInstanceId, _fixture.Create()); + A.CallTo(() => _tenantRepositories.GetSubAccountIdAndServiceBindingNameByTenantId(_tenantId)) + .Returns((subAccountId, serviceBindingName)); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(new ServiceManagementBindingItem("test", "test123", "https://example.org/sm", "https://example.org")); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(serviceManagementBinding); + A.CallTo(() => _serviceClient.GetServiceBinding(A._, serviceBindingName, A._)) + .Returns(binding); + A.CallTo(() => _serviceClient.CreateServiceBinding(serviceManagementBinding, serviceBindingName, A._)) + .Returns(new CreateServiceBindingResponse(Guid.NewGuid().ToString(), "expectedName")); + + // Act + var result = await _sut.SubscribeApplication(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _subscriptionClient.SubscribeApplication(A._, binding, "decentralized-identity-management-app", "standard", A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT); + } + + #endregion + + #region CreateCloudFoundryEnvironment + + [Fact] + public async Task CreateCloudFoundryEnvironment_WithNotExistingSubAccount_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountIdAndServiceBindingNameByTenantId(_tenantId)) + .Returns((null, null)); + async Task Act() => await _sut.CreateCloudFoundryEnvironment(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task CreateCloudFoundryEnvironment_WithNotExistingServiceInstance_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountIdAndServiceBindingNameByTenantId(_tenantId)) + .Returns((Guid.NewGuid(), null)); + async Task Act() => await _sut.CreateCloudFoundryEnvironment(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("ServiceBindingName must not be null."); + } + + [Fact] + public async Task CreateCloudFoundryEnvironment_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + var serviceInstanceId = Guid.NewGuid(); + var serviceManagementBinding = new ServiceManagementBindingItem("c1", "cs1", "https://example.org/sm", "https://example.org/"); + var binding = new BindingItem("binding1", serviceInstanceId, _fixture.Create()); + A.CallTo(() => _tenantRepositories.GetSubAccountIdAndServiceBindingNameByTenantId(_tenantId)) + .Returns((subAccountId, binding.Name)); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(new ServiceManagementBindingItem("test", "test123", "https://example.org/sm", "https://example.org")); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(serviceManagementBinding); + A.CallTo(() => _serviceClient.GetServiceBinding(serviceManagementBinding, binding.Name, A._)) + .Returns(binding); + + // Act + var result = await _sut.CreateCloudFoundryEnvironment(_tenantId, _tenantName, CancellationToken.None); + + // Assert + A.CallTo(() => _provisioningClient.CreateCloudFoundryEnvironment(A._, binding, _tenantName, A._, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE); + } + + #endregion + + #region CreateCloudFoundrySpace + + [Fact] + public async Task CreateCloudFoundrySpace_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _cfClient.CreateCloudFoundrySpace(_tenantName, A._)) + .Returns(spaceId); + + // Act + var result = await _sut.CreateCloudFoundrySpace(_tenantId, _tenantName, 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.ADD_SPACE_MANAGER_ROLE); + tenant.SpaceId.Should().Be(spaceId); + } + + #endregion + + #region AddSpaceManagerRole + + [Fact] + public async Task AddSpaceManagerRole_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.AddSpaceManagerRole(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SpaceId must not be null."); + } + + [Fact] + public async Task AddSpaceManagerRole_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns(spaceId); + + // Act + var result = await _sut.AddSpaceManagerRole(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _cfClient.AddSpaceRoleToUser("space_manager", A._, spaceId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE); + } + + #endregion + + #region AddSpaceManagerRole + + [Fact] + public async Task AddSpaceDeveloperRole_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.AddSpaceDeveloperRole(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SpaceId must not be null."); + } + + [Fact] + public async Task AddSpaceDeveloperRole_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns(spaceId); + + // Act + var result = await _sut.AddSpaceDeveloperRole(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _cfClient.AddSpaceRoleToUser("space_developer", A._, spaceId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE); + } + + #endregion + + #region CreateDimServiceInstance + + [Fact] + public async Task CreateDimServiceInstance_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + var servicePlanId = Guid.NewGuid(); + A.CallTo(() => _cfClient.GetSpace(_tenantName, A._)) + .Returns(spaceId); + A.CallTo(() => _cfClient.GetServicePlan("decentralized-identity-management", "standard", A._)) + .Returns(servicePlanId); + + // Act + var result = await _sut.CreateDimServiceInstance(_tenantName, _tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _cfClient.CreateDimServiceInstance(_tenantName, spaceId, servicePlanId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING); + } + + #endregion + + #region CreateServiceInstanceBindings + + [Fact] + public async Task CreateServiceInstanceBindings_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.CreateServiceInstanceBindings(_tenantName, _tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SpaceId must not be null."); + } + + [Fact] + public async Task CreateServiceInstanceBindings_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + var servicePlanId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns(spaceId); + A.CallTo(() => _cfClient.GetServicePlan("decentralized-identity-management", "standard", A._)) + .Returns(servicePlanId); + + // Act + var result = await _sut.CreateServiceInstanceBindings(_tenantName, _tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _cfClient.CreateServiceInstanceBindings(_tenantName, spaceId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.GET_DIM_DETAILS); + } + + #endregion + + #region GetDimDetails + + [Fact] + public async Task GetDimDetails_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.GetDimDetails(_tenantName, _tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SpaceId must not be null."); + } + + [Fact] + public async Task GetDimDetails_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + var dimInstanceId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns(spaceId); + A.CallTo(() => _cfClient.GetServiceBinding(_tenantName, spaceId, $"{_tenantName}-dim-key01", A._)) + .Returns(dimInstanceId); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + + // Act + var result = await _sut.GetDimDetails(_tenantName, _tenantId, 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.CREATE_APPLICATION); + tenant.DimInstanceId.Should().Be(dimInstanceId); + } + + #endregion + + #region CreateApplication + + [Fact] + public async Task CreateApplication_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetDimInstanceIdAndHostingUrl(_tenantId)) + .Returns(((Guid?)null, string.Empty, false)); + async Task Act() => await _sut.CreateApplication(_tenantName, _tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DimInstanceId must not be null."); + } + + [Fact] + public async Task CreateApplication_WithValidData_ReturnsExpected() + { + // Arrange + var serviceCrenentialBinding = _fixture.Create(); + var dimInstanceId = Guid.NewGuid(); + var applicationId = Guid.NewGuid().ToString(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetDimInstanceIdAndHostingUrl(_tenantId)) + .Returns((dimInstanceId, string.Empty, false)); + A.CallTo(() => _cfClient.GetServiceBindingDetails(dimInstanceId, A._)) + .Returns(serviceCrenentialBinding); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _dimClient.CreateApplication(A._, A._, _tenantName, A._)) + .Returns(applicationId); + + // Act + var result = await _sut.CreateApplication(_tenantName, _tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _dimClient.CreateApplication(A._, A._, _tenantName, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_COMPANY_IDENTITY); + tenant.ApplicationId.Should().Be(applicationId); + } + + #endregion + + #region CreateCompanyIdentity + + [Fact] + public async Task CreateCompanyIdentity_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetDimInstanceIdAndHostingUrl(_tenantId)) + .Returns(((Guid?)null, string.Empty, false)); + async Task Act() => await _sut.CreateCompanyIdentity(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DimInstanceId must not be null."); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CreateCompanyIdentity_WithValidData_ReturnsExpected(bool isIssuer) + { + // Arrange + var serviceCrenentialBinding = _fixture.Create(); + var identityResponse = _fixture.Create(); + var dimInstanceId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", isIssuer, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetDimInstanceIdAndHostingUrl(_tenantId)) + .Returns((dimInstanceId, "https://example.org/hosting", tenant.IsIssuer)); + A.CallTo(() => _cfClient.GetServiceBindingDetails(dimInstanceId, A._)) + .Returns(serviceCrenentialBinding); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _dimClient.CreateCompanyIdentity(A._, "https://example.org/hosting", A._, _tenantName, tenant.IsIssuer, A._)) + .Returns(identityResponse); + + // Act + var result = await _sut.CreateCompanyIdentity(_tenantId, _tenantName, CancellationToken.None); + + // Assert + A.CallTo(() => _dimClient.CreateCompanyIdentity(A._, A._, A._, _tenantName, tenant.IsIssuer, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION); + tenant.Did.Should().Be(identityResponse.Did); + tenant.DidDownloadUrl.Should().Be(identityResponse.DownloadUrl); + tenant.CompanyId.Should().Be(identityResponse.CompanyId); + } + + #endregion + + #region AssignCompanyApplication + + [Fact] + public async Task AssignCompanyApplication_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns(((string?)null, (Guid?)null, (Guid?)null, false)); + async Task Act() => await _sut.AssignCompanyApplication(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("ApplicationId must always be set here"); + } + + [Fact] + public async Task AssignCompanyApplication_WithNoCompanyId_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns((Guid.NewGuid().ToString(), (Guid?)null, (Guid?)null, false)); + async Task Act() => await _sut.AssignCompanyApplication(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("CompanyId must always be set here"); + } + + [Fact] + public async Task AssignCompanyApplication_WithNoDimInstanceId_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns((Guid.NewGuid().ToString(), Guid.NewGuid(), null, false)); + async Task Act() => await _sut.AssignCompanyApplication(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DimInstanceId must not be null."); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task AssignCompanyApplication_WithValidData_ReturnsExpected(bool isIssuer) + { + // Arrange + var serviceCrenentialBinding = _fixture.Create(); + var identityResponse = _fixture.Create(); + var applicationId = Guid.NewGuid().ToString(); + var applicationKey = Guid.NewGuid().ToString(); + var companyId = Guid.NewGuid(); + var dimInstanceId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns((applicationId, companyId, dimInstanceId, isIssuer)); + A.CallTo(() => _cfClient.GetServiceBindingDetails(dimInstanceId, A._)) + .Returns(serviceCrenentialBinding); + A.CallTo(() => _dimClient.GetApplication(A._, A._, applicationId, A._)) + .Returns(applicationKey); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _dimClient.CreateCompanyIdentity(A._, "https://example.org/hosting", A._, _tenantName, false, A._)) + .Returns(identityResponse); + + // Act + var result = await _sut.AssignCompanyApplication(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _dimClient.AssignApplicationToCompany(A._, A._, applicationKey, companyId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(isIssuer ? ProcessStepTypeId.CREATE_STATUS_LIST : ProcessStepTypeId.SEND_CALLBACK); + tenant.ApplicationKey.Should().Be(applicationKey); + } + + #endregion + + #region CreateStatusList + + [Fact] + public async Task CreateStatusList_WithNoCompanyId_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns((Guid.NewGuid().ToString(), (Guid?)null, (Guid?)null, false)); + async Task Act() => await _sut.CreateStatusList(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("CompanyId must always be set here"); + } + + [Fact] + public async Task CreateStatusList_WithNoDimInstanceId_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns((Guid.NewGuid().ToString(), Guid.NewGuid(), null, false)); + async Task Act() => await _sut.CreateStatusList(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DimInstanceId must not be null."); + } + + [Fact] + public async Task CreateStatusList_WithValidData_ReturnsExpected() + { + // Arrange + var serviceCrenentialBinding = _fixture.Create(); + var applicationId = Guid.NewGuid().ToString(); + var companyId = Guid.NewGuid(); + var dimInstanceId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns((applicationId, companyId, dimInstanceId, false)); + A.CallTo(() => _cfClient.GetServiceBindingDetails(dimInstanceId, A._)) + .Returns(serviceCrenentialBinding); + + // Act + var result = await _sut.CreateStatusList(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _dimClient.CreateStatusList(A._, A._, companyId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.SEND_CALLBACK); + } + + #endregion + + #region SendCallback + + [Fact] + public async Task SendCallback_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetCallbackData(_tenantId)) + .Returns(("bpn123", (string?)null, (string?)null, (Guid?)null)); + async Task Act() => await _sut.SendCallback(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DownloadUrl must not be null."); + } + + [Fact] + public async Task SendCallback_WithNoCompanyId_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetCallbackData(_tenantId)) + .Returns(("bpn123", "https://example.org/did", (string?)null, (Guid?)null)); + async Task Act() => await _sut.SendCallback(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("Did must not be null."); + } + + [Fact] + public async Task SendCallback_WithNoDimInstanceId_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetCallbackData(_tenantId)) + .Returns(("bpn123", "https://example.org/did", Guid.NewGuid().ToString(), (Guid?)null)); + async Task Act() => await _sut.SendCallback(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DimInstanceId must not be null."); + } + + [Fact] + public async Task SendCallback_WithValidData_ReturnsExpected() + { + // Arrange + var serviceCrenentialBinding = _fixture.Create(); + var identityResponse = _fixture.Create(); + var dimInstanceId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + var did = Guid.NewGuid().ToString(); + A.CallTo(() => _tenantRepositories.GetCallbackData(_tenantId)) + .Returns(("bpn123", "https://example.org/did", did, dimInstanceId)); + A.CallTo(() => _cfClient.GetServiceBindingDetails(dimInstanceId, A._)) + .Returns(serviceCrenentialBinding); + A.CallTo(() => _dimClient.GetDidDocument(A._, A._)) + .Returns(JsonDocument.Parse("{}")); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _dimClient.CreateCompanyIdentity(A._, "https://example.org/hosting", A._, _tenantName, false, A._)) + .Returns(identityResponse); + + // Act + var result = await _sut.SendCallback(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _callbackService.SendCallback("bpn123", A._, A._, did, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().BeNull(); + } + + #endregion +} diff --git a/tests/processes/DimProcess.Library.Tests/Usings.cs b/tests/processes/DimProcess.Library.Tests/Usings.cs new file mode 100644 index 0000000..ded99ae --- /dev/null +++ b/tests/processes/DimProcess.Library.Tests/Usings.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +global using AutoFixture; +global using AutoFixture.AutoFakeItEasy; +global using FakeItEasy; +global using FluentAssertions; +global using Xunit; diff --git a/tests/processes/Processes.Library.Tests/ManualProcessDataExtensionsTests.cs b/tests/processes/Processes.Library.Tests/ManualProcessDataExtensionsTests.cs new file mode 100644 index 0000000..45d5ac8 --- /dev/null +++ b/tests/processes/Processes.Library.Tests/ManualProcessDataExtensionsTests.cs @@ -0,0 +1,516 @@ +/******************************************************************************** + * 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.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Collections.Immutable; +using ProcessStepTypeId = Dim.Entities.Enums.ProcessStepTypeId; +using ProcessTypeId = Dim.Entities.Enums.ProcessTypeId; + +namespace Dim.Processes.Library.Tests; + +public class ManualProcessDataExtensionsTests +{ + private readonly IDimRepositories _respositories; + private readonly IProcessStepRepository _processStepRepository; + private readonly string _entityName; + private readonly Func _getProcessEntityName; + private readonly IFixture _fixture; + + public ManualProcessDataExtensionsTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _respositories = A.Fake(); + _processStepRepository = A.Fake(); + + A.CallTo(() => _respositories.GetInstance()) + .Returns(_processStepRepository); + + _entityName = _fixture.Create(); + _getProcessEntityName = () => _entityName; + } + + #region CreateManualProcessData + + [Fact] + public void CreateManualProcessData_ReturnsExpected() + { + // Arrange + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = null }; + var processSteps = _fixture.CreateMany<(Guid ProcessStepId, DateTimeOffset Now)>(5).Select(x => new ProcessStep(x.ProcessStepId, _fixture.Create(), Entities.Enums.ProcessStepStatusId.TODO, process.Id, x.Now)).ToImmutableArray(); + var stepTypeId = processSteps[2].ProcessStepTypeId; + + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.ProcessSteps, processSteps) + .Create(); + + // Act + var result = sut.CreateManualProcessData(stepTypeId, _respositories, _getProcessEntityName); + + // Assert + result.Should().NotBeNull().And.BeOfType().And.Match( + data => + data.ProcessStepTypeId == stepTypeId && + data.Process == sut.Process && + data.ProcessSteps.SequenceEqual(sut.ProcessSteps!) && + data.PortalRepositories == _respositories); + } + + [Fact] + public void CreateManualProcessData_WithNullVerifyProcessData_Throws() + { + // Arrange + var sut = (VerifyProcessData?)null; + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be($"{_entityName} does not exist"); + } + + [Fact] + public void CreateManualProcessData_WithNullProcess_Throws() + { + // Arrange + var sut = _fixture.Build() + .With(x => x.Process, (Process?)null) + .Create(); + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be($"{_entityName} is not associated with any process"); + } + + [Fact] + public void CreateManualProcessData_WithLockedProcess_Throws() + { + // Arrange + var expiryDate = _fixture.Create(); + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = expiryDate }; + var sut = _fixture.Build() + .With(x => x.Process, process) + .Create(); + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be($"process {process.Id} associated with {_entityName} is locked, lock expiry is set to {expiryDate}"); + } + + [Fact] + public void CreateManualProcessData_WithNullProcessSteps_Throws() + { + // Arrange + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = null }; + var processSteps = _fixture.CreateMany<(Guid ProcessStepId, DateTimeOffset Now)>(5).Select(x => new ProcessStep(x.ProcessStepId, _fixture.Create(), Entities.Enums.ProcessStepStatusId.TODO, process.Id, x.Now)).ToImmutableArray(); + + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.ProcessSteps, (IEnumerable?)null) + .Create(); + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be("processSteps should never be null here"); + } + + [Fact] + public void CreateManualProcessData_WithInvalidProcessStepStatus_Throws() + { + // Arrange + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = null }; + var processSteps = _fixture.CreateMany<(Guid ProcessStepId, DateTimeOffset Now)>(5).Select(x => new ProcessStep(x.ProcessStepId, _fixture.Create(), Entities.Enums.ProcessStepStatusId.DONE, process.Id, x.Now)).ToImmutableArray(); + + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.ProcessSteps, processSteps) + .Create(); + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be("processSteps should never have any other status than TODO here"); + } + + [Fact] + public void CreateManualProcessData_WithInvalidProcessStepType_Throws() + { + // Arrange + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = null }; + var processSteps = _fixture.CreateMany<(Guid ProcessStepId, DateTimeOffset Now)>(5).Select(x => new ProcessStep(x.ProcessStepId, _fixture.Create(), Entities.Enums.ProcessStepStatusId.TODO, process.Id, x.Now)).ToImmutableArray(); + var stepTypeId = Enum.GetValues().Except(processSteps.Select(step => step.ProcessStepTypeId)).First(); + + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.ProcessSteps, processSteps) + .Create(); + + var Act = () => sut.CreateManualProcessData(stepTypeId, _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be($"{_entityName}, process step {stepTypeId} is not eligible to run"); + } + + #endregion + + #region RequestLock + + [Fact] + public void RequestLock_WithUnLockedProcess_ReturnsExpected() + { + // Arrange + var expiryDate = _fixture.Create(); + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = null }; + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.PortalRepositories, _respositories) + .Create(); + + // Act + sut.RequestLock(expiryDate); + + // Assert + sut.Process.LockExpiryDate.Should().Be(expiryDate); + A.CallTo(() => _respositories.Attach(process, null)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void RequestLock_WithLockedProcess_Throws() + { + // Arrange + var expiryDate = _fixture.Create(); + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = expiryDate }; + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.PortalRepositories, _respositories) + .Create(); + + var Act = () => sut.RequestLock(DateTimeOffset.UtcNow); + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be("process TryLock should never fail here"); + sut.Process.LockExpiryDate.Should().Be(expiryDate); + A.CallTo(() => _respositories.Attach(process, null)).MustHaveHappenedOnceExactly(); + } + + #endregion + + #region SkipProcessSteps + + [Fact] + public void SkipProcessSteps_ReturnsExpected() + { + // Arrange + var process = _fixture.Create(); + var stepTypeIds = _fixture.CreateMany(4).ToImmutableArray(); + var before = DateTimeOffset.UtcNow.AddDays(-1); + var processSteps0 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps1 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps2 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps3 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + + var processSteps = new[] + { + processSteps0[0], + processSteps1[0], + processSteps2[0], + processSteps3[0], + processSteps0[1], + processSteps1[1], + processSteps2[1], + processSteps3[1], + processSteps0[2], + processSteps1[2], + processSteps2[2], + processSteps3[2], + }; + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .Invokes((IEnumerable<(Guid ProcessStepId, Action? Initialize, Action Modify)> processStepIdInitializeModify) => + { + foreach (var (stepId, initialize, modify) in processStepIdInitializeModify) + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + } + }); + + var sut = _fixture.Build() + .With(x => x.ProcessStepTypeId, stepTypeIds[3]) + .With(x => x.Process, process) + .With(x => x.PortalRepositories, _respositories) + .With(x => x.ProcessSteps, processSteps) + .Create(); + + // Act + sut.SkipProcessSteps(new ProcessStepTypeId[] { stepTypeIds[1], stepTypeIds[2], stepTypeIds[3] }); + + // Assert + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .MustHaveHappenedOnceExactly(); + + modifiedProcessSteps.Should().HaveCount(6).And.Satisfy( + x => processSteps1.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.SKIPPED && x.DateLastChanged != before, + x => processSteps1.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps1.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps2.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.SKIPPED && x.DateLastChanged != before, + x => processSteps2.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps2.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before + ); + } + + #endregion + + #region SkipProcessStepsExcept + + [Fact] + public void SkipProcessStepsExcept_ReturnsExpected() + { + // Arrange + var process = _fixture.Create(); + var stepTypeIds = _fixture.CreateMany(4).ToImmutableArray(); + var before = DateTimeOffset.UtcNow.AddDays(-1); + var processSteps0 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps1 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps2 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps3 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + + var processSteps = new[] + { + processSteps0[0], + processSteps1[0], + processSteps2[0], + processSteps3[0], + processSteps0[1], + processSteps1[1], + processSteps2[1], + processSteps3[1], + processSteps0[2], + processSteps1[2], + processSteps2[2], + processSteps3[2], + }; + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .Invokes((IEnumerable<(Guid ProcessStepId, Action? Initialize, Action Modify)> processStepIdInitializeModify) => + { + foreach (var (stepId, initialize, modify) in processStepIdInitializeModify) + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + } + }); + + var sut = _fixture.Build() + .With(x => x.ProcessStepTypeId, stepTypeIds[3]) + .With(x => x.Process, process) + .With(x => x.PortalRepositories, _respositories) + .With(x => x.ProcessSteps, processSteps) + .Create(); + + // Act + sut.SkipProcessStepsExcept(new ProcessStepTypeId[] { stepTypeIds[1], stepTypeIds[2] }); + + // Assert + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .MustHaveHappenedOnceExactly(); + + modifiedProcessSteps.Should().HaveCount(3).And.Satisfy( + x => processSteps0.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.SKIPPED && x.DateLastChanged != before, + x => processSteps0.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps0.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before + ); + } + + #endregion + + #region ScheduleProcessSteps + + [Fact] + public void ScheduleProcessSteps_ReturnsExpected() + { + // Arrange + var stepTypeIds = _fixture.CreateMany(3).ToImmutableArray(); + var now = DateTimeOffset.UtcNow; + + var createdSteps = new List(); + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .ReturnsLazily((IEnumerable<(ProcessStepTypeId ProcesssStepTypeId, Entities.Enums.ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatusProcessIds) => + { + foreach (var data in processStepTypeStatusProcessIds) + { + createdSteps.Add(new ProcessStep(Guid.NewGuid(), data.ProcesssStepTypeId, data.ProcessStepStatusId, data.ProcessId, now)); + } + return createdSteps; + }); + + var sut = _fixture.Build() + .With(x => x.PortalRepositories, _respositories) + .Create(); + + // Act + sut.ScheduleProcessSteps(stepTypeIds); + + // Assert + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustHaveHappenedOnceExactly(); + createdSteps.Should().HaveCount(3).And.Satisfy( + x => x.ProcessStepTypeId == stepTypeIds[0] && x.ProcessId == sut.Process.Id && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == stepTypeIds[1] && x.ProcessId == sut.Process.Id && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == stepTypeIds[2] && x.ProcessId == sut.Process.Id && x.ProcessStepStatusId == ProcessStepStatusId.TODO + ); + } + + #endregion + + #region FinalizeProcessStep + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void FinalizeProcessStep_ReturnsExpected(bool locked) + { + // Arrange + var version = Guid.NewGuid(); + var process = _fixture.Build() + .With(x => x.Version, version) + .With(x => x.LockExpiryDate, locked ? DateTimeOffset.UtcNow : (DateTimeOffset?)null) + .Create(); + var stepTypeIds = _fixture.CreateMany(3).ToImmutableArray(); + var before = DateTimeOffset.UtcNow.AddDays(-1); + var processSteps = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[0], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], ProcessStepStatusId.TODO, process.Id, before) + }; + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .Invokes((IEnumerable<(Guid ProcessStepId, Action? Initialize, Action Modify)> processStepIdInitializeModify) => + { + foreach (var (stepId, initialize, modify) in processStepIdInitializeModify) + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + } + }); + + var sut = new ManualProcessStepData(stepTypeIds[1], process, processSteps, _respositories); + + // Act + sut.FinalizeProcessStep(); + + // Assert + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _respositories.Attach(process, null)) + .MustHaveHappenedOnceExactly(); + + process.LockExpiryDate.Should().BeNull(); + process.Version.Should().NotBe(version); + modifiedProcessSteps.Should().ContainSingle().Which.Should().Match( + x => x.Id == processSteps[1].Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE && x.DateLastChanged != before + ); + } + + #endregion +} diff --git a/tests/processes/Processes.Library.Tests/Processes.Library.Tests.csproj b/tests/processes/Processes.Library.Tests/Processes.Library.Tests.csproj new file mode 100644 index 0000000..7708a04 --- /dev/null +++ b/tests/processes/Processes.Library.Tests/Processes.Library.Tests.csproj @@ -0,0 +1,48 @@ + + + + + net8.0 + enable + enable + false + Dim.Processes.Library.Tests + Dim.Processes.Library.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/tests/processes/Processes.Library.Tests/Usings.cs b/tests/processes/Processes.Library.Tests/Usings.cs new file mode 100644 index 0000000..ded99ae --- /dev/null +++ b/tests/processes/Processes.Library.Tests/Usings.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +global using AutoFixture; +global using AutoFixture.AutoFakeItEasy; +global using FakeItEasy; +global using FluentAssertions; +global using Xunit; diff --git a/tests/processes/Processes.Worker.Library.Tests/ProcessExecutionServiceTests.cs b/tests/processes/Processes.Worker.Library.Tests/ProcessExecutionServiceTests.cs new file mode 100644 index 0000000..62b0bb0 --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/ProcessExecutionServiceTests.cs @@ -0,0 +1,514 @@ +/******************************************************************************** + * 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.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Dim.Tests.Shared; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; +using System.Collections.Immutable; + +namespace Processes.Worker.Library.Tests; + +public class ProcessExecutionServiceTests +{ + private readonly IProcessStepRepository _processStepRepository; + private readonly IDimRepositories _repositories; + private readonly IProcessExecutor _processExecutor; + private readonly IMockLogger _mockLogger; + private readonly ProcessExecutionService _service; + private readonly IFixture _fixture; + + public ProcessExecutionServiceTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + var dateTimeProvider = A.Fake(); + _repositories = A.Fake(); + _processStepRepository = A.Fake(); + _processExecutor = A.Fake(); + + _mockLogger = A.Fake>(); + ILogger logger = new MockLogger(_mockLogger); + + A.CallTo(() => _repositories.GetInstance()) + .Returns(_processStepRepository); + + var settings = _fixture.Create(); + + var options = Options.Create(settings); + var serviceProvider = A.Fake(); + A.CallTo(() => serviceProvider.GetService(typeof(IDimRepositories))).Returns(_repositories); + A.CallTo(() => serviceProvider.GetService(typeof(IProcessExecutor))).Returns(_processExecutor); + var serviceScope = A.Fake(); + A.CallTo(() => serviceScope.ServiceProvider).Returns(serviceProvider); + var serviceScopeFactory = A.Fake(); + A.CallTo(() => serviceScopeFactory.CreateScope()).Returns(serviceScope); + A.CallTo(() => serviceProvider.GetService(typeof(IServiceScopeFactory))).Returns(serviceScopeFactory); + + _service = new ProcessExecutionService(serviceScopeFactory, dateTimeProvider, options, logger); + } + + [Fact] + public async Task ExecuteAsync_WithNoPendingItems_NoServiceCall() + { + // Arrange + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(Array.Empty().ToAsyncEnumerable()); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_WithPendingItems_CallsProcessExpectedNumberOfTimes() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.SETUP_DIM, Guid.NewGuid())).ToImmutableArray(); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .Returns(Enumerable.Repeat(IProcessExecutor.ProcessExecutionResult.SaveRequested, 2).ToAsyncEnumerable()); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappened(processData.Length, Times.Exactly); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappened(processData.Length * 2, Times.Exactly); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(processData.Length * 2, Times.Exactly); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_Unmodified() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.SETUP_DIM, Guid.NewGuid())).ToImmutableArray(); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .Returns(Enumerable.Repeat(IProcessExecutor.ProcessExecutionResult.Unmodified, 2).ToAsyncEnumerable()); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappened(processData.Length, Times.Exactly); + A.CallTo(() => _repositories.SaveAsync()) + .MustNotHaveHappened(); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(processData.Length * 2, Times.Exactly); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_RequestLock() + { + // Arrange + var processId = Guid.NewGuid(); + var processVersion = Guid.NewGuid(); + var process = new Process(processId, ProcessTypeId.SETUP_DIM, processVersion); + var processData = new[] { process }; + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + A.CallTo(() => _processExecutor.ExecuteProcess(processId, A._, A._)) + .Returns(new[] { IProcessExecutor.ProcessExecutionResult.LockRequested, IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable()); + + var changeHistory = new List<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(); + + A.CallTo(() => _repositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _repositories.Clear()) + .Invokes(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(3, Times.Exactly); + changeHistory.Should().HaveCount(6) + .And.SatisfyRespectively( + first => first.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version != processVersion && x.LockExpiryTime != null && x.Save), + second => second.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[0].Version && x.LockExpiryTime == changeHistory[0].LockExpiryTime && !x.Save), + third => third.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[1].Version && x.LockExpiryTime == changeHistory[1].LockExpiryTime && x.Save), + forth => forth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[2].Version && x.LockExpiryTime == changeHistory[2].LockExpiryTime && !x.Save), + fifth => fifth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version != changeHistory[3].Version && x.LockExpiryTime == null && x.Save), + sixth => sixth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[4].Version && x.LockExpiryTime == null && !x.Save) + ); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_RequestLockTwice() + { + // Arrange + var processId = Guid.NewGuid(); + var processVersion = Guid.NewGuid(); + var process = new Process(processId, ProcessTypeId.SETUP_DIM, processVersion); + var processData = new[] { process }; + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + A.CallTo(() => _processExecutor.ExecuteProcess(processId, A._, A._)) + .Returns(new[] { IProcessExecutor.ProcessExecutionResult.LockRequested, IProcessExecutor.ProcessExecutionResult.LockRequested, IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable()); + + var changeHistory = new List<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(); + + A.CallTo(() => _repositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _repositories.Clear()) + .Invokes(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(4, Times.Exactly); + changeHistory.Should().HaveCount(7) + .And.SatisfyRespectively( + first => first.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version != processVersion && x.LockExpiryTime != null && x.Save), + second => second.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[0].Version && x.LockExpiryTime == changeHistory[0].LockExpiryTime && !x.Save), + third => third.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[1].Version && x.LockExpiryTime == changeHistory[1].LockExpiryTime && !x.Save), + forth => forth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[2].Version && x.LockExpiryTime == changeHistory[2].LockExpiryTime && x.Save), + fifth => fifth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[3].Version && x.LockExpiryTime == changeHistory[3].LockExpiryTime && !x.Save), + sixth => sixth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version != changeHistory[4].Version && x.LockExpiryTime == null && x.Save), + seventh => seventh.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[5].Version && x.LockExpiryTime == null && !x.Save) + ); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_RequestLockThenThrows() + { + // Arrange + var firstId = Guid.NewGuid(); + var secondId = Guid.NewGuid(); + var firstVersion = Guid.NewGuid(); + var secondVersion = Guid.NewGuid(); + var firstProcess = new Process(firstId, ProcessTypeId.SETUP_DIM, firstVersion); + var secondProcess = new Process(secondId, ProcessTypeId.SETUP_DIM, secondVersion); + + var processData = new[] { firstProcess, secondProcess }; + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + IEnumerable ThrowingEnumerable() + { + yield return IProcessExecutor.ProcessExecutionResult.LockRequested; + throw new Exception("normal error"); + } + + Process? process = null; + + A.CallTo(() => _processExecutor.ExecuteProcess(firstId, A._, A._)) + .ReturnsLazily((Guid Id, ProcessTypeId _, CancellationToken _) => + { + process = firstProcess; + return ThrowingEnumerable().ToAsyncEnumerable(); + }); + + A.CallTo(() => _processExecutor.ExecuteProcess(secondId, A._, A._)) + .ReturnsLazily((Guid Id, ProcessTypeId _, CancellationToken _) => + { + process = secondProcess; + return new[] { IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable(); + }); + + var changeHistory = new List<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(); + + A.CallTo(() => _repositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add(process == null + ? (Guid.Empty, Guid.Empty, null, true) + : (process.Id, process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _repositories.Clear()) + .Invokes(() => + { + changeHistory.Add(process == null + ? (Guid.Empty, Guid.Empty, null, false) + : (process.Id, process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(firstId, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _processExecutor.ExecuteProcess(secondId, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappenedTwiceExactly(); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(3, Times.Exactly); + changeHistory.Should().SatisfyRespectively( + first => first.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Id == firstId && x.Version != firstVersion && x.LockExpiryTime != null && x.Save), + second => second.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Id == firstId && x.Version == changeHistory[0].Version && x.LockExpiryTime == changeHistory[0].LockExpiryTime && !x.Save), + second => second.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Id == firstId && x.Version == changeHistory[1].Version && x.LockExpiryTime == changeHistory[0].LockExpiryTime && !x.Save), + third => third.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Id == secondId && x.Version != secondVersion && x.LockExpiryTime == null && x.Save), + forth => forth.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Id == secondId && x.Version == changeHistory[3].Version && x.LockExpiryTime == null && !x.Save) + ); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.Matches(e => e != null && e.Message == "normal error"), A.That.StartsWith("error processing process"))).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_UnmodifiedSafeRequested() + { + // Arrange + var firstVersion = Guid.NewGuid(); + var process = new Process(Guid.NewGuid(), ProcessTypeId.SETUP_DIM, firstVersion); + var processData = new[] { process }; + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .Returns(new[] { IProcessExecutor.ProcessExecutionResult.Unmodified, IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable()); + + var changeHistory = new List<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(); + + A.CallTo(() => _repositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _repositories.Clear()) + .Invokes(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappenedTwiceExactly(); + changeHistory.Should().HaveCount(3) + .And.SatisfyRespectively( + first => first.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == firstVersion && x.LockExpiryTime == null && !x.Save), + second => second.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version != changeHistory[0].Version && x.LockExpiryTime == null && x.Save), + third => third.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[1].Version && x.LockExpiryTime == null && !x.Save) + ); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_RequestLock_SaveAsyncThrows() + { + // Arrange + var firstProcessId = Guid.NewGuid(); + var firstVersion = Guid.NewGuid(); + var firstProcess = new Process(firstProcessId, ProcessTypeId.SETUP_DIM, firstVersion); + var secondProcessId = Guid.NewGuid(); + var secondVersion = Guid.NewGuid(); + var secondProcess = new Process(secondProcessId, ProcessTypeId.SETUP_DIM, secondVersion); + var thirdProcessId = Guid.NewGuid(); + var thirdVersion = Guid.NewGuid(); + var thirdProcess = new Process(thirdProcessId, ProcessTypeId.SETUP_DIM, thirdVersion); + var processData = new[] { firstProcess, secondProcess, thirdProcess }; + var error = new Exception("save conflict error"); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + Process? process = null; + + A.CallTo(() => _processExecutor.ExecuteProcess(firstProcessId, A._, A._)) + .ReturnsLazily((Guid _, ProcessTypeId _, CancellationToken _) => + { + process = firstProcess; + return new[] { IProcessExecutor.ProcessExecutionResult.LockRequested, IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable(); + }); + + A.CallTo(() => _processExecutor.ExecuteProcess(secondProcessId, A._, A._)) + .ReturnsLazily((Guid _, ProcessTypeId _, CancellationToken _) => + { + process = secondProcess; + return new[] { IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable(); + }); + + A.CallTo(() => _processExecutor.ExecuteProcess(thirdProcessId, A._, A._)) + .ReturnsLazily((Guid _, ProcessTypeId _, CancellationToken _) => + { + process = thirdProcess; + return new[] { IProcessExecutor.ProcessExecutionResult.Unmodified }.ToAsyncEnumerable(); + }); + + var changeHistory = new List<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime)>(); + + A.CallTo(() => _repositories.SaveAsync()) + .Throws(error); + + A.CallTo(() => _repositories.Clear()) + .Invokes(() => + { + changeHistory.Add( + process == null + ? (Guid.Empty, Guid.Empty, null) + : (process.Id, process.Version, process.LockExpiryDate)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappenedTwiceExactly(); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(3, Times.Exactly); + changeHistory.Should().HaveCount(3) + .And.SatisfyRespectively( + first => first.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime)>(x => x.Id == firstProcessId && x.Version != firstVersion && x.LockExpiryTime != null), + second => second.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime)>(x => x.Id == secondProcessId && x.Version != secondVersion && x.LockExpiryTime == null), + third => third.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime)>(x => x.Id == thirdProcessId && x.Version == thirdVersion && x.LockExpiryTime == null) + ); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.Matches(e => e != null && e.Message == error.Message), A.That.StartsWith($"error processing process {firstProcessId}"))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.Matches(e => e != null && e.Message == error.Message), A.That.StartsWith($"error processing process {secondProcessId}"))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.Matches(e => e != null && e.Message == error.Message), A.That.StartsWith($"error processing process {thirdProcessId}"))) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_IgnoresLockedProcesses_LogsInformation() + { + var lockExpiryDate = _fixture.Create(); + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.SETUP_DIM, Guid.NewGuid()) { LockExpiryDate = lockExpiryDate }).ToImmutableArray(); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNull(), A.That.StartsWith("skipping locked process"))) + .MustHaveHappened(processData.Length, Times.Exactly); + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustNotHaveHappened(); + A.CallTo(() => _repositories.SaveAsync()) + .MustNotHaveHappened(); + A.CallTo(() => _repositories.Clear()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_WithException_LogsError() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.SETUP_DIM, Guid.NewGuid())).ToImmutableArray(); + var error = new Exception("Only a test"); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .Throws(error); + + Environment.ExitCode = 0; + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + Environment.ExitCode.Should().Be(0); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNull(), A.That.Matches(x => x.StartsWith("start processing process")))).MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.Matches(e => e != null && e.Message == error.Message), A.That.StartsWith("error processing process"))).MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNull(), A.That.Matches(x => x.StartsWith("finished processing process")))).MustNotHaveHappened(); + A.CallTo(() => _mockLogger.Log(LogLevel.Error, A._, A._)).MustNotHaveHappened(); + A.CallTo(() => _repositories.SaveAsync()).MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_WithSystemException_Exits() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.SETUP_DIM, Guid.NewGuid())).ToImmutableArray(); + var error = new SystemException("unrecoverable failure"); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .Throws(error); + + Environment.ExitCode = 0; + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + Environment.ExitCode.Should().Be(1); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNull(), A.That.Matches(x => x.StartsWith("start processing process")))).MustHaveHappenedOnceExactly(); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNotNull(), A._)).MustNotHaveHappened(); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNull(), A.That.Matches(x => x.StartsWith("finished processing process")))).MustNotHaveHappened(); + A.CallTo(() => _mockLogger.Log(LogLevel.Error, A.That.Matches(e => e != null && e.Message == error.Message), $"processing failed with following Exception {error.Message}")).MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.SaveAsync()).MustNotHaveHappened(); + } +} diff --git a/tests/processes/Processes.Worker.Library.Tests/ProcessExecutorTests.cs b/tests/processes/Processes.Worker.Library.Tests/ProcessExecutorTests.cs new file mode 100644 index 0000000..222fcf9 --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/ProcessExecutorTests.cs @@ -0,0 +1,878 @@ +/******************************************************************************** + * 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.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Microsoft.Extensions.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; +using System.Collections.Immutable; +using ProcessTypeId = Dim.Entities.Enums.ProcessTypeId; + +namespace Processes.Worker.Library.Tests; + +public class ProcessExecutorTests +{ + private readonly IProcessTypeExecutor _processTypeExecutor; + private readonly IProcessStepRepository _processStepRepository; + private readonly IProcessExecutor _sut; + private readonly IFixture _fixture; + + public ProcessExecutorTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _processTypeExecutor = A.Fake(); + _processStepRepository = A.Fake(); + + var dimRepositories = A.Fake(); + var logger = A.Fake>(); + + A.CallTo(() => dimRepositories.GetInstance()) + .Returns(_processStepRepository); + + A.CallTo(() => _processTypeExecutor.GetProcessTypeId()) + .Returns(ProcessTypeId.SETUP_DIM); + + _sut = new ProcessExecutor( + new[] { _processTypeExecutor }, + dimRepositories, + logger); + } + + #region GetRegisteredProcessTypeIds + + [Fact] + public void GetRegisteredProcessTypeIds_ReturnsExpected() + { + // Act + var result = _sut.GetRegisteredProcessTypeIds(); + + // Assert + result.Should().HaveCount(1).And.Contain(ProcessTypeId.SETUP_DIM); + } + + #endregion + + #region ExecuteProcess + + [Fact] + public async Task ExecuteProcess_WithInvalidProcessTypeId_Throws() + { + // Arrange + var Act = async () => await _sut.ExecuteProcess(Guid.NewGuid(), (ProcessTypeId)default, CancellationToken.None).ToListAsync().ConfigureAwait(false); + + // Act + var result = await Assert.ThrowsAsync(Act); + + // Assert + result.Message.Should().Be($"processType {(ProcessTypeId)default} is not a registered executable processType."); + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + public async Task ExecuteProcess_WithInitialSteps_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepTypeId = _fixture.Create(); + var processStepData = (Id: Guid.NewGuid(), processStepTypeId); + var initialStepTypeIds = Enum.GetValues().Where(x => x != processStepTypeId).Take(3).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(new[] { processStepData }.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, initialStepTypeIds)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, null, null)); + + IEnumerable? createdProcessSteps = null; + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .ReturnsLazily((IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatus) => + { + createdProcessSteps = processStepTypeStatus.Select(x => new ProcessStep(Guid.NewGuid(), x.ProcessStepTypeId, x.ProcessStepStatusId, x.ProcessId, DateTimeOffset.UtcNow)).ToImmutableList(); + return createdProcessSteps; + }); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustHaveHappenedOnceExactly(); + + createdProcessSteps + .Should().NotBeNull() + .And.HaveSameCount(initialStepTypeIds) + .And.Satisfy( + x => x.ProcessStepTypeId == initialStepTypeIds[0] && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == initialStepTypeIds[1] && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == initialStepTypeIds[2] && x.ProcessStepStatusId == ProcessStepStatusId.TODO); + + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(initialStepTypeIds.Length + 1, Times.Exactly); + modifiedProcessSteps + .Should().HaveCount(initialStepTypeIds.Length + 1) + .And.Satisfy( + x => x.Id == processStepData.Id && x.ProcessStepStatusId == stepStatusId, + x => x.Id == createdProcessSteps!.ElementAt(0).Id && x.ProcessStepStatusId == stepStatusId, + x => x.Id == createdProcessSteps!.ElementAt(1).Id && x.ProcessStepStatusId == stepStatusId, + x => x.Id == createdProcessSteps!.ElementAt(2).Id && x.ProcessStepStatusId == stepStatusId); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + } + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + public async Task ExecuteProcess_NoScheduleOrSkippedSteps_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = _fixture.CreateMany(3).Select(stepTypeId => (Id: Guid.NewGuid(), StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, null, null)); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(processStepData.Length, Times.Exactly); + + modifiedProcessSteps + .Should().HaveSameCount(processStepData) + .And.Satisfy( + x => x.Id == processStepData[0].Id && x.ProcessStepStatusId == stepStatusId, + x => x.Id == processStepData[1].Id && x.ProcessStepStatusId == stepStatusId, + x => x.Id == processStepData[2].Id && x.ProcessStepStatusId == stepStatusId); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + } + } + + [Fact] + public async Task ExecuteProcess_NoExecutableSteps_ReturnsExpected() + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = _fixture.CreateMany().Select(stepTypeId => (Id: Guid.NewGuid(), StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(false); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveCount(1).And.Contain(IProcessExecutor.ProcessExecutionResult.Unmodified); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustNotHaveHappened(); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + public async Task ExecuteProcess_NoScheduleOrSkippedSteps_SingleStepTypeWithDuplicates_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var stepTypeId = _fixture.Create(); + var processStepData = _fixture.CreateMany(3).Select(x => (Id: x, StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, null, null)); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(processStepData.Length, Times.Exactly); + modifiedProcessSteps + .Should().HaveSameCount(processStepData) + .And.Satisfy( + x => x.Id == processStepData[0].Id, + x => x.Id == processStepData[1].Id, + x => x.Id == processStepData[2].Id) + .And.Satisfy( + x => x.ProcessStepStatusId == stepStatusId, + x => x.ProcessStepStatusId == ProcessStepStatusId.DUPLICATE, + x => x.ProcessStepStatusId == ProcessStepStatusId.DUPLICATE); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + } + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + public async Task ExecuteProcess_WithScheduledSteps_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = (Id: Guid.NewGuid(), StepTypeId: _fixture.Create()); + var scheduleStepTypeIds = Enum.GetValues().Where(x => x != processStepData.StepTypeId).Take(3).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(new[] { processStepData }.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(processStepData.StepTypeId, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, scheduleStepTypeIds, null, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A.That.Not.IsEqualTo(processStepData.StepTypeId), A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, null, null)); + + IEnumerable? createdProcessSteps = null; + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .ReturnsLazily((IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatus) => + { + createdProcessSteps = processStepTypeStatus.Select(x => new ProcessStep(Guid.NewGuid(), x.ProcessStepTypeId, x.ProcessStepStatusId, x.ProcessId, DateTimeOffset.UtcNow)).ToImmutableList(); + return createdProcessSteps; + }); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result. + Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappened(scheduleStepTypeIds.Length + 1, Times.Exactly); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustHaveHappenedOnceExactly(); + + createdProcessSteps + .Should().NotBeNull() + .And.HaveSameCount(scheduleStepTypeIds) + .And.Satisfy( + x => x.ProcessStepTypeId == scheduleStepTypeIds[0] && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == scheduleStepTypeIds[1] && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == scheduleStepTypeIds[2] && x.ProcessStepStatusId == ProcessStepStatusId.TODO); + + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(scheduleStepTypeIds.Length + 1, Times.Exactly); + modifiedProcessSteps + .Should().HaveCount(scheduleStepTypeIds.Length + 1) + .And.Satisfy( + x => x.Id == processStepData.Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE, + x => x.Id == createdProcessSteps!.ElementAt(0).Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE, + x => x.Id == createdProcessSteps!.ElementAt(1).Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE, + x => x.Id == createdProcessSteps!.ElementAt(2).Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + } + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + public async Task ExecuteProcess_WithDuplicateScheduledSteps_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var stepTypeId = _fixture.Create(); + var processStepData = (Id: Guid.NewGuid(), StepTypeId: stepTypeId); + var scheduleStepTypeIds = Enumerable.Repeat(stepTypeId, 3); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(new[] { processStepData }.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(stepTypeId, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, scheduleStepTypeIds, null, null)) + .Once() + .Then + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, null, null)); + + IEnumerable? createdProcessSteps = null; + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .ReturnsLazily((IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatus) => + { + createdProcessSteps = processStepTypeStatus.Select(x => new ProcessStep(Guid.NewGuid(), x.ProcessStepTypeId, x.ProcessStepStatusId, x.ProcessId, DateTimeOffset.UtcNow)).ToImmutableList(); + return createdProcessSteps; + }); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + // Assert + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustHaveHappenedOnceExactly(); + + createdProcessSteps + .Should().NotBeNull() + .And.HaveCount(1) + .And.Satisfy( + x => x.ProcessStepTypeId == stepTypeId && x.ProcessStepStatusId == ProcessStepStatusId.TODO); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappened(2, Times.Exactly); + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(2, Times.Exactly); + + modifiedProcessSteps + .Should().HaveCount(2) + .And.Satisfy( + x => x.Id == processStepData.Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE, + x => x.Id == createdProcessSteps!.ElementAt(0).Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + } + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + public async Task ExecuteProcess_WithSkippedSteps_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = _fixture.CreateMany(3).Select(stepTypeId => (Id: Guid.NewGuid(), StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + var skipStepTypeIds = processStepData.Skip(1).Select(x => x.StepTypeId).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, skipStepTypeIds, null)); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(skipStepTypeIds.Length + 1, Times.Exactly); + modifiedProcessSteps + .Should().HaveCount(skipStepTypeIds.Length + 1) + .And.Satisfy( + x => x.Id == processStepData[0].Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE, + x => x.Id == processStepData[1].Id && x.ProcessStepStatusId == ProcessStepStatusId.SKIPPED, + x => x.Id == processStepData[2].Id && x.ProcessStepStatusId == ProcessStepStatusId.SKIPPED); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(skipStepTypeIds.Length, Times.Exactly); + modifiedProcessSteps + .Should().HaveCount(skipStepTypeIds.Length) + .And.Satisfy( + x => x.Id == processStepData[1].Id && x.ProcessStepStatusId == ProcessStepStatusId.SKIPPED, + x => x.Id == processStepData[2].Id && x.ProcessStepStatusId == ProcessStepStatusId.SKIPPED); + } + } + + [Theory] + [InlineData(true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + public async Task ExecuteProcess_ProcessThrowsTestException_ReturnsExpected(bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = _fixture.CreateMany(3).Select(stepTypeId => (Id: Guid.NewGuid(), StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + var error = _fixture.Create(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Throws(error); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappened(processStepData.Length, Times.Exactly); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(processStepData.Length, Times.Exactly); + + modifiedProcessSteps + .Should().HaveCount(processStepData.Length) + .And.Satisfy( + x => x.Id == processStepData[0].Id && x.ProcessStepStatusId == ProcessStepStatusId.FAILED, + x => x.Id == processStepData[1].Id && x.ProcessStepStatusId == ProcessStepStatusId.FAILED, + x => x.Id == processStepData[2].Id && x.ProcessStepStatusId == ProcessStepStatusId.FAILED); + } + + [Theory] + [InlineData(true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + })] + [InlineData(false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + })] + public async Task ExecuteProcess_ProcessThrowsSystemException_Throws(bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = _fixture.CreateMany().Select(stepTypeId => (Id: Guid.NewGuid(), StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + var error = _fixture.Create(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Throws(error); + + var stepResults = new List(); + + var Act = async () => + { + await foreach (var stepResult in _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ConfigureAwait(false)) + { + stepResults.Add(stepResult); + } + }; + + // Act + var result = await Assert.ThrowsAsync(Act); + + // Assert + stepResults.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + result.Message.Should().Be(error.Message); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + } + + #endregion + + [Serializable] + public class TestException : Exception + { + public TestException() { } + public TestException(string message) : base(message) { } + public TestException(string message, Exception inner) : base(message, inner) { } + } +} diff --git a/tests/processes/Processes.Worker.Library.Tests/Processes.Worker.Library.Tests.csproj b/tests/processes/Processes.Worker.Library.Tests/Processes.Worker.Library.Tests.csproj new file mode 100644 index 0000000..5046ca2 --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/Processes.Worker.Library.Tests.csproj @@ -0,0 +1,50 @@ + + + + + net8.0 + enable + enable + false + Processes.Worker.Library.Tests + Processes.Worker.Library.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/processes/Processes.Worker.Library.Tests/Usings.cs b/tests/processes/Processes.Worker.Library.Tests/Usings.cs new file mode 100644 index 0000000..ded99ae --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/Usings.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +global using AutoFixture; +global using AutoFixture.AutoFakeItEasy; +global using FakeItEasy; +global using FluentAssertions; +global using Xunit; diff --git a/tests/shared/Tests.Shared/MockLogger.cs b/tests/shared/Tests.Shared/MockLogger.cs new file mode 100644 index 0000000..951b1a8 --- /dev/null +++ b/tests/shared/Tests.Shared/MockLogger.cs @@ -0,0 +1,53 @@ +/******************************************************************************** + * 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.Extensions.Logging; + +namespace Dim.Tests.Shared; + +public interface IMockLogger +{ + void Log(LogLevel logLevel, Exception? exception, string logMessage); +} + +public class MockLogger : ILogger +{ + private readonly IMockLogger _logger; + + public MockLogger(IMockLogger logger) + { + _logger = logger; + } + + public IDisposable? BeginScope(TState state) where TState : notnull => new TestDisposable(); + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => + _logger.Log(logLevel, exception, formatter(state, exception)); + + public class TestDisposable : IDisposable + { + public void Dispose() + { + GC.SuppressFinalize(this); + } + } +} diff --git a/tests/shared/Tests.Shared/Tests.Shared.csproj b/tests/shared/Tests.Shared/Tests.Shared.csproj new file mode 100644 index 0000000..983211f --- /dev/null +++ b/tests/shared/Tests.Shared/Tests.Shared.csproj @@ -0,0 +1,36 @@ + + + + + + Dim.Tests.Shared + Dim.Tests.Shared + net8.0 + enable + enable + + + + + + + +