Skip to content

feat(org-support): organisation and collection support#41

Open
b0x42 wants to merge 43 commits intomainfrom
feat/org-support
Open

feat(org-support): organisation and collection support#41
b0x42 wants to merge 43 commits intomainfrom
feat/org-support

Conversation

@b0x42
Copy link
Copy Markdown
Owner

@b0x42 b0x42 commented Apr 14, 2026

Summary

  • Org key crypto — RSA-OAEP-SHA1 unwrap of org symmetric keys from sync response into OrgKeyCache actor; zeroed on lock per Constitution §III
  • Domain entitiesOrganization, Collection, OrgRole; VaultItem extended with organizationId + collectionIds
  • Sync decodingSyncResponse decodes organizations + collections (camelCase + PascalCase); RawCipher includes collectionIds
  • Cipher mapper — selects org CryptoKeys when organizationId != nil; routes org items through org key for encrypt/decrypt
  • RepositoryVaultRepositoryImpl stores and filters by org/collection; org item create routes to POST /api/ciphers/create
  • Sidebar — Organizations section with DisclosureGroup per org, collection child rows, item count badges, inline create/rename/delete (role-gated)
  • Item detail/edit — org field row in detail pane; collection picker in edit sheet; folder picker hidden for org items
  • Constitution compliance — doc comments on all crypto files, RSA-OAEP-SHA1 rationale (Bitwarden protocol requirement), SECURITY.md updated with org key lifecycle

Test plan

  • All unit tests green (KATs for RSA unwrap, org cipher decryption, collection name round-trip)
  • OrgKeyCache cleared on lock
  • Sync populates orgs and collections
  • Collection filtering returns correct items
  • Personal item create still routes to POST /api/ciphers
  • Org item create routes to POST /api/ciphers/create
  • Manual: sync against live Vaultwarden instance with org membership
  • Manual: create/rename/delete collections (admin role)
  • Manual: create org item pre-filled from collection context

🤖 Generated with Claude Code

b0x42 and others added 26 commits April 13, 2026 14:10
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>
@b0x42 b0x42 added the enhancement New feature or request label Apr 15, 2026
@b0x42 b0x42 self-assigned this Apr 15, 2026
@b0x42 b0x42 marked this pull request as draft April 15, 2026 10:13
- 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
b0x42 and others added 5 commits April 16, 2026 13:51
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.
@b0x42 b0x42 force-pushed the feat/org-support branch from e13e756 to 4c90a4b Compare April 17, 2026 14:02
b0x42 added 11 commits April 17, 2026 16:06
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant