-
Notifications
You must be signed in to change notification settings - Fork 5
feat(accounts): add platform credential slots #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
veryCrunchy
wants to merge
119
commits into
feature/account-registry-foundation
from
feature/account-credential-slots
Open
Changes from all commits
Commits
Show all changes
119 commits
Select commit
Hold shift + click to select a range
93631d6
feat(accounts): add platform credential slots
veryCrunchy cf90d81
chore(changelog): link pull request
veryCrunchy 32f6c79
fix(accounts): serialize credential lifecycle state
veryCrunchy eee6c83
fix(accounts): preserve retained account recovery
veryCrunchy d62cea8
fix(android): defer retained-account uploads
veryCrunchy 5f17d3f
fix(accounts): harden credential recovery races
veryCrunchy 83ea0d9
fix(accounts): coordinate background account work
veryCrunchy 04c2a0a
fix(accounts): close credential lifecycle races
veryCrunchy 4f6fa2a
fix(accounts): close remaining removal races
veryCrunchy 1c31b71
fix(accounts): preserve credential recovery invariants
veryCrunchy cec7136
fix(accounts): retire account-bound transfer resources
veryCrunchy 1e404f6
fix(accounts): preserve active resource identity
veryCrunchy 6a1f33e
fix(android): serialize account-bound writebacks
veryCrunchy 5dcc9c0
fix(accounts): retain credential recovery paths
veryCrunchy 30941b0
fix(android): close credential recovery gaps
veryCrunchy 3965ead
fix(accounts): close remaining transition races
veryCrunchy b477088
fix(accounts): retire removed account resources
veryCrunchy 32fd78b
fix(accounts): close recovery transition gaps
veryCrunchy 1cbd3fc
fix(accounts): harden credential recovery boundaries
veryCrunchy 5040557
fix(accounts): serialize account-bound mutations
veryCrunchy de408fa
fix(accounts): preserve removal recovery state
veryCrunchy 44e8071
fix(accounts): preflight account-bound transitions
veryCrunchy 4a851d9
fix(accounts): close retained-session safety gaps
veryCrunchy 0e96293
fix(accounts): finish committed account transitions
veryCrunchy aa76381
fix(accounts): isolate credential recovery mutations
veryCrunchy 6b9ff27
fix(accounts): preflight sync ownership on removal
veryCrunchy 262127b
fix(accounts): reject unsupported desktop registries
veryCrunchy 628d2ca
fix(accounts): retain removal cleanup ownership
veryCrunchy db72523
fix(accounts): finish removal recovery
veryCrunchy d4db556
fix(accounts): harden transition recovery
veryCrunchy 9ecd00c
fix(accounts): harden damaged state recovery
veryCrunchy b54d88d
fix(accounts): complete removal isolation
veryCrunchy f721f91
test(accounts): verify durable root cleanup
veryCrunchy f198eb0
fix(accounts): harden removal recovery
veryCrunchy 8b11831
fix(accounts): fail closed on recovery tombstones
veryCrunchy 10157c7
test(accounts): split removal recovery coverage
veryCrunchy fbc8764
fix(accounts): quiesce Linux writes before removal
veryCrunchy 5e6da1b
fix(accounts): persist desktop cleanup markers first
veryCrunchy 0eb5e5d
fix(accounts): recover Android accounts before enumeration
veryCrunchy 97e9e13
fix(accounts): retry Android removal cleanup
veryCrunchy a847037
test(desktop): inject FUSE ownership in lifecycle tests
veryCrunchy 83e6b7b
fix(accounts): harden Android removal recovery
veryCrunchy d6dfd46
fix(uploads): preserve retained account queues
veryCrunchy 8c370d3
fix(accounts): journal desktop credential transitions
veryCrunchy c405223
refactor(desktop): decouple Linux cleanup lifecycle
veryCrunchy 908507d
fix(desktop): clear recovered Cloud Files failures
veryCrunchy 5b536d7
fix(accounts): preserve distinct legacy cleanup keys
veryCrunchy 48f99de
test(desktop): invoke lifecycle failures explicitly
veryCrunchy 84298c4
fix(desktop): count lazy hydration as live
veryCrunchy 260d2bd
fix(accounts): quarantine malformed removal journals
veryCrunchy 1daa249
fix(accounts): close Android removal races
veryCrunchy 2d6fcb6
fix(desktop): finish sign-out after revocation
veryCrunchy 9d9e98f
fix(sync): retire account SAF state safely
veryCrunchy dc2635c
fix(sharing): defer uploads for unreadable accounts
veryCrunchy 99ab7be
fix(desktop): quiesce Cloud Files on sign-out
veryCrunchy 1f03da9
refactor(accounts): share retained identity policy
veryCrunchy eb003fb
fix(desktop): return quiescence outcome
veryCrunchy d8bb7af
test(desktop): prove callback drain ordering
veryCrunchy 3008c67
fix(desktop): disable reads after failed FUSE unmount
veryCrunchy 9963216
fix(android): recover credential slots before selection
veryCrunchy ee428b0
fix(android): complete account retirement cleanup
veryCrunchy 7f37455
fix(desktop): harden account removal recovery
veryCrunchy e75f752
fix(accounts): preserve unreadable retained slots
veryCrunchy 2253d2e
fix(android): quiesce account file cache reads
veryCrunchy 4f13f42
fix(accounts): preserve desktop rollback selection
veryCrunchy aa4e460
fix(accounts): retry desktop removal cleanup
veryCrunchy 036511d
fix(desktop): remove accounts without loading secrets
veryCrunchy 81e33d9
fix(windows): purge removed Cloud Files roots
veryCrunchy bc5a26e
fix(android): revoke handoffs across account transitions
veryCrunchy b2b1f83
fix(accounts): retain preview cleanup recovery
veryCrunchy 9065961
test(accounts): fix preview cleanup fixtures
veryCrunchy 1b0a940
fix(accounts): clear unreadable active ownership
veryCrunchy 56d4800
fix(accounts): preserve unreadable removal rollback
veryCrunchy 396609f
fix(accounts): retain original preview retry identity
veryCrunchy d447aa4
fix(uploads): retain jobs for unreadable account registry
veryCrunchy 5221e8d
fix(desktop): recover malformed cleanup phases
veryCrunchy a37b2c4
fix(android): finish committed account selection
veryCrunchy ea9acbd
fix(accounts): purge retained mutation recovery
veryCrunchy 330ed9f
fix(accounts): persist mutation cleanup retries
veryCrunchy ba32b37
fix(account): purge private state after removal
veryCrunchy 139c220
chore(website): refresh marketing captures
obiente-automations[bot] 8858e08
fix(account): serialize private-state publication
veryCrunchy e04fd66
fix(account): purge removal residual state
veryCrunchy a2d4b09
fix(desktop): clear legacy handoffs on removal
veryCrunchy 6c240a4
test(android): make media cleanup deterministic
veryCrunchy 71c60c8
fix(account): close residual cleanup races
veryCrunchy 8c51d43
fix(desktop): reserve external handoff cache budget
veryCrunchy 3ea964b
chore(website): refresh marketing captures
obiente-automations[bot] ecaf99e
fix(desktop): recover credential removals
veryCrunchy cb6c6e6
fix(desktop): validate handoff paths portably
veryCrunchy bcd1836
fix(accounts): retire deck draft state
veryCrunchy bf4f21f
fix(desktop): delete read-only handoff copies
veryCrunchy 5598786
fix(accounts): harden retained account recovery
veryCrunchy 8e55759
fix(accounts): gate credential recovery transitions
veryCrunchy c167e28
chore(architecture): lower Android service baseline
veryCrunchy f501e08
fix(android): serialize conditional file creation
veryCrunchy bc317fa
test(android): cover offline account recovery policy
veryCrunchy 449489f
refactor(desktop): preserve compact session boundary
veryCrunchy f3a26a1
chore(website): refresh marketing captures
obiente-automations[bot] 6383844
fix(accounts): purge removed private state
veryCrunchy ca9b1d9
chore(website): refresh marketing captures
obiente-automations[bot] c4b9068
fix(accounts): purge private state on removal
veryCrunchy c54a0ad
chore(website): refresh marketing captures
obiente-automations[bot] 7055dd5
fix(accounts): fence private cache retirement
veryCrunchy f2c5359
chore(website): refresh marketing captures
obiente-automations[bot] 2f09ad2
fix(accounts): finish private state cleanup
veryCrunchy 32f97e9
fix(desktop): clear removed account state
veryCrunchy fd10112
fix(ui): validate workspace cleanup identity
veryCrunchy a626ade
chore(website): refresh marketing captures
obiente-automations[bot] f3c0647
fix(accounts): close remaining retirement races
veryCrunchy 412f36b
fix(accounts): close desktop cleanup gaps
veryCrunchy 5d63343
fix(accounts): quiesce android private activity
veryCrunchy 31c7c7e
chore(website): refresh marketing captures
obiente-automations[bot] eadef3d
fix(android): reuse account lease for provider mutation lookups
veryCrunchy fc550cf
fix(accounts): fence complete dynamic read lifetimes
veryCrunchy ef0f432
fix(accounts): persist credential rollback completion
veryCrunchy 6fe80d0
fix(deck): retire legacy drafts before replacement
veryCrunchy a64f83b
fix(deck): retain legacy submission tombstones during cleanup
veryCrunchy d3a3482
chore(website): refresh marketing captures
obiente-automations[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
800 changes: 800 additions & 0 deletions
800
androidApp/src/main/kotlin/dev/obiente/nextcloudnative/AndroidAccountCredentialController.kt
Large diffs are not rendered by default.
Oops, something went wrong.
369 changes: 369 additions & 0 deletions
369
androidApp/src/main/kotlin/dev/obiente/nextcloudnative/AndroidAccountCredentialRecovery.kt
Large diffs are not rendered by default.
Oops, something went wrong.
193 changes: 193 additions & 0 deletions
193
...oidApp/src/main/kotlin/dev/obiente/nextcloudnative/AndroidAccountCredentialTransitions.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| package dev.obiente.nextcloudnative | ||
|
|
||
| import dev.obiente.nextcloudnative.app.NextcloudSession | ||
| import kotlinx.coroutines.CancellationException | ||
| import kotlinx.coroutines.NonCancellable | ||
| import kotlinx.coroutines.withContext | ||
|
|
||
| internal fun removeActiveAndroidAccountCredentialState( | ||
| state: AndroidAccountCredentialState, | ||
| ): AndroidAccountCredentialState = state.registry.activeAccountId?.let(state::remove) ?: state | ||
|
|
||
| internal suspend fun replaceAndroidActiveStateWithAccountLeases( | ||
| replacement: AndroidAccountCredentialState, | ||
| previousSession: NextcloudSession?, | ||
| replacedSession: NextcloudSession?, | ||
| suspectEncrypted: String?, | ||
| guard: AndroidAccountOperationGuard = ANDROID_ACCOUNT_OPERATION_GUARD, | ||
| coordinator: AndroidFileRangeSessionCoordinator = ANDROID_FILE_RANGE_SESSION_COORDINATOR, | ||
| replace: suspend (AndroidAccountCredentialState, NextcloudSession?, String?, NextcloudSession?) -> Unit, | ||
| ) { | ||
| val replacementSession = requireNotNull(replacement.activeSession) | ||
| val accountIdentities = listOfNotNull(previousSession, replacementSession, replacedSession) | ||
| .map(NextcloudDocumentIds::accountKey) | ||
| guard.withAccounts(accountIdentities) { | ||
| quiesceAndroidFileRangesBeforeCredentialReplacement(replacedSession, replacementSession, coordinator) | ||
| replace(replacement, previousSession, suspectEncrypted, replacedSession) | ||
| } | ||
| } | ||
|
|
||
| internal suspend fun rollbackUnavailableAndroidAccountRemoval( | ||
| active: Boolean = false, | ||
| recovered: AndroidAccountCredentialState, | ||
| persistRecovered: suspend (AndroidAccountCredentialState) -> Unit, | ||
| clearCleanup: suspend () -> Unit, | ||
| ) { | ||
| if (!active) persistRecovered(recovered) | ||
| clearCleanup() | ||
| } | ||
|
|
||
| internal suspend fun retryAndroidAccountRemovalCleanup( | ||
| accountOwnedByRegistry: Boolean?, | ||
| removeAccountOwnedWork: suspend () -> Unit, | ||
| clearCleanup: suspend () -> Unit, | ||
| ) { | ||
| when (accountOwnedByRegistry) { | ||
| true -> clearCleanup() | ||
| false -> { | ||
| removeAccountOwnedWork() | ||
| clearCleanup() | ||
| } | ||
| null -> error("Account ownership is unavailable; pending cleanup cannot run safely.") | ||
| } | ||
| } | ||
|
|
||
| internal fun androidAccountRemovalCleanupRetryFailure(failure: Exception) = IllegalStateException( | ||
| "Previous account cleanup must finish before this account can be added again.", | ||
| failure, | ||
| ) | ||
|
|
||
| internal suspend fun retryAndroidAccountOwnedStateCleanup( | ||
| session: NextcloudSession, | ||
| pending: AndroidPendingAccountRemovalCleanup, | ||
| retry: suspend (NextcloudSession, String, String?, String?, String?) -> Unit, | ||
| ) { | ||
| retry( | ||
| session, | ||
| pending.workIdentity, | ||
| pending.previewCacheIdentity, | ||
| pending.durableMutationIdentity, | ||
| pending.legacyAccountScopeDigest, | ||
| ) | ||
| } | ||
|
|
||
| internal suspend fun resumeAndroidQueuedUploadsAfterSelection( | ||
| resume: suspend () -> Unit, | ||
| notifyDocumentRootsChanged: () -> Unit, | ||
| recordFailure: () -> Unit, | ||
| ) { | ||
| try { | ||
| resume() | ||
| } catch (cancelled: CancellationException) { | ||
| throw cancelled | ||
| } catch (_: Exception) { | ||
| recordFailure() | ||
| } finally { | ||
| notifyDocumentRootsChanged() | ||
| } | ||
| } | ||
|
|
||
| internal suspend fun removeAndroidAccountCredentialData( | ||
| active: Boolean, | ||
| prepareAccountRemoval: suspend () -> Unit = {}, | ||
| removeQueuedUploads: suspend () -> Unit, | ||
| clearActiveAccount: suspend () -> Unit, | ||
| rollbackActiveRemoval: suspend () -> Unit, | ||
| persistInactiveRemoval: suspend () -> Unit, | ||
| rollbackInactiveRemoval: suspend () -> Unit, | ||
| completeCommittedCleanup: suspend () -> Unit = {}, | ||
| recordCommittedCleanupFailure: (Exception) -> Unit = {}, | ||
| ) { | ||
| prepareAccountRemoval() | ||
| if (active) { | ||
| try { | ||
| clearActiveAccount() | ||
| } catch (failure: Exception) { | ||
| withContext(NonCancellable) { | ||
| runCatching { rollbackActiveRemoval() } | ||
| .onFailure(failure::addSuppressed) | ||
| } | ||
| throw failure | ||
| } | ||
| finishCommittedAndroidAccountRemovalCleanup( | ||
| removeQueuedUploads, | ||
| completeCommittedCleanup, | ||
| recordCommittedCleanupFailure, | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| try { | ||
| persistInactiveRemoval() | ||
| } catch (failure: Exception) { | ||
| withContext(NonCancellable) { | ||
| runCatching { rollbackInactiveRemoval() } | ||
| .onFailure(failure::addSuppressed) | ||
| } | ||
| throw failure | ||
| } | ||
| finishCommittedAndroidAccountRemovalCleanup( | ||
| removeQueuedUploads, | ||
| completeCommittedCleanup, | ||
| recordCommittedCleanupFailure, | ||
| ) | ||
| } | ||
|
|
||
| internal suspend fun removeUnavailableAndroidAccountCredentialData( | ||
| accountIdentity: String, | ||
| active: Boolean = false, | ||
| prepareAccountRemoval: suspend () -> Unit, | ||
| removeAccountOwnedWorkWithoutCredentials: suspend (String) -> Unit, | ||
| persistRemoval: suspend () -> Unit, | ||
| clearActiveAccount: suspend () -> Unit = persistRemoval, | ||
| rollbackRemoval: suspend () -> Unit, | ||
| completeCommittedCleanup: suspend () -> Unit = {}, | ||
| recordCommittedCleanupFailure: (Exception) -> Unit = {}, | ||
| ) { | ||
| require(accountIdentity.isNotBlank()) | ||
| removeAndroidAccountCredentialData( | ||
| active = active, | ||
| prepareAccountRemoval = prepareAccountRemoval, | ||
| removeQueuedUploads = { removeAccountOwnedWorkWithoutCredentials(accountIdentity) }, | ||
| clearActiveAccount = clearActiveAccount, | ||
| rollbackActiveRemoval = rollbackRemoval, | ||
| persistInactiveRemoval = persistRemoval, | ||
| rollbackInactiveRemoval = rollbackRemoval, | ||
| completeCommittedCleanup = completeCommittedCleanup, | ||
| recordCommittedCleanupFailure = recordCommittedCleanupFailure, | ||
| ) | ||
| } | ||
|
|
||
| private suspend fun finishCommittedAndroidAccountRemovalCleanup( | ||
| removeQueuedUploads: suspend () -> Unit, | ||
| completeCommittedCleanup: suspend () -> Unit, | ||
| recordFailure: (Exception) -> Unit, | ||
| ) { | ||
| try { | ||
| removeQueuedUploads() | ||
| completeCommittedCleanup() | ||
| } catch (cancelled: CancellationException) { | ||
| throw cancelled | ||
| } catch (failure: Exception) { | ||
| recordFailure(failure) | ||
| } | ||
| } | ||
|
|
||
| internal suspend fun removeRecoveredAndroidAccountCredentialData( | ||
| prepareAccountRemoval: suspend () -> Unit = {}, | ||
| removeQueuedUploads: suspend () -> Unit, | ||
| clearRecoveredAccount: suspend () -> Unit, | ||
| rollbackRecoveredAccount: suspend () -> Unit, | ||
| completeCommittedCleanup: suspend () -> Unit = {}, | ||
| recordCommittedCleanupFailure: (Exception) -> Unit = {}, | ||
| ) = removeAndroidAccountCredentialData( | ||
| active = true, | ||
| prepareAccountRemoval = prepareAccountRemoval, | ||
| removeQueuedUploads = removeQueuedUploads, | ||
| clearActiveAccount = clearRecoveredAccount, | ||
| rollbackActiveRemoval = rollbackRecoveredAccount, | ||
| persistInactiveRemoval = {}, | ||
| rollbackInactiveRemoval = {}, | ||
| completeCommittedCleanup = completeCommittedCleanup, | ||
| recordCommittedCleanupFailure = recordCommittedCleanupFailure, | ||
| ) | ||
57 changes: 57 additions & 0 deletions
57
androidApp/src/main/kotlin/dev/obiente/nextcloudnative/AndroidAccountFileListing.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package dev.obiente.nextcloudnative | ||
|
|
||
| import dev.obiente.nextcloudnative.app.NextcloudFile | ||
| import dev.obiente.nextcloudnative.app.NextcloudFileListing | ||
| import dev.obiente.nextcloudnative.app.NextcloudFileListingHttpException | ||
| import dev.obiente.nextcloudnative.app.NextcloudFileListingSource | ||
| import dev.obiente.nextcloudnative.app.NextcloudSession | ||
| import java.io.IOException | ||
|
|
||
| internal data class AndroidDavFileListingResponse(val status: Int, val files: List<NextcloudFile>) | ||
|
|
||
| /** The caller may reuse a lease only while its enclosing account operation still owns it. */ | ||
| internal suspend fun loadAndroidAccountFileListing( | ||
| session: NextcloudSession, | ||
| resolveSession: suspend () -> NextcloudSession?, | ||
| cache: AndroidFileReadCache, | ||
| path: String, | ||
| accountLeaseHeld: Boolean = false, | ||
| guard: AndroidAccountOperationGuard = ANDROID_ACCOUNT_OPERATION_GUARD, | ||
| request: suspend () -> AndroidDavFileListingResponse, | ||
| ): NextcloudFileListing { | ||
| val read: suspend () -> NextcloudFileListing = { | ||
| readAndroidAccountFileListing(cache, NextcloudDocumentIds.accountKey(session), path, request) | ||
| } | ||
| return if (accountLeaseHeld) read() else withRetainedAndroidAccountFileRead(session, resolveSession, guard, read) | ||
| } | ||
|
|
||
| private suspend fun readAndroidAccountFileListing( | ||
| cache: AndroidFileReadCache, | ||
| accountId: String, | ||
| path: String, | ||
| request: suspend () -> AndroidDavFileListingResponse, | ||
| ): NextcloudFileListing = try { | ||
| val response = request() | ||
| if (response.status == 207) { | ||
| val files = response.files.drop(1) | ||
| .sortedWith(compareByDescending<NextcloudFile> { it.isDirectory }.thenBy { it.name.lowercase() }) | ||
| runCatching { cache.storeListing(accountId, path, files) } | ||
| NextcloudFileListing(files, NextcloudFileListingSource.Network) | ||
| } else { | ||
| val cached = if (response.status >= 500) cache.cachedListing(accountId, path) else null | ||
| cached?.let { NextcloudFileListing(it.files, NextcloudFileListingSource.Cache) } | ||
| ?: throw NextcloudFileListingHttpException(response.status) | ||
| } | ||
| } catch (failure: IOException) { | ||
| cache.cachedListing(accountId, path)?.files | ||
| ?.let { NextcloudFileListing(it, NextcloudFileListingSource.Cache) } | ||
| ?: throw failure | ||
| } | ||
|
|
||
| internal fun requireAndroidDocumentDirectory( | ||
| reference: NextcloudDocumentReference, | ||
| findDocument: (String) -> NextcloudFile, | ||
| ) { | ||
| if (reference.isRoot) return | ||
| require(findDocument(reference.path).isDirectory) { "The selected parent is not a folder." } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.