Skip to content

Commit

Permalink
feat(Gate): add business partner relations
Browse files Browse the repository at this point in the history
- add new endpoints to search, create, update and delete business partner relations
- add logic for CRUD and constraint checking
- add database table for relations
- add tests
  • Loading branch information
nicoprow committed Jan 9, 2025
1 parent 342af39 commit a20ab68
Show file tree
Hide file tree
Showing 54 changed files with 4,584 additions and 265 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ For changes to the BPDM Helm charts please consult the [changelog](charts/bpdm/C
- BPDM Gate & Orchestrator: Enhance the error handling mechanism for the orchestrator and gate components by extending the list of available error codes.([#1003](https://github.com/eclipse-tractusx/bpdm/pull/1003#pullrequestreview-2477395867))
- BPDM System Test: Tester module which performs automated end-to-end tests on an existing golden record process.([#1070](https://github.com/eclipse-tractusx/bpdm/pull/1070))
- BPDM Gate & Pool: Update linkage from AdditionalAddress to SiteMainAddress and LegalAddress to LegalAndSiteMainAddress.([#1121](https://github.com/eclipse-tractusx/bpdm/issues/1121))
- BPDM Gate: Add endpoints for managing business partner relations ([#1027](https://github.com/eclipse-tractusx/bpdm/issues/1027))

### Changed

Expand Down
12 changes: 6 additions & 6 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ maven/mavencentral/commons-logging/commons-logging/1.2, Apache-2.0, approved, CQ
maven/mavencentral/io.cucumber/ci-environment/10.0.1, MIT, approved, #15218
maven/mavencentral/io.cucumber/cucumber-core/7.20.1, MIT AND (Apache-2.0 AND MIT), approved, #18204
maven/mavencentral/io.cucumber/cucumber-expressions/17.1.0, MIT, approved, #14271
maven/mavencentral/io.cucumber/cucumber-gherkin-messages/7.20.1, None, restricted, #18205
maven/mavencentral/io.cucumber/cucumber-gherkin-messages/7.20.1, MIT, approved, #18205
maven/mavencentral/io.cucumber/cucumber-gherkin/7.20.1, MIT, approved, clearlydefined
maven/mavencentral/io.cucumber/cucumber-java/7.20.1, MIT, approved, clearlydefined
maven/mavencentral/io.cucumber/cucumber-junit/7.20.1, None, restricted, #18206
maven/mavencentral/io.cucumber/cucumber-junit/7.20.1, MIT, approved, #18206
maven/mavencentral/io.cucumber/cucumber-plugin/7.20.1, MIT, approved, clearlydefined
maven/mavencentral/io.cucumber/cucumber-spring/7.20.1, MIT, approved, clearlydefined
maven/mavencentral/io.cucumber/datatable/7.20.1, Apache-2.0 AND MIT, approved, clearlydefined
maven/mavencentral/io.cucumber/docstring/7.20.1, None, restricted, #18207
maven/mavencentral/io.cucumber/docstring/7.20.1, MIT, approved, #18207
maven/mavencentral/io.cucumber/gherkin/28.0.0, MIT, approved, #14276
maven/mavencentral/io.cucumber/html-formatter/21.7.0, Apache-2.0 AND MIT, approved, #18208
maven/mavencentral/io.cucumber/junit-xml-formatter/0.5.0, MIT, approved, clearlydefined
Expand Down Expand Up @@ -196,9 +196,9 @@ maven/mavencentral/org.rnorth.duct-tape/duct-tape/1.0.8, MIT, approved, clearlyd
maven/mavencentral/org.skyscreamer/jsonassert/1.5.3, Apache-2.0, approved, clearlydefined
maven/mavencentral/org.slf4j/jul-to-slf4j/2.0.16, MIT, approved, #7698
maven/mavencentral/org.slf4j/slf4j-api/2.0.16, MIT, approved, #5915
maven/mavencentral/org.springdoc/springdoc-openapi-starter-common/2.8.1, , restricted, clearlydefined
maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-api/2.8.1, , restricted, clearlydefined
maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-ui/2.8.1, , restricted, clearlydefined
maven/mavencentral/org.springdoc/springdoc-openapi-starter-common/2.8.1, Apache-2.0, approved, #18209
maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-api/2.8.1, Apache-2.0, approved, #18210
maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-ui/2.8.1, Apache-2.0, approved, #18211
maven/mavencentral/org.springframework.boot/spring-boot-actuator-autoconfigure/3.4.1, Apache-2.0, approved, #17576
maven/mavencentral/org.springframework.boot/spring-boot-actuator/3.4.1, Apache-2.0, approved, #17574
maven/mavencentral/org.springframework.boot/spring-boot-autoconfigure/3.4.1, Apache-2.0, approved, #17570
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ package org.eclipse.tractusx.bpdm.test.containers

import dasniko.testcontainers.keycloak.KeycloakContainer
import org.eclipse.tractusx.bpdm.test.config.SelfClientConfigProperties
import org.eclipse.tractusx.bpdm.test.containers.KeyCloakInitializer.Companion.TENANT_BPNL
import org.eclipse.tractusx.bpdm.test.containers.KeyCloakInitializer.Companion.keycloakContainer
import org.keycloak.representations.idm.ClientRepresentation
import org.keycloak.representations.idm.ProtocolMapperRepresentation
import org.springframework.boot.test.util.TestPropertyValues
import org.springframework.context.ApplicationContextInitializer
import org.springframework.context.ConfigurableApplicationContext
Expand Down Expand Up @@ -105,17 +107,42 @@ abstract class CreateNewSelfClientInitializer: SelfClientInitializer(){

val createdClientUuid = clients.findByClientId(clientId).first().id


val newProtocolMapper = ProtocolMapperRepresentation().apply {
name = "BPN"
protocol = "openid-connect"
protocolMapper = "oidc-usermodel-attribute-mapper"
config = mapOf(
"introspection.token.claim" to "true",
"userinfo.token.claim" to "true",
"user.attribute" to "bpn",
"id.token.claim" to "true",
"access.token.claim" to "true",
"claim.name" to "bpn",
"jsonType.label" to "String"
)
}
clients
.get(createdClientUuid)
.protocolMappers
.createMapper(newProtocolMapper)

val newServiceAccount = clients
.get(createdClientUuid)
.serviceAccountUser

newServiceAccount.attributes = mutableMapOf(Pair("bpn", listOf(TENANT_BPNL)))

realm.users()
.get(newServiceAccount.id)
.update(newServiceAccount)

realm.users()
.get(newServiceAccount.id)
.roles()
.clientLevel(roleManagementClient.toRepresentation().id)
.add(listOf(role))


super.initialize(applicationContext)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* SPDX-License-Identifier: Apache-2.0
******************************************************************************/

package org.eclipse.tractusx.bpdm.test.system.utils
package org.eclipse.tractusx.bpdm.test.testdata.gate

import com.neovisionaries.i18n.CountryCode
import org.eclipse.tractusx.bpdm.common.dto.AddressType
Expand All @@ -30,15 +30,14 @@ import org.eclipse.tractusx.bpdm.gate.api.model.request.BusinessPartnerInputRequ
import org.eclipse.tractusx.bpdm.gate.api.model.response.AddressRepresentationInputDto
import org.eclipse.tractusx.bpdm.gate.api.model.response.LegalEntityRepresentationInputDto
import org.eclipse.tractusx.bpdm.gate.api.model.response.SiteRepresentationInputDto
import org.eclipse.tractusx.bpdm.test.system.config.TestRunData
import java.time.Duration
import java.time.Instant
import java.time.ZoneOffset
import kotlin.random.Random

class GateInputFactory(
private val testMetadata: TestMetadata,
private val testRunData: TestRunData
private val testRunData: TestRunData?
) {
val genericFullValidWithSiteWithoutAnyBpn = createAllFieldsFilled("genericFullValidWithSiteWithoutAnyBpn")
{ it.withoutAnyBpn().withAddressType(null) }
Expand All @@ -48,7 +47,7 @@ class GateInputFactory(
}

fun createFullValid(seed: String, externalId: String = seed): BusinessPartnerInputRequest {
return SeededTestDataCreator(seed).createAllFieldsFilled().copy(externalId = testRunData.toExternalId(externalId))
return SeededTestDataCreator(seed).createAllFieldsFilled().copy(externalId = testRunData?.toExternalId(externalId) ?: externalId)
}

inner class SeededTestDataCreator(
Expand All @@ -62,7 +61,7 @@ class GateInputFactory(
fun createAllFieldsFilled(): BusinessPartnerInputRequest{

return BusinessPartnerInputRequest(
externalId = "${seed}_${testRunData.testTime}",
externalId = testRunData?.let { "${seed}_${testRunData.testTime}" } ?: seed ,
nameParts = listRange.map { "Name Part $seed $it" },
identifiers = emptyList(),
roles = BusinessPartnerRole.entries,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* SPDX-License-Identifier: Apache-2.0
******************************************************************************/

package org.eclipse.tractusx.bpdm.test.system.config
package org.eclipse.tractusx.bpdm.test.testdata.gate

import java.time.Instant

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,15 @@ import java.util.stream.Collectors
class CustomJwtAuthenticationConverter(private val resourceId: String, private val requiredBpn: String = "") : Converter<Jwt, AbstractAuthenticationToken> {
private val defaultGrantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
override fun convert(source: Jwt): AbstractAuthenticationToken {
val authorities: Collection<GrantedAuthority> =
defaultGrantedAuthoritiesConverter.convert(source)!!.plus(extractResourceRoles(source, resourceId, requiredBpn)).toSet()

val bpn = source.claims["bpn"] as String? ?: ""
val tokenAttributes = mutableMapOf<String, Any>("bpn" to bpn)

val authorities: Collection<GrantedAuthority> =
defaultGrantedAuthoritiesConverter.convert(source)!!
.plus(extractResourceRoles(source, resourceId, requiredBpn))
.let { if(bpn.isNotBlank()) it.plus(SimpleGrantedAuthority(bpn)) else it }
.toSet()

return JwtAuthenticationToken(source, authorities, tokenAttributes.toString())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ import org.springframework.web.bind.annotation.ResponseStatus


@ResponseStatus(HttpStatus.BAD_REQUEST)
class BpdmValidationErrorException(
open class BpdmValidationErrorException(
val validationErrors: List<ValidationError>
): RuntimeException(validationErrors.joinToString())
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import org.eclipse.tractusx.bpdm.gate.api.GateBusinessPartnerApi.Companion.BUSINESS_PARTNER_PATH
import org.eclipse.tractusx.bpdm.gate.api.model.response.BusinessPartnerInputDto
import org.eclipse.tractusx.bpdm.gate.api.model.response.PartnerUploadErrorResponse
import org.eclipse.tractusx.bpdm.gate.api.model.response.GateErrorResponse
import org.springframework.core.io.ByteArrayResource
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
Expand Down Expand Up @@ -55,7 +55,7 @@ interface GatePartnerUploadApi {
ApiResponse(responseCode = "400", description = "On malformed Business partner upload request",
content = [Content(
mediaType = "application/json",
schema = Schema(implementation = PartnerUploadErrorResponse::class)
schema = Schema(implementation = GateErrorResponse::class)
)]),
]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*******************************************************************************
* Copyright (c) 2021,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
******************************************************************************/

package org.eclipse.tractusx.bpdm.gate.api

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import jakarta.validation.Valid
import org.eclipse.tractusx.bpdm.common.dto.PageDto
import org.eclipse.tractusx.bpdm.common.dto.PaginationRequest
import org.eclipse.tractusx.bpdm.gate.api.GateRelationApi.Companion.RELATIONS_PATH
import org.eclipse.tractusx.bpdm.gate.api.model.RelationDto
import org.eclipse.tractusx.bpdm.gate.api.model.RelationType
import org.eclipse.tractusx.bpdm.gate.api.model.request.RelationPostRequest
import org.eclipse.tractusx.bpdm.gate.api.model.request.RelationPutRequest
import org.eclipse.tractusx.bpdm.gate.api.model.response.GateErrorResponse
import org.springdoc.core.annotations.ParameterObject
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.*
import java.time.Instant

@RequestMapping(RELATIONS_PATH, produces = [MediaType.APPLICATION_JSON_VALUE])
interface GateRelationApi {

companion object{
const val RELATIONS_PATH = "${ApiCommons.BASE_PATH}/input/business-partner-relations"
}

@Operation(
summary = "Find business partner input relations",
description = "Find paginated list of business partner relations from the input stage. " +
"There are various filter criteria available. " +
"All filters are 'AND' filters."
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "A paginated list of business partner relations for the input stage")
]
)
@GetMapping
fun get(
@Schema(description = "Only show relations with the given external identifiers")
@RequestParam externalIds: List<String>? = null,
@Schema(description = "Only show relations of the given type")
@RequestParam relationType: RelationType? = null,
@Schema(description = "Only show relations which have the given business partners as sources")
@RequestParam businessPartnerSourceExternalIds: List<String>? = null,
@Schema(description = "Only show relations which have the given business partners as targets")
@RequestParam businessPartnerTargetExternalIds: List<String>? = null,
@Schema(description = "Only show relations which have been modified after the given time stamp")
@RequestParam updatedAtFrom: Instant? = null,
@ParameterObject @Valid paginationRequest: PaginationRequest = PaginationRequest()
): PageDto<RelationDto>

@Operation(
summary = "Create a new business partner input relation",
description = "Create a new relation between two business partner entries on the input stage. " +
"The external identifier is optional and a new one will be automatically created if not given. " +
"A given external identifier has to be unique."
)
@ApiResponses(
value = [
ApiResponse(responseCode = "201", description = "The created business partner input relation"),
ApiResponse(responseCode = "400", description = "If the business partner could not be created based on wrong or insufficient data provided such as non-existent business partners or violated relation constraints. ",
content = [Content(
mediaType = "application/json",
schema = Schema(implementation = GateErrorResponse::class)
)]),
ApiResponse(responseCode = "409", description = "If a relation with the given external identifier already exists",
content = [Content(
mediaType = "application/json",
schema = Schema(implementation = GateErrorResponse::class)
)]),
]
)
@PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE])
fun post(
@RequestBody requestBody: RelationPostRequest
): RelationDto

@Operation(
summary = "Update a business partner input relation",
description = "Update an existing business partner relation on the input stage. " +
"By using a request parameter it is also possible to create a relation if the relation with the given external identifier does not exist. "
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "The given business partner relation has been updated."),
ApiResponse(responseCode = "400", description = "On wrong or insufficient user provided data like references to non-existent business partners or relations that violate the relation constraints. ",
content = [Content(
mediaType = "application/json",
schema = Schema(implementation = GateErrorResponse::class)
)])
]
)
@PutMapping(consumes = [MediaType.APPLICATION_JSON_VALUE])
fun put(
@Schema(description = "If true a business partner relation will be created even if a relation could not be found under the given external identifier.")
@RequestParam createIfNotExist: Boolean = false,
@RequestBody requestBody: RelationPutRequest
): RelationDto

@Operation(
summary = "Delete an existing business partner relation",
description = "Delete a relation between two business partners on the input stage."
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "The specified relation has been deleted."),
ApiResponse(responseCode = "400", description = "On specifying a relation that does not exist. ",
content = [Content(
mediaType = "application/json",
schema = Schema(implementation = GateErrorResponse::class)
)])
]
)
@DeleteMapping
fun delete(
@Schema(description = "The external identifier of the business partner relation to delete")
@RequestParam externalId: String
)


}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

package org.eclipse.tractusx.bpdm.gate.api.client

import org.eclipse.tractusx.bpdm.gate.api.StatsApi

interface GateClient {

val businessParters: BusinessPartnerApiClient
Expand All @@ -32,4 +30,6 @@ interface GateClient {
val stats: StatsApiClient

val partnerUpload: PartnerUploadApiClient

val relation: RelationApiClient
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class GateClientImpl(

override val partnerUpload by lazy { createClient<PartnerUploadApiClient>() }

override val relation: RelationApiClient by lazy { createClient<RelationApiClient>() }

private inline fun <reified T> createClient() =
httpServiceProxyFactory.createClient(T::class.java)
}
Loading

0 comments on commit a20ab68

Please sign in to comment.