From 35b8e90becb0afa2761c72eed732a2614674b95d Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 21 Aug 2024 13:10:08 +0200 Subject: [PATCH] feat: implement subordinate relationship create --- .../controllers/SubordinateController.kt | 21 +++- .../com/sphereon/oid/fed/openapi/openapi.yaml | 119 ++++++++++++++++++ .../FederationEntityMetadataBuilder.kt | 16 +++ .../repositories/SubordinateRepository.kt | 4 +- .../sphereon/oid/fed/persistence/models/3.sqm | 6 +- .../oid/fed/persistence/models/Subordinate.sq | 2 +- .../sphereon/oid/fed/services/Constants.kt | 1 + .../fed/services/EntityStatementService.kt | 37 +++--- .../oid/fed/services/SubordinateService.kt | 17 ++- .../extensions/SubordinateExtension.kt | 13 ++ 10 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateController.kt index f11bbdff..84359641 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateController.kt @@ -1,11 +1,11 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO +import com.sphereon.oid.fed.openapi.models.SubordinateAdminDTO import com.sphereon.oid.fed.persistence.models.Subordinate import com.sphereon.oid.fed.services.SubordinateService -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import com.sphereon.oid.fed.services.extensions.toSubordinateAdminDTO +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/accounts/{accountUsername}/subordinates") @@ -13,7 +13,16 @@ class SubordinateController { private val subordinateService = SubordinateService() @GetMapping - fun getSubordinates(@PathVariable accountUsername: String): List { - return subordinateService.findSubordinatesByAccount(accountUsername) + fun getSubordinates(@PathVariable accountUsername: String): Array { + return subordinateService.findSubordinatesByAccount(accountUsername).map { it.toSubordinateAdminDTO() } + .toTypedArray() + } + + @PostMapping + fun createSubordinate( + @PathVariable accountUsername: String, + @RequestBody subordinate: CreateSubordinateDTO + ): Subordinate { + return subordinateService.createSubordinate(accountUsername, subordinate) } } \ No newline at end of file diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index de06301d..5d8bb84d 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -1165,6 +1165,81 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /accounts/{accountUsername}/subordinates: + post: + tags: + - Account Admin + - Account User + summary: Create a new Subordinate entry + description: Create a new Subordinate relationship. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + requestBody: + description: Subordinate data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSubordinateDTO' + responses: + '201': + description: Subordinate relationship created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateAdminDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Account Admin + - Account User + summary: List Subordinates + description: List all active Subordinates for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful list of Subordinates + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SubordinateAdminDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /account/{accountUsername}/subordinate-statement: post: tags: @@ -3399,6 +3474,50 @@ components: required: - username + CreateSubordinateDTO: + type: object + properties: + identifier: + type: string + description: The identifier of the subordinate account. + example: https://www.sphereon.com/subordinate + required: + - identifier + + SubordinateAdminDTO: + type: object + properties: + id: + type: integer + format: int32 + description: The unique identifier of the subordinate. + example: 123 + accountId: + type: integer + format: int32 + description: The ID of the account associated with this subordinate. + example: 456 + identifier: + type: string + description: The unique identifier for the subordinate. + example: https://www.sphereon.com/subordinate + createdAt: + type: string + format: date-time + description: The timestamp when the subordinate was created. + example: 2023-08-21T14:52:00Z + deletedAt: + type: string + format: date-time + nullable: true + description: The timestamp when the subordinate was deleted, if applicable. + example: 2024-08-21T14:52:00Z + required: + - id + - accountId + - identifier + - createdAt + AddUserToAccountRequest: type: object properties: diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt new file mode 100644 index 00000000..30241882 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt @@ -0,0 +1,16 @@ +package com.sphereon.oid.fed.common.builder + +import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata + +class FederationEntityMetadataBuilder { + private var identifier: String? = null + + fun identifier(identifier: String) = apply { this.identifier = identifier } + + fun build(): FederationEntityMetadata { + return FederationEntityMetadata( + federationListEndpoint = "${identifier}/list", + federationFetchEndpoint = "${identifier}/fetch" + ) + } +} \ No newline at end of file diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt index 8cf9bc6e..a3799146 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt @@ -8,8 +8,8 @@ class SubordinateRepository(private val subordinateQueries: SubordinateQueries) return subordinateQueries.findByAccountId(accountId).executeAsList() } - fun create(accountId: Int, subordinateIdentifier: String): Subordinate { - return subordinateQueries.create(account_id = accountId, subordinate_identifier = subordinateIdentifier) + fun create(accountId: Int, identifier: String): Subordinate { + return subordinateQueries.create(account_id = accountId, identifier) .executeAsOne() } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm index e8764795..6aba4c3b 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm @@ -1,12 +1,12 @@ CREATE TABLE subordinate ( id SERIAL PRIMARY KEY, account_id INT NOT NULL, - subordinate_identifier TEXT NOT NULL, + identifier TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, CONSTRAINT FK_ParentSubordinate FOREIGN KEY (account_id) REFERENCES account (id), - UNIQUE (account_id, subordinate_identifier) + UNIQUE (account_id, identifier) ); CREATE INDEX subordinate_account_id_index ON subordinate (account_id); -CREATE INDEX subordinate_account_id_subordinate_identifier_index ON subordinate (account_id, subordinate_identifier); +CREATE INDEX subordinate_account_id_subordinate_identifier_index ON subordinate (account_id, identifier); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq index af7164d0..6c2a1b92 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -1,7 +1,7 @@ create: INSERT INTO subordinate ( account_id, - subordinate_identifier + identifier ) VALUES (?, ?) RETURNING *; delete: diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index 4d2511ad..871bf596 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -6,5 +6,6 @@ class Constants { const val ACCOUNT_NOT_FOUND = "Account not found" const val KEY_NOT_FOUND = "Key not found" const val KEY_ALREADY_REVOKED = "Key already revoked" + const val SUBORDINATE_ALREADY_EXISTS = "Subordinate already exists" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt index c67d85d3..b8d43c0d 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt @@ -1,38 +1,45 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementBuilder +import com.sphereon.oid.fed.common.builder.FederationEntityMetadataBuilder import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata import com.sphereon.oid.fed.services.extensions.toJwkDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject class EntityStatementService { private val keyService = KeyService() + private val subordinateService = SubordinateService() fun findByUsername(accountUsername: String): EntityConfigurationStatement { - val metadata = Pair( - "federation_entity", Json.parseToJsonElement( - "{\n" + - " \"federation_fetch_endpoint\": \"https://www.sphereon.com/fetch\",\n" + - " \"federation_resolve_endpoint\": \"https://www.sphereon.com/resolve\",\n" + - " \"federation_list_endpoint\": \"https://www.sphereon.com/list\"\n" + - " }" - ).jsonObject - ) - val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() + val hasSubordinates = subordinateService.findSubordinatesByAccount(accountUsername).isNotEmpty() + println(hasSubordinates); + val entityConfigurationStatement = EntityConfigurationStatementBuilder() .iss("https://www.sphereon.com") .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) - .metadata( - metadata - ) .jwks(keys) - .build() - return entityConfigurationStatement + if (hasSubordinates) { + val federationEntityMetadata = FederationEntityMetadataBuilder() + .identifier(accountUsername) + .build() + + println(federationEntityMetadata); + + entityConfigurationStatement.metadata( + Pair( + "federation_entity", + Json.encodeToJsonElement(FederationEntityMetadata.serializer(), federationEntityMetadata).jsonObject + ) + ) + } + + return entityConfigurationStatement.build() } fun publishByUsername(accountUsername: String): EntityConfigurationStatement { diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt index d517598c..a1a0e1cd 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Subordinate @@ -16,6 +17,20 @@ class SubordinateService { fun findSubordinatesByAccountAsList(accountUsername: String): List { val subordinates = findSubordinatesByAccount(accountUsername) - return subordinates.map { it.subordinate_identifier } + return subordinates.map { it.identifier } + } + + fun createSubordinate(accountUsername: String, subordinateDTO: CreateSubordinateDTO): Subordinate { + val account = accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val subordinateAlreadyExists = subordinateRepository.findByAccountId(account.id) + .any { it.identifier == subordinateDTO.identifier } + + if (subordinateAlreadyExists) { + throw IllegalArgumentException(Constants.SUBORDINATE_ALREADY_EXISTS) + } + + return subordinateRepository.create(account.id, subordinateDTO.identifier) } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt new file mode 100644 index 00000000..97587b27 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt @@ -0,0 +1,13 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.SubordinateAdminDTO +import com.sphereon.oid.fed.persistence.models.Subordinate + +fun Subordinate.toSubordinateAdminDTO(): SubordinateAdminDTO { + return SubordinateAdminDTO( + id = this.id, + accountId = this.account_id, + identifier = this.identifier, + createdAt = this.created_at.toString(), + ) +}