Skip to content

Experiment: PAM POC#21358

Draft
Hinton wants to merge 76 commits into
mainfrom
pam/poc
Draft

Experiment: PAM POC#21358
Hinton wants to merge 76 commits into
mainfrom
pam/poc

Conversation

@Hinton

@Hinton Hinton commented Jun 18, 2026

Copy link
Copy Markdown
Member

🎟️ Tracking

📔 Objective

📸 Screenshots

abergs and others added 30 commits May 30, 2026 21:32
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.
… 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.
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.
Hinton and others added 29 commits June 12, 2026 17:21
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.
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).
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.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants