Skip to content

feat(api,recs): permission-gated direct purchase execute (execute-{any,own}) bypassing approval email, with cost warning + confirm gate #289

@cristim

Description

@cristim

Background

Today every purchase goes through approval-by-email — see internal/config/defaults.go:59, internal/config/types.go:15 (ApprovalRequired bool, default true). There is no way for a sufficiently privileged user to skip the email round-trip and execute a purchase directly from the dashboard, even though they're logged in, RBAC-authorized, and looking at the rec they want to buy.

For trusted operators (admin role, finance lead, automation accounts) the email round-trip is friction without security benefit — they already have the authority that the email step is verifying.

What needs to happen

Permission verb

Add execute-any:purchases and execute-own:purchases to internal/auth/types.go:284-300, mirroring the existing cancel-any/cancel-own and retry-any/retry-own shape:

  • RoleAdminexecute-any:purchases (admin-implies-everything precedent)
  • RoleUser → no default grant. Must be explicitly granted per-user/per-role; this is a finance-impacting permission.
  • execute-own:purchases exists for parity with the other *-own verbs (lets a delegated buyer execute purchases they themselves drafted) but is similarly opt-in.

Backend: bypass approval when permission holds

The current executePurchase flow (find via frontend/src/app.ts:351, 433internal/api/handler_purchases.go) routes every request through SendPurchaseApprovalRequest (:1167). Branch the path:

if h.executor.SessionHasExecutePermission(session, plan) {
  // Direct execution — bypass approval, charge immediately.
  result, err := h.executor.ExecuteImmediately(ctx, plan)
  // Audit trail: persist executed_by_user_id, executed_at, and a
  // non-null pre_approval_skip_reason = "direct-execute permission".
  ...
} else {
  // Existing path: send approval email, return pending_approval.
  ...
}

Audit fields are non-optional — finance auditors need to be able to ask "who direct-executed this $X purchase?".

Frontend: warning + confirmation gate

openPurchaseModal (frontend/src/recommendations.ts:2084) renders the modal. When the session has execute-any:purchases:

  • Toggle / radio: "How would you like to handle this purchase?" with two options: Send for Approval (default, current behavior) and Execute Now.
  • When Execute Now is selected, render a styled red callout:

    ⚠️ This will charge $X,XXX upfront immediately. This bypasses the approval step. AWS allows cancellation within 24 hours via the Account & Billing console (see # for the revocation procedure).

  • The Execute button label becomes "Execute Purchase Now".
  • A second-confirmation step before firing the API call — either a typed-confirmation ("type EXECUTE to confirm") or a styled red confirmDialog with the rec summary echoed back. The Cancel button's confirmDialog shape is sufficient precedent.

Email notification (sibling issue)

When direct-execute fires, an email STILL goes out — but it's a notification ("we executed this purchase on your behalf, here's a cancel link") rather than an approval request. See sibling: "Post-execution cancel-link email".

Acceptance

  • A user with execute-any:purchases sees the Send-for-Approval / Execute-Now toggle in the purchase modal.
  • Selecting Execute-Now shows the cost callout + the cancellation-window note + the second-confirmation gate.
  • The API call results in an immediate charge (the actual cloud-side commit fires inline rather than enqueueing the approval).
  • A user without execute-any:purchases only sees the Send-for-Approval path; the toggle is absent.
  • Audit fields (executed_by_user_id, executed_at, pre_approval_skip_reason) are populated for direct executes.
  • Tests:
    • Unit: backend branches correctly on permission.
    • Frontend: toggle renders only when the permission is present.
    • End-to-end: a direct-execute by an admin produces an executed row and triggers the cancel-link email (sibling issue).

Severity

p2 / severity/medium / impact/few — affects only the small set of users who'd hold the new permission. Not a bug; the current flow is safe-by-default. This is a deliberate convenience escape hatch with audit + warning + cancellation-window guardrails.

References

  • internal/auth/types.go:284-300 (default permission grants — the cancel-own / retry-own precedents)
  • internal/api/handler_purchases.go (the full executePurchase + SendPurchaseApprovalRequest flow)
  • internal/config/defaults.go:59 + internal/config/types.go:15 (the global approval-required default — stays as the fallback)
  • frontend/src/recommendations.ts:2084 (the purchase modal — needs the toggle + warning + confirm-gate)
  • frontend/src/app.ts:351, 433 (the execute-button text reset — needs the new label branch)
  • Sibling: "Purchase modal copy clarification" — must land before or with this so the default-path button reads correctly even before this lands.
  • Sibling: "Post-execution cancel-link email" — the after-the-fact notification path for direct-executes.
  • Sibling: "AWS RI/SP revocation via support case" — the actual AWS-side cancellation mechanism the warning references.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions