Conversation
Adds the @bitwarden/pam library and web-vault PAM surfaces behind the Pam feature flag: access-rule administration, governance dashboard, kill switch, approver inbox, my-access-requests, and cipher-open gating with an injected lease banner in the vault cipher view. Includes a localStorage-gated demo mock PAM API/store so the flow can be exercised without a server backend. Domain behaviour is captured in libs/pam/pam.allium. Flattened from the PAM development stack into a single commit on main.
Brings in additive pieces from ohinton/pm-37751/web-leasing-policies-admin: - LeaseRequestResponse / LeaseRequestStatus - LeaseRequestPatchRequest - deriveGatedState helper + spec (uses existing GatedState type) MockPamApiService now extends DefaultPamApiService, so access-rule CRUD (and any unmocked method) falls through to the real server while the mock continues to fake the lease/request transport.
Replaces the mock-driven cipher-open flow with the three real PAM lease
endpoints from PM-37044 (server branch pam/collection-leasing-config):
GET /ciphers/{id}/lease/pre-check resolves automatic vs human approval
POST /ciphers/{id}/lease mints an active lease or pending request
GET /ciphers/{id}/lease/cipher returns full data under an active lease
Clicking a partial-data cipher now drives the new flow: try the leased
fetch first; on 404, open RequestAccessModal (duration form for automatic,
window + required reason for human) and re-fetch on LeaseCreated. The
fresh Cipher is rendered transiently via a new "openWith" CipherOpenVerdict
and is never written to CipherService — every view re-hits the server.
Adds:
- Wire DTOs (LeaseModelResponse, LeaseRequestModelResponse,
AccessPreCheckResponse, AccessRequestEnvelopeResponse, CreateLeaseRequest)
- RequestAccessTrigger abstraction in libs/pam; WebRequestAccessTrigger
host impl opens the modal and propagates the close result
- Host-agnostic LeasedCipherFetcher in libs/pam that maps 404 to null
- "Request access" branch on CipherLeaseBannerComponent (gated banner)
- en/messages.json keys for the new modal + banner; also fixes a
pre-existing key typo on the older access-request modal
MockPamApiService falls through to the real impl for all three methods —
the "mock extends real" win promised by the prior contract pull-in.
Tests: +18 in libs/pam, +20 in apps/web (request-access modal, gate,
fetcher).
# Conflicts: # libs/common/src/enums/feature-flag.enum.ts
# Conflicts: # apps/web/src/app/vault/components/vault-items/vault-items.module.ts
AccessLeaseResponse / AccessRequestResponse / AccessRequestDetailsResponse replace the Lease*/Inbox* models, statuses become lowercase strings on every surface, the condition tree is AccessConditionTree with conditions children, and the wire properties follow the server rename (approvalMode, verdict, requesterId, approverId, producedLeaseId, activationDeadline). The mock start route becomes /activate; ticket/redeem wording becomes approved request / activate.
…solution open question
… as planned in PAM spec
… honestly Matches the server's deferred lease minting: approval no longer creates the lease, so the cipher-open gate now activates a startable approved request when the requester opens the item (with a visible toast) and the approver history only labels rows Active once a lease actually exists — an approved-but-not-started request shows 'Approved · not started' under Upcoming with its startable-for countdown, and the My-requests Start button hides once the window lapses.
The inbox bucketed a produced lease as active purely by its time window, so a lease ended elsewhere (the requester's End lease, expiry, another approver) kept a clickable Revoke that 409'd. Bucket on the new producedLeaseStatus so only active leases are revocable, label ended leases as Revoked, and refresh the open inbox on RefreshApproverInbox pushes and local mutations.
…, audited attempts, unified startable bound
Opening a gated cipher with an approved request no longer mints the lease. The open gate now returns "open" for an approved-but-unactivated request instead of calling activateLease, so activation stays an explicit member action via the cipher-lease banner's "Start access" button (CipherOpenAwaitingActivation), matching the spec. To reveal the full cipher once the member starts the lease, add an optional GATED_CIPHER_RELOADER seam to the vault-item dialog. When a gated cipher is open, the dialog watches the cipher's lease state and swaps the partial copy for the full, decryptable cipher in place — no reopen. The banner is untouched: the dialog reacts to lease state, so this also covers grants from the request-access flow. The seam is inert for non-gated ciphers and on platforms without PAM.
In the access rule dialog's lease settings, the default access duration must not exceed the maximum. Previously the two duration selects moved independently, so an admin could pick a default longer than the cap. Couple them: raising the default above the max drags the max up to match, and lowering the max below the default drags the default down. A max of "No maximum" never constrains the default.
Replace the separate "Request access" modal with a form that folds out within the cipher-view lease banner, so partial cipher data stays visible while requesting. The gate now opens the partial view for gated ciphers with no active lease; the injected banner owns the request flow inline, and the existing gated-cipher reloader reveals the full cipher in place after an automatic lease. Extract the modal's form into a dialog-free RequestAccessFormComponent in libs/pam (co-located with the banner), move lease-window.utils into the lib, and remove the now-dead modal, WebRequestAccessTrigger, and RequestAccessTrigger abstraction. Align libs/pam's jest config with the other Angular libs so its component specs can run.
The access-rule editor gains an allow-extensions toggle and a
max-extensions input. The cipher-lease banner replaces the one-click
extend with a fold-out form (duration presets + required justification),
gated on the new ExtensionsAllowed/ExtensionsRemaining fields from the
cipher access-state snapshot. AccessLeaseExtensionRequest becomes
{ leaseId, durationSeconds, reason }; the demo mock and en locale are
updated to match.
Mirror the server change from a per-rule extension count to a maximum extension length. The access-rule editor replaces the "maximum extensions" number input with a length dropdown (30m/1h/2h/4h/8h); the request/response types rename maxExtensions to maxExtensionDurationSeconds. In the cipher-lease banner the extend duration picker is now filtered to the presets within the rule's maximum (surfaced via the access-state snapshot), the Extend control gates on extensionsAllowed (a lease may be extended once), and the default duration is clamped to the cap. The demo mock and en locale are updated to match.
Mirror the server change: the conditions document is now a bare AccessCondition[] on the wire rather than an all_of tree. Drop the tree machinery (AccessConditionTree, parseConditionTree, treeToConditions, conditionsToTree); the request sends a flat array and the response parses one via parseAccessConditions.
Two intertwined PAM efforts on this POC branch (not cleanly separable post-hoc): - Cancel non-activated requests (requester + approver) and revoke active leases regardless of window; allium spec and cipher-lease banner updated to match. - Extract the My requests list into a reusable component (MyAccessRequestsListComponent) and embed it as a section on the approver inbox. - Remove the standalone /vault/my-requests page and route.
…T /leases/active, /leases/history)
…uled for removal)
Both the requester's access-request list and the approver inbox rendered denormalized cipher/collection names — the inbox decrypted them with the org key, the requester's list showed raw cipher ids. An access-rule-gated cipher already syncs to the vault of anyone who governs or requested it as a partial CipherView (name decrypted) plus its CollectionView, so resolve names from local vault state instead. - Add a shared AccessRequestNameResolver (CipherService + CollectionService) that fills cipherName/collectionName in place; both pages use it - my-access-requests now also shows the collection name - approver-inbox drops its org-key name decryption - fall back to the raw id when an item isn't in local vault state
Add an "Access requests" entry to the Admin Console PAM nav group so admins can review and act on access requests without switching to the Password Manager. Reuses the existing standalone ApproverInboxComponent via a new org-scoped route; no component or business logic is duplicated. The nav item mirrors the Password Manager's pending-count badge via the root-provided ApproverInboxBadgeService. Also wires the org layout spec: mocks the new ApproverInboxBadgeService dependency and provides the previously-missing ConfigService mock (the suite was already failing on clean HEAD after the ts-strict refactor).
…requests/Audit log)
The reactive collection-name backfill lost a load-ordering race: the warm-up emission of collectionNames$() reached the page-scoped services' constructor subscription before load() populated the row subjects, so it filled names on an empty list and never re-ran. resolveDisplayNames also read decryptedCollections$ at load time, when it is often still cold, so names resolved to null. Cipher names survived because the cipher vault state is warm by then. Derive the public row/lease/history streams reactively by combining the held data with the resolver's collectionNames$() instead of imperatively mutating-and-re-emitting from a constructor subscription. Now whenever either the data or the decrypted-collection state emits, names re-apply - no ordering dependency.
Replaces the flat approver fields with a decisions[] log: each element carries deciderKind (human|automatic), the verdict, decidedAt, and (for a human decision) the approver identity + comment. resolveApprover/resolveResolver now pick the human decision (else show the access-rule label).
Introduce a root-singleton ApproverInboxRequestsService as the single source of the /access-requests/inbox fetch. Page services self-load and derive their views from it, the nav badges live in PAM-owned nav-slot components that read its count$, and components subscribe instead of calling load() — eliminating the duplicate /inbox, /mine, and /leases fetches on inbox open. Removes the standalone ApproverInboxBadgeService.
Extending a lease creates a second AccessRequest (extensionOfLeaseId points
at the parent lease); on approval the server applies it in place and never
mints a new lease. My Requests rendered each extension as its own unlabeled
row, so one grant looked like several duplicate requests.
Fold extensions into the grant they extend instead:
- Extension requests are no longer standalone rows; the grant is badged with
the total time added and the new end ("Extended +30m until ...").
- While the lease is active the grant and badge live in Active leases; once it
ends the grant moves to History (badge preserved). A grant is no longer
shown in both sections at once.
Joins on producedLeaseId / extensionOfLeaseId, treating an applied extension as
either approved (server) or activated (mock/spec).
Two scoped CLAUDE.md files documenting the PAM (credential leasing) feature: - libs/pam/CLAUDE.md: domain model, pam.allium as source of truth, the cross-repo split (evaluation logic lives in bitwarden/server), the PamApiService route table + refresh model, enums, and conventions. - apps/web/src/app/pam/CLAUDE.md: routing mount points, surfaces, the cipher-open/banner/reloader vault seam via DI tokens, service patterns, feature flag, and the mock layer. Notes captured: governance/kill-switch API methods are still stubbed (Promise.reject) so those surfaces are mock-only, and FeatureFlag.Pam is defaulted TRUE in this worktree for the demo.
|
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.



🎟️ Tracking
📔 Objective
📸 Screenshots