Skip to content

Commit 85be404

Browse files
authored
Merge pull request #322 from niscy-eudiw/main
Attestation Revocation Implementation
2 parents 991f066 + fd02bd7 commit 85be404

File tree

39 files changed

+885
-69
lines changed

39 files changed

+885
-69
lines changed

assembly-logic/src/main/java/eu/europa/ec/assemblylogic/Application.kt

+19
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
package eu.europa.ec.assemblylogic
1818

1919
import android.app.Application
20+
import androidx.work.ExistingPeriodicWorkPolicy
21+
import androidx.work.PeriodicWorkRequest
22+
import androidx.work.WorkManager
2023
import eu.europa.ec.analyticslogic.controller.AnalyticsController
2124
import eu.europa.ec.assemblylogic.di.setupKoin
2225
import eu.europa.ec.businesslogic.config.ConfigLogic
26+
import eu.europa.ec.corelogic.worker.RevocationWorkManager
2327
import eu.europa.ec.eudi.rqesui.infrastructure.EudiRQESUi
2428
import org.koin.android.ext.android.inject
2529
import org.koin.core.KoinApplication
@@ -33,6 +37,7 @@ class Application : Application() {
3337
super.onCreate()
3438
initializeKoin().initializeRqes()
3539
initializeReporting()
40+
initializeRevocationWorkManager()
3641
}
3742

3843
private fun KoinApplication.initializeRqes() {
@@ -50,4 +55,18 @@ class Application : Application() {
5055
private fun initializeReporting() {
5156
analyticsController.initialize(this)
5257
}
58+
59+
private fun initializeRevocationWorkManager() {
60+
61+
val periodicWorkRequest = PeriodicWorkRequest.Builder(
62+
workerClass = RevocationWorkManager::class.java,
63+
repeatInterval = configLogic.revocationInterval,
64+
).build()
65+
66+
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
67+
RevocationWorkManager.REVOCATION_WORK_NAME,
68+
ExistingPeriodicWorkPolicy.KEEP,
69+
periodicWorkRequest
70+
)
71+
}
5372
}

build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt

+2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
7474
apply("org.jetbrains.kotlin.android")
7575
apply("kotlinx-serialization")
7676
apply("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
77+
apply("kotlin-parcelize")
7778
}
7879

7980
extensions.configure<LibraryExtension> {
@@ -146,6 +147,7 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
146147
add("implementation", libs.findLibrary("kotlinx-coroutines-android").get())
147148
add("implementation", libs.findLibrary("kotlinx-coroutines-guava").get())
148149
add("implementation", libs.findLibrary("kotlinx.serialization.json").get())
150+
add("implementation", libs.findLibrary("androidx-work-ktx").get())
149151
}
150152
afterEvaluate {
151153
if (!config.module.isLogicModule && !config.module.isFeatureCommon) {

business-logic/src/main/java/eu/europa/ec/businesslogic/config/ConfigLogic.kt

+10
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package eu.europa.ec.businesslogic.config
1818

1919
import eu.europa.ec.businesslogic.BuildConfig
2020
import eu.europa.ec.eudi.rqesui.infrastructure.config.EudiRQESUiConfig
21+
import java.time.Duration
2122

2223
interface ConfigLogic {
2324

@@ -58,6 +59,15 @@ interface ConfigLogic {
5859
* changelog is maintained for development builds.
5960
*/
6061
val changelogUrl: String?
62+
63+
64+
/**
65+
* The interval at which revocations are checked.
66+
*
67+
* This property defines the time interval between checks for revoked tokens or credentials.
68+
* It is currently set to 15 minutes.
69+
*/
70+
val revocationInterval: Duration get() = Duration.ofMinutes(15)
6171
}
6272

6373
enum class AppFlavor {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2023 European Commission
3+
*
4+
* Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European
5+
* Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work
6+
* except in compliance with the Licence.
7+
*
8+
* You may obtain a copy of the Licence at:
9+
* https://joinup.ec.europa.eu/software/page/eupl
10+
*
11+
* Unless required by applicable law or agreed to in writing, software distributed under
12+
* the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF
13+
* ANY KIND, either express or implied. See the Licence for the specific language
14+
* governing permissions and limitations under the Licence.
15+
*/
16+
17+
package eu.europa.ec.businesslogic.extension
18+
19+
import android.content.Intent
20+
import android.os.Build
21+
import android.os.Parcelable
22+
import androidx.annotation.CheckResult
23+
24+
@CheckResult
25+
inline fun <reified T : Parcelable> Intent?.getParcelableArrayListExtra(
26+
action: String
27+
): List<T>? {
28+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
29+
this?.getParcelableArrayListExtra(
30+
action,
31+
T::class.java
32+
)
33+
} else {
34+
@Suppress("DEPRECATION")
35+
this?.getParcelableArrayListExtra(action)
36+
}?.filterNotNull()
37+
}

common-feature/src/main/java/eu/europa/ec/commonfeature/model/DocumentDetailsUi.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import eu.europa.ec.eudi.wallet.document.DocumentId
2121
import eu.europa.ec.uilogic.component.wrap.ExpandableListItem
2222

2323
enum class DocumentUiIssuanceState {
24-
Issued, Pending, Failed, Expired
24+
Issued, Pending, Failed, Expired, Revoked
2525
}
2626

2727
data class DocumentDetailsUi(

common-feature/src/main/java/eu/europa/ec/commonfeature/ui/request/RequestViewModel.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ sealed class Event : ViewEvent {
6363
sealed class Effect : ViewSideEffect {
6464
sealed class Navigation : Effect() {
6565
data class SwitchScreen(
66-
val screenRoute: String
66+
val screenRoute: String,
6767
) : Navigation()
6868

6969
data object Pop : Navigation()
7070
data class PopTo(
71-
val screenRoute: String
71+
val screenRoute: String,
7272
) : Navigation()
7373
}
7474

@@ -99,7 +99,7 @@ abstract class RequestViewModel : MviViewModel<Event, State, Effect>() {
9999

100100
open fun updateData(
101101
updatedItems: List<RequestDocumentItemUi>,
102-
allowShare: Boolean? = null
102+
allowShare: Boolean? = null,
103103
) {
104104
val hasAtLeastOneFieldSelected = hasAtLeastOneFieldSelected(
105105
requestDocuments = updatedItems
@@ -265,7 +265,7 @@ abstract class RequestViewModel : MviViewModel<Event, State, Effect>() {
265265
}
266266

267267
private fun hasAtLeastOneFieldSelected(
268-
requestDocuments: List<RequestDocumentItemUi>
268+
requestDocuments: List<RequestDocumentItemUi>,
269269
): Boolean {
270270
val hasAtLeastOneFieldSelected: Boolean = requestDocuments.any { requestDocument ->
271271
requestDocument.headerUi.nestedItems.hasAnySingleSelected()

common-feature/src/main/java/eu/europa/ec/commonfeature/ui/request/transformer/RequestTransformer.kt

+13-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import eu.europa.ec.commonfeature.ui.request.model.DomainDocumentFormat
2222
import eu.europa.ec.commonfeature.ui.request.model.RequestDocumentItemUi
2323
import eu.europa.ec.commonfeature.util.docNamespace
2424
import eu.europa.ec.commonfeature.util.transformPathsToDomainClaims
25+
import eu.europa.ec.corelogic.extension.toClaimPaths
2526
import eu.europa.ec.corelogic.model.ClaimPath
2627
import eu.europa.ec.eudi.iso18013.transfer.response.DisclosedDocument
2728
import eu.europa.ec.eudi.iso18013.transfer.response.DisclosedDocuments
@@ -48,16 +49,27 @@ object RequestTransformer {
4849
val resultList = mutableListOf<DocumentPayloadDomain>()
4950

5051
requestDocuments.forEach { requestDocument ->
52+
5153
val storageDocument =
5254
storageDocuments.first { it.id == requestDocument.documentId }
5355

56+
val claimsPaths = storageDocument.data.claims.flatMap { claim ->
57+
claim.toClaimPaths()
58+
}
59+
5460
val requestedItemsPaths = requestDocument.requestedItems.keys
5561
.map {
5662
it.toClaimPath()
5763
}
5864

65+
val filteredPaths = claimsPaths.filter { available ->
66+
requestedItemsPaths.any { requested ->
67+
available.joined.startsWith(requested.joined)
68+
}
69+
}
70+
5971
val domainClaims = transformPathsToDomainClaims(
60-
paths = requestedItemsPaths,
72+
paths = filteredPaths,
6173
claims = storageDocument.data.claims,
6274
metadata = storageDocument.metadata,
6375
resourceProvider = resourceProvider,

common-feature/src/main/java/eu/europa/ec/commonfeature/util/DocumentHelper.kt

+60-17
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ fun getReadableNameFromIdentifier(
105105
)
106106
}
107107

108-
@OptIn(ExperimentalUuidApi::class)
109108
fun createKeyValue(
110109
item: Any,
111110
groupKey: String,
@@ -115,24 +114,75 @@ fun createKeyValue(
115114
metadata: DocumentMetaData?,
116115
allItems: MutableList<DomainClaim>,
117116
) {
117+
118+
@OptIn(ExperimentalUuidApi::class)
119+
fun addFlatOrGroupedChildren(
120+
allItems: MutableList<DomainClaim>,
121+
children: List<DomainClaim>,
122+
groupKey: String,
123+
metadata: DocumentMetaData?,
124+
locale: Locale,
125+
predicate: () -> Boolean
126+
) {
127+
128+
val groupIsAlreadyPresent = children
129+
.filterIsInstance<DomainClaim.Group>()
130+
.any { it.key == groupKey }
131+
132+
if (predicate() && !groupIsAlreadyPresent) {
133+
allItems.add(
134+
DomainClaim.Group(
135+
key = groupKey,
136+
displayTitle = getReadableNameFromIdentifier(
137+
metadata = metadata,
138+
userLocale = locale,
139+
identifier = groupKey
140+
),
141+
path = ClaimPath(listOf(Uuid.random().toString())),
142+
items = children
143+
)
144+
)
145+
} else {
146+
allItems.addAll(children)
147+
}
148+
}
149+
118150
when (item) {
119151

120152
is Map<*, *> -> {
153+
154+
val children: MutableList<DomainClaim> = mutableListOf()
155+
val childKeys: MutableList<String> = mutableListOf()
156+
121157
item.forEach { (key, value) ->
122158
safeLet(key as? String, value) { key, value ->
159+
123160
val newGroupKey = if (value is Collection<*>) key else groupKey
124161
val newChildKey = if (value is Collection<*>) "" else key
162+
163+
childKeys.add(newChildKey)
164+
125165
createKeyValue(
126166
item = value,
127167
groupKey = newGroupKey,
128168
childKey = newChildKey,
129169
disclosurePath = disclosurePath,
130170
resourceProvider = resourceProvider,
131171
metadata = metadata,
132-
allItems = allItems
172+
allItems = children
133173
)
134174
}
135175
}
176+
177+
addFlatOrGroupedChildren(
178+
allItems = allItems,
179+
children = children,
180+
groupKey = groupKey,
181+
metadata = metadata,
182+
locale = resourceProvider.getLocale()
183+
) {
184+
!childKeys.any { it.isEmpty() }
185+
}
136186
}
137187

138188
is Collection<*> -> {
@@ -152,21 +202,14 @@ fun createKeyValue(
152202
}
153203
}
154204

155-
if (childKey.isEmpty()) {
156-
allItems.add(
157-
DomainClaim.Group(
158-
key = groupKey,
159-
displayTitle = getReadableNameFromIdentifier(
160-
metadata = metadata,
161-
userLocale = resourceProvider.getLocale(),
162-
identifier = groupKey
163-
),
164-
path = ClaimPath(listOf(Uuid.random().toString())),
165-
items = children
166-
)
167-
)
168-
} else {
169-
allItems.addAll(children)
205+
addFlatOrGroupedChildren(
206+
allItems = allItems,
207+
children = children,
208+
groupKey = groupKey,
209+
metadata = metadata,
210+
locale = resourceProvider.getLocale()
211+
) {
212+
childKey.isEmpty()
170213
}
171214
}
172215

core-logic/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ moduleConfig {
3434
dependencies {
3535
implementation(project(LibraryModule.ResourcesLogic.path))
3636
implementation(project(LibraryModule.BusinessLogic.path))
37+
implementation(project(LibraryModule.StorageLogic.path))
3738
implementation(project(LibraryModule.AuthenticationLogic.path))
3839

3940
implementation(libs.androidx.biometric)

core-logic/src/main/java/eu/europa/ec/corelogic/controller/WalletCoreDocumentsController.kt

+24-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import eu.europa.ec.corelogic.model.FormatType
2929
import eu.europa.ec.corelogic.model.ScopedDocument
3030
import eu.europa.ec.corelogic.model.toDocumentIdentifier
3131
import eu.europa.ec.eudi.openid4vci.MsoMdocCredential
32+
import eu.europa.ec.eudi.statium.Status
3233
import eu.europa.ec.eudi.wallet.EudiWallet
3334
import eu.europa.ec.eudi.wallet.document.DeferredDocument
3435
import eu.europa.ec.eudi.wallet.document.Document
@@ -45,6 +46,7 @@ import eu.europa.ec.eudi.wallet.issue.openid4vci.OfferResult
4546
import eu.europa.ec.eudi.wallet.issue.openid4vci.OpenId4VciManager
4647
import eu.europa.ec.resourceslogic.R
4748
import eu.europa.ec.resourceslogic.provider.ResourceProvider
49+
import eu.europa.ec.storagelogic.controller.RevokedDocumentsStorageController
4850
import kotlinx.coroutines.CoroutineDispatcher
4951
import kotlinx.coroutines.Dispatchers
5052
import kotlinx.coroutines.channels.ProducerScope
@@ -171,12 +173,19 @@ interface WalletCoreDocumentsController {
171173
suspend fun getScopedDocuments(locale: Locale): FetchScopedDocumentsPartialState
172174

173175
fun getAllDocumentCategories(): DocumentCategories
176+
177+
suspend fun getRevokedDocumentIds(): List<String>
178+
179+
suspend fun isDocumentRevoked(id: String): Boolean
180+
181+
suspend fun resolveDocumentStatus(document: IssuedDocument): Result<Status>
174182
}
175183

176184
class WalletCoreDocumentsControllerImpl(
177185
private val resourceProvider: ResourceProvider,
178186
private val eudiWallet: EudiWallet,
179187
private val walletCoreConfig: WalletCoreConfig,
188+
private val revokedDocumentsStorageController: RevokedDocumentsStorageController,
180189
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
181190
) : WalletCoreDocumentsController {
182191

@@ -329,7 +338,10 @@ class WalletCoreDocumentsControllerImpl(
329338
override fun deleteDocument(documentId: String): Flow<DeleteDocumentPartialState> = flow {
330339
eudiWallet.deleteDocumentById(documentId = documentId)
331340
.kotlinResult
332-
.onSuccess { emit(DeleteDocumentPartialState.Success) }
341+
.onSuccess {
342+
revokedDocumentsStorageController.delete(documentId)
343+
emit(DeleteDocumentPartialState.Success)
344+
}
333345
.onFailure {
334346
emit(
335347
DeleteDocumentPartialState.Failure(
@@ -346,10 +358,12 @@ class WalletCoreDocumentsControllerImpl(
346358

347359
override fun deleteAllDocuments(mainPidDocumentId: String): Flow<DeleteAllDocumentsPartialState> =
348360
flow {
361+
349362
val allDocuments = getAllDocuments()
350363
val mainPidDocument = getMainPidDocument()
351364

352365
mainPidDocument?.let {
366+
353367
val restOfDocuments = allDocuments.minusElement(it)
354368

355369
var restOfAllDocsDeleted = true
@@ -510,6 +524,15 @@ class WalletCoreDocumentsControllerImpl(
510524
return walletCoreConfig.documentCategories
511525
}
512526

527+
override suspend fun getRevokedDocumentIds(): List<String> =
528+
revokedDocumentsStorageController.retrieveAll().map { it.identifier }
529+
530+
override suspend fun isDocumentRevoked(id: String): Boolean =
531+
revokedDocumentsStorageController.retrieve(id) != null
532+
533+
override suspend fun resolveDocumentStatus(document: IssuedDocument): Result<Status> =
534+
eudiWallet.resolveStatus(document)
535+
513536
private fun issueDocumentWithOpenId4VCI(configId: String): Flow<IssueDocumentsPartialState> =
514537
callbackFlow {
515538

0 commit comments

Comments
 (0)