From 6fa7e80c1c8d523a7da428d231fcb935d338bf31 Mon Sep 17 00:00:00 2001 From: mikeplotean <101570226+mikeplotean@users.noreply.github.com> Date: Wed, 13 Dec 2023 21:01:06 +0200 Subject: [PATCH] chore: event log UI (#37) * chore: key create log on did create * chore: event-log table * chore: display eventlog menu item * chore: updated events table to allow nullable wallet-ids --- .../id/walt/webwallet/db/models/Events.kt | 8 +- .../webwallet/service/SSIKit2WalletService.kt | 153 ++++++++++-------- .../service/account/AccountsService.kt | 28 +++- .../id/walt/webwallet/service/events/Event.kt | 6 +- .../webwallet/service/events/EventService.kt | 5 +- .../web/controllers/AuthController.kt | 24 --- .../web/controllers/EventLogController.kt | 2 +- waltid-web-wallet/web/src/layouts/default.vue | 2 + .../src/pages/wallet/[wallet]/eventlog.vue | 103 ++++++++++++ 9 files changed, 232 insertions(+), 99 deletions(-) create mode 100644 waltid-web-wallet/web/src/pages/wallet/[wallet]/eventlog.vue diff --git a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/db/models/Events.kt b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/db/models/Events.kt index 431050859..b8b81d7a5 100644 --- a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/db/models/Events.kt +++ b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/db/models/Events.kt @@ -1,14 +1,14 @@ package id.walt.webwallet.db.models -import kotlinx.uuid.exposed.KotlinxUUIDTable import kotlinx.uuid.exposed.kotlinxUUID +import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.javatime.timestamp -object Events : KotlinxUUIDTable("events") { +object Events : IntIdTable("events") { val tenant = varchar("tenant", 128).default("global") val originator = varchar("originator", 128).default("unknown") - val account = kotlinxUUID("accountId") - val wallet = reference("wallet", Wallets) + val account = kotlinxUUID("account") + val wallet = kotlinxUUID("wallet").nullable() val timestamp = timestamp("timestamp") val event = varchar("event", 48) val action = varchar("action", 48) diff --git a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/SSIKit2WalletService.kt b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/SSIKit2WalletService.kt index 3fb7bf820..a037dbcfe 100644 --- a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/SSIKit2WalletService.kt +++ b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/SSIKit2WalletService.kt @@ -58,11 +58,11 @@ import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import java.net.URLDecoder -import kotlin.math.abs import kotlin.time.Duration.Companion.seconds -class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : WalletService(tenant, accountId, walletId) { +class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : + WalletService(tenant, accountId, walletId) { companion object { init { @@ -84,7 +84,7 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W it.document } - override suspend fun deleteCredential(id: String) = let{ + override suspend fun deleteCredential(id: String) = let { CredentialsService.get(walletId, id)?.run { logEvent(EventType.Credential.Delete, "wallet", createCredentialEventData(this.parsedDocument, null)) } @@ -192,12 +192,15 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W ) : IllegalArgumentException(message) - - /** * @return redirect uri */ - override suspend fun usePresentationRequest(request: String, did: String, selectedCredentialIds: List, disclosures: Map>?): Result { + override suspend fun usePresentationRequest( + request: String, + did: String, + selectedCredentialIds: List, + disclosures: Map>? + ): Result { val credentialWallet = getCredentialWallet(did) val authReq = AuthorizationRequest.fromHttpQueryString(Url(request).encodedQuery) @@ -205,12 +208,15 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W println("USING PRESENTATION REQUEST, SELECTED CREDENTIALS: $selectedCredentialIds") - SessionAttributes.HACK_outsideMappedSelectedCredentialsPerSession[authReq.state + authReq.presentationDefinition] = selectedCredentialIds + SessionAttributes.HACK_outsideMappedSelectedCredentialsPerSession[authReq.state + authReq.presentationDefinition] = + selectedCredentialIds if (disclosures != null) { - SessionAttributes.HACK_outsideMappedSelectedDisclosuresPerSession[authReq.state + authReq.presentationDefinition] = disclosures + SessionAttributes.HACK_outsideMappedSelectedDisclosuresPerSession[authReq.state + authReq.presentationDefinition] = + disclosures } - val presentationSession = credentialWallet.initializeAuthorization(authReq, 60.seconds, selectedCredentialIds.toSet()) + val presentationSession = + credentialWallet.initializeAuthorization(authReq, 60.seconds, selectedCredentialIds.toSet()) println("Initialized authorization (VPPresentationSession): $presentationSession") println("Resolved presentation definition: ${presentationSession.authorizationRequest!!.presentationDefinition!!.toJSONString()}") @@ -265,7 +271,8 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W override suspend fun resolvePresentationRequest(request: String): String { val credentialWallet = getAnyCredentialWallet() - return Url(request).protocolWithAuthority.plus("?").plus(credentialWallet.parsePresentationRequest(request).toHttpQueryString()) + return Url(request).protocolWithAuthority.plus("?") + .plus(credentialWallet.parsePresentationRequest(request).toHttpQueryString()) } @@ -279,7 +286,8 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W ) } - private fun getAnyCredentialWallet() = credentialWallets.values.firstOrNull() ?: getCredentialWallet("did:test:test") + private fun getAnyCredentialWallet() = + credentialWallets.values.firstOrNull() ?: getCredentialWallet("did:test:test") private val testCIClientConfig = OpenIDClientConfig("test-client", null, redirectUri = "http://blank") @@ -361,7 +369,8 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W }.body().let { BatchCredentialResponse.fromJSON(it) } println("credentialResponses: $credentialResponses") - credentialResponses.credentialResponses ?: throw IllegalArgumentException("No credential responses returned") + credentialResponses.credentialResponses + ?: throw IllegalArgumentException("No credential responses returned") } credReqs.size == 1 -> { @@ -390,51 +399,54 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W val credentialJwt = credential.decodeJws(withSignature = true) - val credentialResultPair = when (val typ = credentialJwt.header["typ"]?.jsonPrimitive?.content?.lowercase()) { - "jwt" -> { - val credentialId = credentialJwt.payload["vc"]!!.jsonObject["id"]?.jsonPrimitive?.content?.takeIf { it.isNotBlank() } - ?: randomUUID() - - println("Got JWT credential: $credentialJwt") - - Pair( - WalletCredential( - wallet = walletId, - id = credentialId, - document = credential, - disclosures = null, - addedOn = Clock.System.now() - ), createCredentialEventData(credentialJwt.payload["vc"]?.jsonObject, typ) - ) - } + val credentialResultPair = + when (val typ = credentialJwt.header["typ"]?.jsonPrimitive?.content?.lowercase()) { + "jwt" -> { + val credentialId = + credentialJwt.payload["vc"]!!.jsonObject["id"]?.jsonPrimitive?.content?.takeIf { it.isNotBlank() } + ?: randomUUID() + + println("Got JWT credential: $credentialJwt") + + Pair( + WalletCredential( + wallet = walletId, + id = credentialId, + document = credential, + disclosures = null, + addedOn = Clock.System.now() + ), createCredentialEventData(credentialJwt.payload["vc"]?.jsonObject, typ) + ) + } - "vc+sd-jwt" -> { - val credentialId = credentialJwt.payload["id"]?.jsonPrimitive?.content?.takeIf { it.isNotBlank() } - ?: randomUUID() + "vc+sd-jwt" -> { + val credentialId = + credentialJwt.payload["id"]?.jsonPrimitive?.content?.takeIf { it.isNotBlank() } + ?: randomUUID() - println("Got SD-JWT credential: $credentialJwt") + println("Got SD-JWT credential: $credentialJwt") - val disclosures = credentialJwt.signature.split("~").drop(1) - println("Disclosures (${disclosures.size}): $disclosures") + val disclosures = credentialJwt.signature.split("~").drop(1) + println("Disclosures (${disclosures.size}): $disclosures") - val disclosuresString = disclosures.joinToString("~") + val disclosuresString = disclosures.joinToString("~") - val credentialWithoutDisclosures = credential.substringBefore("~") + val credentialWithoutDisclosures = credential.substringBefore("~") - Pair( - WalletCredential( - wallet = walletId, - id = credentialId, - document = credentialWithoutDisclosures, - disclosures = disclosuresString, - addedOn = Clock.System.now() - ), createCredentialEventData(credentialJwt.payload["vc"]!!.jsonObject, typ) - ) - } + Pair( + WalletCredential( + wallet = walletId, + id = credentialId, + document = credentialWithoutDisclosures, + disclosures = disclosuresString, + addedOn = Clock.System.now() + ), createCredentialEventData(credentialJwt.payload["vc"]!!.jsonObject, typ) + ) + } - null -> throw IllegalArgumentException("WalletCredential JWT does not have \"typ\"") - else -> throw IllegalArgumentException("Invalid credential \"typ\": $typ") - } + null -> throw IllegalArgumentException("WalletCredential JWT does not have \"typ\"") + else -> throw IllegalArgumentException("Invalid credential \"typ\": $typ") + } logEvent( EventType.Credential.Accept, parsedOfferReq.credentialOffer!!.credentialIssuer, @@ -454,7 +466,17 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W /* DIDs */ override suspend fun createDid(method: String, args: Map): String { - val key = args["keyId"]?.content?.takeIf { it.isNotEmpty() }?.let { getKey(it) } ?: LocalKey.generate(KeyType.Ed25519) +// val key = args["keyId"]?.content?.takeIf { it.isNotEmpty() }?.let { getKey(it) } +// ?: getKey(generateKey(KeyType.Ed25519.name))//TODO: throws keyid unique constraint violation + val key = args["keyId"]?.content?.takeIf { it.isNotEmpty() }?.let { getKey(it) } ?: let { + LocalKey.generate(KeyType.Ed25519) + }.also { + logEvent( + EventType.Key.Create, "wallet", KeyEventData( + id = it.getKeyId(), algorithm = it.keyType.name, keyManagementService = "local" + ) + ) + } val options = getDidOptions(method, args) val result = DidService.registerByKey(method, key, options) @@ -551,11 +573,13 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W override suspend fun generateKey(type: String): String = LocalKey.generate(KeyType.valueOf(type)).let { createdKey -> - logEvent(EventType.Key.Create, "wallet", KeyEventData( - id = createdKey.getKeyId(), - algorithm = createdKey.keyType.name, - keyManagementService = "local", - )) + logEvent( + EventType.Key.Create, "wallet", KeyEventData( + id = createdKey.getKeyId(), + algorithm = createdKey.keyType.name, + keyManagementService = "local", + ) + ) insertKey(createdKey) createdKey.getKeyId() } @@ -590,11 +614,13 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W val key = keyResult.getOrThrow() val keyId = key.getKeyId() - logEvent(EventType.Key.Import, "wallet", KeyEventData( - id = keyId, - algorithm = key.keyType.name, - keyManagementService = EventDataNotAvailable - )) + logEvent( + EventType.Key.Import, "wallet", KeyEventData( + id = keyId, + algorithm = key.keyType.name, + keyManagementService = EventDataNotAvailable + ) + ) insertKey(key) @@ -652,13 +678,13 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W val pageSize = filter.limit val count = EventService.count(walletId, dataFilter) val offset = startingAfterItemIndex + 1 - val events = EventService.get(walletId, filter.limit, offset, dataFilter) + val events = EventService.get(accountId, walletId, filter.limit, offset, dataFilter) EventLogFilterDataResult( items = events, count = events.size, currentStartingAfter = computeCurrentStartingAfter(startingAfterItemIndex, pageSize), nextStartingAfter = computeNextStartingAfter(startingAfterItemIndex, pageSize, count) - ) + ) }.fold(onSuccess = { it }, onFailure = { @@ -713,6 +739,7 @@ class SSIKit2WalletService(tenant: String?, accountId: UUID, walletId: UUID) : W ) ) + //TODO: move to related entity private fun createCredentialEventData(json: JsonObject?, type: String?) = CredentialEventData( ecosystem = EventDataNotAvailable, issuerId = json?.jsonObject?.get("issuer")?.jsonObject?.get("id")?.jsonPrimitive?.content diff --git a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/account/AccountsService.kt b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/account/AccountsService.kt index 4d6e36c09..ef475eae8 100644 --- a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/account/AccountsService.kt +++ b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/account/AccountsService.kt @@ -1,13 +1,17 @@ package id.walt.webwallet.service.account +import id.walt.web.controllers.generateToken +import id.walt.webwallet.db.models.* import id.walt.webwallet.db.models.todo.AccountIssuers import id.walt.webwallet.db.models.todo.Issuers import id.walt.webwallet.service.WalletServiceManager -import id.walt.web.controllers.generateToken +import id.walt.webwallet.service.events.AccountEventData +import id.walt.webwallet.service.events.Event +import id.walt.webwallet.service.events.EventService +import id.walt.webwallet.service.events.EventType import id.walt.webwallet.web.model.AccountRequest import id.walt.webwallet.web.model.AddressAccountRequest import id.walt.webwallet.web.model.EmailAccountRequest -import id.walt.webwallet.db.models.* import kotlinx.datetime.toKotlinInstant import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonPrimitive @@ -31,6 +35,16 @@ object AccountsService { } val walletService = WalletServiceManager.getWalletService(tenant, registeredUserId, createdInitialWalletId) + EventService.add( + Event( + action = EventType.Account.Create, + tenant = tenant, + originator = "wallet", + account = registeredUserId, + wallet = createdInitialWalletId, + data = AccountEventData(accountId = request.name) + ) + ) // Add default data: val createdDid = walletService.createDid("key", mapOf("alias" to JsonPrimitive("Onboarding"))) @@ -61,6 +75,16 @@ object AccountsService { is AddressAccountRequest -> Web3WalletAccountStrategy.authenticate(tenant, request) } }.fold(onSuccess = { + EventService.add( + Event( + action = EventType.Account.Login, + tenant = tenant, + originator = "wallet", + account = it.id, + wallet = null, + data = AccountEventData(accountId = it.username) + ) + ) Result.success( AuthenticationResult( id = it.id, diff --git a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/events/Event.kt b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/events/Event.kt index 09006ecfb..b3842874c 100644 --- a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/events/Event.kt +++ b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/events/Event.kt @@ -22,11 +22,11 @@ data class Event( val tenant: String? = null, val originator: String? = null, val account: UUID, - val wallet: UUID, + val wallet: UUID? = null, val data: JsonObject, ) { constructor( - action: EventType.Action, tenant: String?, originator: String?, account: UUID, wallet: UUID, data: EventData + action: EventType.Action, tenant: String?, originator: String?, account: UUID, wallet: UUID?, data: EventData ) : this( event = action.type, action = action.toString(), @@ -44,7 +44,7 @@ data class Event( tenant = row[Events.tenant], originator = row[Events.originator], account = row[Events.account], - wallet = row[Events.wallet].value, + wallet = row[Events.wallet], data = Json.parseToJsonElement(row[Events.data]).jsonObject, ) } \ No newline at end of file diff --git a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/events/EventService.kt b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/events/EventService.kt index 9315e00dd..13ec867f9 100644 --- a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/events/EventService.kt +++ b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/service/events/EventService.kt @@ -8,13 +8,14 @@ import kotlinx.uuid.UUID import org.jetbrains.exposed.sql.Query import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction object EventService { - fun get(walletId: UUID, limit: Int, offset: Long, dataFilter: Map = emptyMap()): List = + fun get(accountId: UUID, walletId: UUID, limit: Int, offset: Long, dataFilter: Map = emptyMap()): List = addWhereClause( - Events.select { Events.wallet eq walletId }, dataFilter + Events.select { Events.account eq accountId or (Events.wallet eq walletId) }, dataFilter ).orderBy(Events.timestamp).limit(n = limit, offset = offset).map { Event(it) } diff --git a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/web/controllers/AuthController.kt b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/web/controllers/AuthController.kt index 78ce2bed1..9b460ad49 100644 --- a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/web/controllers/AuthController.kt +++ b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/web/controllers/AuthController.kt @@ -5,10 +5,6 @@ import id.walt.webwallet.db.models.AccountWalletMappings import id.walt.webwallet.db.models.AccountWalletPermissions import id.walt.webwallet.service.WalletServiceManager import id.walt.webwallet.service.account.AccountsService -import id.walt.webwallet.service.events.AccountEventData -import id.walt.webwallet.service.events.Event -import id.walt.webwallet.service.events.EventService -import id.walt.webwallet.service.events.EventType import id.walt.webwallet.utils.RandomUtils import id.walt.webwallet.web.ForbiddenException import id.walt.webwallet.web.InsufficientPermissionsException @@ -154,16 +150,6 @@ fun Application.auth() { "username" to it.username ) ) - EventService.add( - Event( - action = EventType.Account.Login, - tenant = null, - originator = null, - account = it.id, - wallet = getWalletId(), - data = AccountEventData(accountId = it.username) - ) - ) }.onFailure { call.respond(HttpStatusCode.BadRequest, it.localizedMessage) } @@ -196,16 +182,6 @@ fun Application.auth() { println("Registration succeed.") call.response.status(HttpStatusCode.Created) call.respond("Registration succeed.") - EventService.add( - Event( - action = EventType.Account.Create, - tenant = null, - originator = null, - account = it.id, - wallet = getWalletId(), - data = AccountEventData(accountId = req.name) - ) - ) }.onFailure { call.respond(HttpStatusCode.BadRequest, it.localizedMessage) } diff --git a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/web/controllers/EventLogController.kt b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/web/controllers/EventLogController.kt index 616bcfad3..f7cdc7996 100644 --- a/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/web/controllers/EventLogController.kt +++ b/waltid-web-wallet/src/main/kotlin/id/walt/webwallet/web/controllers/EventLogController.kt @@ -62,7 +62,7 @@ fun Application.eventLogs() = walletRoute { } }) { val wallet = getWalletService() - val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 0 + val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: -1 val event = call.request.queryParameters["event"] val action = call.request.queryParameters["action"] val tenant = call.request.queryParameters["tenant"] diff --git a/waltid-web-wallet/web/src/layouts/default.vue b/waltid-web-wallet/web/src/layouts/default.vue index 4ae0ed396..c59089086 100644 --- a/waltid-web-wallet/web/src/layouts/default.vue +++ b/waltid-web-wallet/web/src/layouts/default.vue @@ -300,7 +300,9 @@ const navigation = [ { name: "Tokens", href: `/wallet/${currentWallet.value}/settings/tokens`, icon: GlobeAltIcon }, { name: "DIDs", href: `/wallet/${currentWallet.value}/settings/dids`, icon: FingerPrintIcon }, { name: "Keys", href: `/wallet/${currentWallet.value}/settings/keys`, icon: KeyIcon }, + { name: "EventLog", href: `/wallet/${currentWallet.value}/eventlog`, icon: ClipboardDocumentListIcon }, // { name: "History", href: `/wallet/${currentWallet.value}/history`, icon: ClipboardDocumentListIcon }, + // {name: 'History', href: '/history', icon: ClockIcon} ], diff --git a/waltid-web-wallet/web/src/pages/wallet/[wallet]/eventlog.vue b/waltid-web-wallet/web/src/pages/wallet/[wallet]/eventlog.vue new file mode 100644 index 000000000..da8e0016a --- /dev/null +++ b/waltid-web-wallet/web/src/pages/wallet/[wallet]/eventlog.vue @@ -0,0 +1,103 @@ + + + + +