feat(org-support): organisation and collection support#41
Open
feat(org-support): organisation and collection support#41
Conversation
Adds full OpenSpec change for Bitwarden Organizations & Collections support — RSA org key crypto, collection CRUD, and full item CRUD in org context across two implementation phases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- §III: strengthen zeroing language for RSA private key and org keys - §IV: add missing KATs for org cipher decrypt and collection name encrypt - §VII: add tasks for inline crypto comments and SECURITY.md update - fix: PKCS#8 stripping called out explicitly in task 3.4 - fix: OrgKeyCache actor/sync CipherMapper impedance mismatch resolved (pass snapshot not actor ref to keep map() synchronous) - fix: resolve stale design open question (badge: yes, decided in spec) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Blocking: - §IV: reorder tasks for TDD (Red-first groups 2.0, 3.0, 4.0 before impl) - wire model: add RawCipher.collectionIds (was missing, collectionIds always empty) - add toRawCipher org key source: task 3.9/3.10 — caller must pass org key from OrgKeyCache; without this org saves encrypt with personal vault key Consistency: - canManageCollections for .custom role: explicit false + reason - add server error scenarios to all collection CRUD requirements (§V) - harmonize + button model: always-visible org header button (not selection- dependent), matching Folders section pattern; fix both org-collections and vault-folder-organization specs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds full Bitwarden org vault support: RSA-OAEP-SHA1 org key unwrapping, OrgKeyCache actor with zeroing on lock, collection CRUD (create/rename/delete) encrypted with org key, sidebar Organizations section with role-gated management, org membership badge on item rows, collection picker in item edit sheet, and context-aware item create pre-populated from collection selection. Includes KATs for RSA unwrap and org cipher decryption, SyncResponse decoding tests, collection filtering/routing tests, and SECURITY.md updated with org key lifecycle documentation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rt branch - Fix smart-quote syntax error in SidebarView delete confirmation text - Fix AppContainer init order (OrgKeyCache declared before VaultRepositoryImpl) - Add orgKeyCache to RootViewModelDependencies protocol and MockRootDependencies - Add custom RawProfile decoder to handle both PascalCase (Bitwarden) and camelCase (Vaultwarden) field names - Add custom RawCipher decoder so collectionIds defaults to [] when absent - Fix test fixtures: use lowercase cipher id, correct DraftVaultItem call sites - Unlock mock crypto in testSync_populatesOrganizationsAndCollections - Fix XCTUnwrap usage for failable CryptoKeys init in OrgKeyCryptoKATTests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… green - Add gen-rsa-kat.sh script to generate RSA-2048 OAEP-SHA1 known-answer test fixtures for OrgKeyCryptoKATTests (task 3.5) - Run the script to replace the placeholder testRSAPrivateKeyPKCS8Base64 and testEncOrgKey values with a real offline-generated key pair - testUnwrapOrgKey_knownVector() now passes; full suite is green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All tests are green; tasks were verified during build-fix session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… org ciphers Two gaps prevented organisations and collections from appearing in the sidebar: 1. RawOrganization and RawCollection used synthesised Codable, which only matched camelCase JSON. Bitwarden and some Vaultwarden versions emit PascalCase for these objects (e.g. "Id", "Name", "Key", "Type"). Adding custom init(from:) with FlexOrgKeys / FlexCollKeys mirrors the existing pattern used by RawProfile and SyncResponse, handling both casings. 2. decryptList always skipped org ciphers (organizationId != nil) because org keys weren't yet available. After the org key unwrap block now runs, a second pass over syncResponse.ciphers decrypts org ciphers using the unwrapped orgKeysSnapshot and appends them to the items list. Per-item keys from org ciphers are merged into cipherKeyMap and the VaultKeyCache is refreshed so attachment downloads work correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… flow state - Add `UnlockFlowState.enrollmentPrompt(reason:)` case; mark `EnrollmentReason: Equatable` - Remove `showEnrollmentPrompt` and `enrollmentReason` published properties from UnlockViewModel - `checkEnrollmentOrSync()` now sets `flowState = .enrollmentPrompt(reason:)` instead of toggling a sheet bool - `confirmEnrollBiometric()` and `dismissEnrollmentPrompt()` set `flowState = .loading` before sync - `UnlockView` renders enrollment UI inline inside the `flowState` switch; sheet modifier removed - `PrizmApp.handleUnlockFlow` handles `.enrollmentPrompt` (stays on unlock screen) - Delete `BiometricEnrollmentPromptView.swift` and remove from project.pbxproj - Update `UnlockViewModelBiometricTests` to watch `$flowState` instead of removed properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… key Bitwarden and Vaultwarden nest org memberships inside the Profile object (`Profile.Organizations`), not as a separate top-level `organizations` key. The decoder was silently falling through both try? attempts and defaulting to [], so syncResponse.organizations was always empty and the org block in SyncRepositoryImpl never ran — causing 0 orgs and 0 collections in the vault. Fix: decode organizations inside RawProfile and expose them via SyncResponse.organizations sourced from profile.organizations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…utton Use tray.fill.badge.plus at .title3 (matching folder.badge.plus) instead of a plain plus at .caption size. Also align trailing padding to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…llable macOS Tahoe draws an automatic separator below the window toolbar when any NavigationSplitView column contains scrollable content. Adding organizations to the sidebar made it tall enough to scroll, triggering the line. Adding .toolbarBackground(.hidden, for: .windowToolbar) suppresses it, matching the look on main. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Collections now parse `/`-delimited names into a tree (like folders), rendered via a new recursive `CollectionTreeRow` and `CollectionTreeNode` entity. Also fixes the new-collection icon to match new-folder style. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move .tag to DisclosureGroup so List registers clicks on the org row - Broaden org filter to include items with organizationId set directly, not just items assigned to one of the org's collections Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add explicit .foregroundStyle(.secondary) so the icon doesn't inherit the list row's foreground and disappear against the background. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix OrgRole raw values: Bitwarden API uses 2=User, 3=Manager (not the reversed order the enum had). Regular users were incorrectly getting manager permissions and vice-versa. - Replace tray.fill.badge.plus (unreliable) with plus.circle for the new-collection button so it renders consistently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the folder.badge.plus pattern used by the new-folder button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap each Picker in an HStack + Spacer so the menu button anchors to the left edge instead of stretching full-width. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
W1 — Correct OrgRole mapping in spec (2=User, 3=Manager to match Bitwarden API and code) W2 — Exclude org items from folder selections in VaultRepositoryImpl W3 — Formally descope org picker for new items from All Items context in spec S1 — Call refreshCounts() after renameCollection alongside refreshOrganizations() S2 — Add CollectionTreeNodeTests (8 cases: flat, nested, virtual, multi-level, edge cases) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…PI shape Organizations are nested inside Profile.Organizations in the Bitwarden/ Vaultwarden API — not a top-level key. The test fixture was using a top-level "Organizations" key which broke after the SyncResponse fix sourced orgs from profile.organizations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move touch-id-inline to dated archive and sync biometric-unlock spec to reflect inline enrollment view (replaces modal sheet). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add organisation & collection feature bullet - Remove "personal vault only" limitation - Move org support off the roadmap (shipped) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Multiple accounts → Now - Watchtower + Bitwarden cloud login → Next Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OrgKeyCache.clear(): fix exclusivity violation crash by capturing byte
counts before mutating via dictionary subscript
- createOrgCipher: wrap body in { cipher, collectionIds } per Bitwarden API
- Collection CRUD: replace JSONSerialization with Codable CollectionBody
- VaultRepositoryImpl: use .contains(where:) instead of .filter{}.isEmpty
- CollectionTreeNode: filter empty segments from split(separator: "/")
- ItemEditView: document single-collection picker v1 limitation
- RootViewModelLockTests: replace flaky fixed sleep with polling helper
This was referenced Apr 16, 2026
Reverts 32b5e57. That commit re-applied the inline enrollment flow state to this branch after the merge that had already brought in the main-branch revert (861d8a6). Sheet presentation via BiometricEnrollmentPromptView is the correct behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…embership update
- VaultRepositoryImpl: org badge count now includes items with organizationId
but empty collectionIds (Default collection items)
- VaultRepositoryImpl: selecting a parent collection (e.g. Engineering) now
includes items from all descendant collections via "/" prefix matching
- CollectionTreeNode: duplicate collection names no longer replace each other —
append as separate siblings so both are reachable in the sidebar
- PrizmAPIClient: add updateCipherCollections (PUT /api/ciphers/{id}/collections)
since PUT /api/ciphers/{id} does not update collection membership
- VaultRepositoryImpl.update: call updateCipherCollections after cipher PUT for
org items so choosing "None" or changing collection actually persists
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PUT /api/ciphers/{id} returns the cipher with its pre-update collectionIds
(collection membership is updated separately via the collections endpoint).
Without patching, the in-memory cache holds stale collectionIds and the UI
shows the old collection until the next full sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove collectionIds(includingDescendantsOf:) which matched by name prefix, causing parent collections to double-count nested items - Collection counts and items(for: .collection) now use direct ID matching, consistent with folder count behavior - Add debug logging for org role/canManageCollections on sync
Use plain Label with .badge() when canManageCollections is false, avoiding the HStack/Spacer layout that pushes the count left.
Use overlay instead of HStack so the .badge() stays at its natural trailing position regardless of whether the + button is present.
…per org row - Organizations section header shows 'Organizations' title (like Folders) - Org disclosure rows show name + folder.badge.plus button, no count badge - Collection child rows keep their individual item count badges - Layout matches folder section header pattern exactly
Both folder.badge.plus buttons now use .padding(.trailing, 14) for consistent horizontal alignment between Folders and Organizations.
Section header (folder +): .trailing 10 Row (org +): .trailing 2 Different containers have different insets.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
OrgKeyCacheactor; zeroed on lock per Constitution §IIIOrganization,Collection,OrgRole;VaultItemextended withorganizationId+collectionIdsSyncResponsedecodesorganizations+collections(camelCase + PascalCase);RawCipherincludescollectionIdsCryptoKeyswhenorganizationId != nil; routes org items through org key for encrypt/decryptVaultRepositoryImplstores and filters by org/collection; org item create routes toPOST /api/ciphers/createDisclosureGroupper org, collection child rows, item count badges, inline create/rename/delete (role-gated)SECURITY.mdupdated with org key lifecycleTest plan
OrgKeyCachecleared on lockPOST /api/ciphersPOST /api/ciphers/create🤖 Generated with Claude Code