Skip to content

Commit

Permalink
feat: implement published entity configuration statement persistence
Browse files Browse the repository at this point in the history
  • Loading branch information
jcmelati committed Aug 21, 2024
1 parent ab53661 commit 8e02389
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 80 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
management.endpoints.web.path-mapping.health=status
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ class FederationController {
}

@GetMapping("/list")
fun getRootSubordinatesList(): List<String> {
return subordinateService.findSubordinatesByAccountAsList("root")
fun getRootSubordinatesList(): Array<String> {
return subordinateService.findSubordinatesByAccountAsArray("root")
}

@GetMapping("/{username}/list")
fun getSubordinatesList(@PathVariable username: String): List<String> {
return subordinateService.findSubordinatesByAccountAsList(username)
fun getSubordinatesList(@PathVariable username: String): Array<String> {
return subordinateService.findSubordinatesByAccountAsArray(username)
}

@GetMapping("/fetch")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
management.endpoints.web.path-mapping.health=status
server.port=8080
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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;
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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountDTO> {
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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,69 @@ 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<Jwk> {
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)
}

if (key.revoked_at != null) {
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()
}
Expand Down
Loading

0 comments on commit 8e02389

Please sign in to comment.