From 8e023894aff7438645cf7d7bc241fa5ac8afc1ad Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 21 Aug 2024 21:03:51 +0200 Subject: [PATCH] feat: implement published entity configuration statement persistence --- .env | 1 + .../controllers/EntityStatementController.kt | 2 +- .../src/main/resources/application.properties | 2 +- .../controllers/FederationController.kt | 8 +-- .../src/main/resources/application.properties | 3 +- .../oid/fed/persistence/Persistence.kt | 14 ++--- .../sphereon/oid/fed/persistence/models/4.sqm | 11 ++++ .../models/EntityConfigurationStatement.sq | 30 +++++------ .../oid/fed/persistence/models/Subordinate.sq | 3 ++ .../Persistence.jvm.kt | 21 ++++---- .../oid/fed/services/AccountService.kt | 27 ++++++++-- .../fed/services/EntityStatementService.kt | 24 +++++++-- .../sphereon/oid/fed/services/KeyService.kt | 53 ++++++++++++------- .../oid/fed/services/SubordinateService.kt | 26 +++++---- 14 files changed, 145 insertions(+), 80 deletions(-) diff --git a/.env b/.env index 4ac71d15..5a918d31 100644 --- a/.env +++ b/.env @@ -3,3 +3,4 @@ DATASOURCE_USER=openid-federation-db-user DATASOURCE_PASSWORD=openid-federation-db-password DATASOURCE_DB=openid-federation-db APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb +ROOT_IDENTIFIER=http://localhost:8080 \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt index a32f0b93..fb92e72a 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt @@ -5,7 +5,7 @@ import com.sphereon.oid.fed.services.EntityStatementService import org.springframework.web.bind.annotation.* @RestController -@RequestMapping("/accounts/{accountUsername}/statement") +@RequestMapping("/accounts/{accountUsername}/entity-statement") class EntityStatementController { private val entityStatementService = EntityStatementService() diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 8ba9c117..a3ca1daa 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -6,4 +6,4 @@ spring.datasource.password=${DATASOURCE_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver # Mapping /actuator/health to /status management.endpoints.web.base-path=/ -management.endpoints.web.path-mapping.health=status \ No newline at end of file +management.endpoints.web.path-mapping.health=status diff --git a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/controllers/FederationController.kt b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/controllers/FederationController.kt index 53943c2d..0f769ad8 100644 --- a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/controllers/FederationController.kt +++ b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/controllers/FederationController.kt @@ -22,13 +22,13 @@ class FederationController { } @GetMapping("/list") - fun getRootSubordinatesList(): List { - return subordinateService.findSubordinatesByAccountAsList("root") + fun getRootSubordinatesList(): Array { + return subordinateService.findSubordinatesByAccountAsArray("root") } @GetMapping("/{username}/list") - fun getSubordinatesList(@PathVariable username: String): List { - return subordinateService.findSubordinatesByAccountAsList(username) + fun getSubordinatesList(@PathVariable username: String): Array { + return subordinateService.findSubordinatesByAccountAsArray(username) } @GetMapping("/fetch") diff --git a/modules/federation-server/src/main/resources/application.properties b/modules/federation-server/src/main/resources/application.properties index 523035b3..0ac4201e 100644 --- a/modules/federation-server/src/main/resources/application.properties +++ b/modules/federation-server/src/main/resources/application.properties @@ -5,4 +5,5 @@ spring.datasource.username=${DATASOURCE_USER} spring.datasource.password=${DATASOURCE_PASSWORD} # Mapping /actuator/health to /status management.endpoints.web.base-path=/ -management.endpoints.web.path-mapping.health=status \ No newline at end of file +management.endpoints.web.path-mapping.health=status +server.port=8080 diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index 73d1248b..b3d8157a 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -1,11 +1,13 @@ package com.sphereon.oid.fed.persistence -import com.sphereon.oid.fed.persistence.repositories.AccountRepository -import com.sphereon.oid.fed.persistence.repositories.KeyRepository -import com.sphereon.oid.fed.persistence.repositories.SubordinateRepository +import com.sphereon.oid.fed.persistence.models.AccountQueries +import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries +import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.SubordinateQueries expect object Persistence { - val accountRepository: AccountRepository - val keyRepository: KeyRepository - val subordinateRepository: SubordinateRepository + val entityConfigurationStatementQueries: EntityConfigurationStatementQueries + val accountQueries: AccountQueries + val keyQueries: KeyQueries + val subordinateQueries: SubordinateQueries } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm index e69de29b..ac84d46c 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm @@ -0,0 +1,11 @@ +CREATE TABLE entityConfigurationStatement ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + statement TEXT NOT NULL, + expires_at BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_ParentEntityStatement FOREIGN KEY (account_id) REFERENCES account (id), + UNIQUE (account_id) +); + +CREATE INDEX entity_statement_account_id_index ON subordinate (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq index cb9f5153..9a80ef3b 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq @@ -1,15 +1,15 @@ -findByAccountId: -SELECT * FROM entityStatement WHERE account_id = ?; - -findById: -SELECT * FROM entityStatement WHERE id = ?; - -create: -INSERT INTO entityStatement ( - account_id, - statement, - expires_at -) VALUES (?, ?, ?) RETURNING *; - -findLatestByAccountId: -SELECT * FROM entityStatement WHERE account_id = ? ORDER BY created_at DESC LIMIT 1; \ No newline at end of file +findByAccountId: +SELECT * FROM entityConfigurationStatement WHERE account_id = ?; + +findById: +SELECT * FROM entityConfigurationStatement WHERE id = ?; + +create: +INSERT INTO entityConfigurationStatement ( + account_id, + statement, + expires_at +) VALUES (?, ?, ?) RETURNING *; + +findLatestByAccountId: +SELECT * FROM entityConfigurationStatement WHERE account_id = ? ORDER BY created_at DESC LIMIT 1; \ No newline at end of file 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 6c2a1b92..f226fad3 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 @@ -10,5 +10,8 @@ UPDATE subordinate SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_a findByAccountId: SELECT * FROM subordinate WHERE account_id = ? AND deleted_at IS NULL; +findByAccountIdAndIdentifier: +SELECT * FROM subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; + findById: SELECT * FROM subordinate WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index 913b31d5..c7631466 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -4,23 +4,26 @@ import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlCursor import app.cash.sqldelight.db.SqlDriver import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver -import com.sphereon.oid.fed.persistence.repositories.AccountRepository -import com.sphereon.oid.fed.persistence.repositories.KeyRepository -import com.sphereon.oid.fed.persistence.repositories.SubordinateRepository +import com.sphereon.oid.fed.persistence.models.AccountQueries +import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries +import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.SubordinateQueries actual object Persistence { - actual val accountRepository: AccountRepository - actual val keyRepository: KeyRepository - actual val subordinateRepository: SubordinateRepository + actual val entityConfigurationStatementQueries: EntityConfigurationStatementQueries + actual val accountQueries: AccountQueries + actual val keyQueries: KeyQueries + actual val subordinateQueries: SubordinateQueries init { val driver = getDriver() runMigrations(driver) val database = Database(driver) - accountRepository = AccountRepository(database.accountQueries) - keyRepository = KeyRepository(database.keyQueries) - subordinateRepository = SubordinateRepository(database.subordinateQueries) + accountQueries = database.accountQueries + entityConfigurationStatementQueries = database.entityConfigurationStatementQueries + keyQueries = database.keyQueries + subordinateQueries = database.subordinateQueries } private fun getDriver(): SqlDriver { diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt index 3adc608d..3de14058 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -3,22 +3,39 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.openapi.models.AccountDTO import com.sphereon.oid.fed.openapi.models.CreateAccountDTO import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.extensions.toAccountDTO class AccountService { - private val accountRepository = Persistence.accountRepository + private val accountQueries = Persistence.accountQueries fun create(account: CreateAccountDTO): AccountDTO { - val accountAlreadyExists = accountRepository.findByUsername(account.username) != null + val accountAlreadyExists = accountQueries.findByUsername(account.username).executeAsOneOrNull() - if (accountAlreadyExists) { + if (accountAlreadyExists != null) { throw IllegalArgumentException(Constants.ACCOUNT_ALREADY_EXISTS) } - return accountRepository.create(account).executeAsOne().toAccountDTO() + return accountQueries.create( + username = account.username, + ).executeAsOne().toAccountDTO() } fun findAll(): List { - return accountRepository.findAll().map { it.toAccountDTO() } + return accountQueries.findAll().executeAsList().map { it.toAccountDTO() } + } + + fun getAccountIdentifier(accountUsername: String): String { + val rootIdentifier = System.getenv("ROOT_IDENTIFIER") ?: "https://www.sphereon.com" + + if (accountUsername == "root") { + return rootIdentifier + } + + return "$rootIdentifier/$accountUsername" + } + + fun getAccountByUsername(accountUsername: String): Account { + return accountQueries.findByUsername(accountUsername).executeAsOne() } } 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 b8d43c0d..ca5cf891 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 @@ -4,33 +4,37 @@ 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.persistence.Persistence import com.sphereon.oid.fed.services.extensions.toJwkDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject class EntityStatementService { + private val accountService = AccountService() private val keyService = KeyService() private val subordinateService = SubordinateService() + private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries fun findByUsername(accountUsername: String): EntityConfigurationStatement { + val account = accountService.getAccountByUsername(accountUsername) + val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() val hasSubordinates = subordinateService.findSubordinatesByAccount(accountUsername).isNotEmpty() - println(hasSubordinates); + + val identifier = accountService.getAccountIdentifier(account.username) val entityConfigurationStatement = EntityConfigurationStatementBuilder() - .iss("https://www.sphereon.com") + .iss(identifier) .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) .jwks(keys) if (hasSubordinates) { val federationEntityMetadata = FederationEntityMetadataBuilder() - .identifier(accountUsername) + .identifier(identifier) .build() - println(federationEntityMetadata); - entityConfigurationStatement.metadata( Pair( "federation_entity", @@ -43,9 +47,19 @@ class EntityStatementService { } fun publishByUsername(accountUsername: String): EntityConfigurationStatement { + val account = accountService.getAccountByUsername(accountUsername) + // fetching + val entityConfigurationStatement = findByUsername(accountUsername) // signing + // publishing + entityConfigurationStatementQueries.create( + account_id = account.id, + expires_at = entityConfigurationStatement.exp.toLong(), + statement = Json.encodeToString(EntityConfigurationStatement.serializer(), entityConfigurationStatement) + ) + throw UnsupportedOperationException("Not implemented") } } \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt index f79aa23b..9aac7a58 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -9,44 +9,59 @@ import com.sphereon.oid.fed.services.extensions.encrypt import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO class KeyService { - private val accountRepository = Persistence.accountRepository - private val keyRepository = Persistence.keyRepository + private val accountQueries = Persistence.accountQueries + private val keyQueries = Persistence.keyQueries fun create(accountUsername: String): Jwk { val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + accountQueries.findByUsername(accountUsername).executeAsOne() - val key = keyRepository.create( + val encryptedKeyPair = generateKeyPair().encrypt() + + val key = keyQueries.create( account.id, - generateKeyPair().encrypt() - ) + y = encryptedKeyPair.y, + x = encryptedKeyPair.x, + d = encryptedKeyPair.d, + crv = encryptedKeyPair.crv, + kty = encryptedKeyPair.kty, + use = encryptedKeyPair.use, + alg = encryptedKeyPair.alg, + kid = encryptedKeyPair.kid, + e = encryptedKeyPair.e, + n = encryptedKeyPair.n, + p = encryptedKeyPair.p, + x5c = encryptedKeyPair.x5c, + dp = encryptedKeyPair.dp, + x5t_s256 = encryptedKeyPair.x5tS256, + q = encryptedKeyPair.q, + qi = encryptedKeyPair.qi, + dq = encryptedKeyPair.dq, + x5u = encryptedKeyPair.x5u, + x5t = encryptedKeyPair.x5t, + ).executeAsOne() return key } fun getDecryptedKey(keyId: Int): Jwk { - var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + var key = keyQueries.findById(keyId).executeAsOne() return key.decrypt() } fun getKeys(accountUsername: String): Array { val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - val accountId = account.id - return keyRepository.findByAccountId(accountId) + accountQueries.findByUsername(accountUsername).executeAsOne() + return keyQueries.findByAccountId(account.id).executeAsList().toTypedArray() } fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - val accountId = account.id + accountQueries.findByUsername(accountUsername).executeAsOne() - var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + var key = keyQueries.findById(keyId).executeAsOne() - if (key.account_id != accountId) { + if (key.account_id != account.id) { throw IllegalArgumentException(Constants.KEY_NOT_FOUND) } @@ -54,9 +69,9 @@ class KeyService { throw IllegalArgumentException(Constants.KEY_ALREADY_REVOKED) } - keyRepository.revokeKey(keyId, reason) + keyQueries.revoke(reason, keyId) - key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + key = keyQueries.findById(keyId).executeAsOne() return key.toJwkAdminDTO() } 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 a1a0e1cd..01e16b8a 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 @@ -5,32 +5,30 @@ import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Subordinate class SubordinateService { - private val accountRepository = Persistence.accountRepository - private val subordinateRepository = Persistence.subordinateRepository + private val accountQueries = Persistence.accountQueries + private val subordinateQueries = Persistence.subordinateQueries - fun findSubordinatesByAccount(accountUsername: String): List { - val account = accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + fun findSubordinatesByAccount(accountUsername: String): Array { + val account = accountQueries.findByUsername(accountUsername).executeAsOne() - return subordinateRepository.findByAccountId(account.id) + return subordinateQueries.findByAccountId(account.id).executeAsList().toTypedArray() } - fun findSubordinatesByAccountAsList(accountUsername: String): List { + fun findSubordinatesByAccountAsArray(accountUsername: String): Array { val subordinates = findSubordinatesByAccount(accountUsername) - return subordinates.map { it.identifier } + return subordinates.map { it.identifier }.toTypedArray() } fun createSubordinate(accountUsername: String, subordinateDTO: CreateSubordinateDTO): Subordinate { - val account = accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + val account = accountQueries.findByUsername(accountUsername).executeAsOne() - val subordinateAlreadyExists = subordinateRepository.findByAccountId(account.id) - .any { it.identifier == subordinateDTO.identifier } + val subordinateAlreadyExists = + subordinateQueries.findByAccountIdAndIdentifier(account.id, subordinateDTO.identifier).executeAsList() - if (subordinateAlreadyExists) { + if (subordinateAlreadyExists.isNotEmpty()) { throw IllegalArgumentException(Constants.SUBORDINATE_ALREADY_EXISTS) } - return subordinateRepository.create(account.id, subordinateDTO.identifier) + return subordinateQueries.create(account.id, subordinateDTO.identifier).executeAsOne() } }