Skip to content

feat(security): per-tool approver allowlist for DEFER approvals (email-based, fail-closed) #313

Description

@initializ-mk

Follow-up from PR #311 (#310) review.

Problem

Native Slack DEFER approvals (#311) record who clicked Approve/Reject but never check whether that user is authorized to approve. Today channel membership is the entire approval ACL — anyone who can see the message and click a button resolves a security-sensitive deferred tool call. There is no approver allowlist, no per-tool restriction, and no requester≠approver (four-eyes) gate. #oncall-style channels routinely include guests, contractors, bots, and integrations.

PR #311 documents this boundary (route to a tightly-scoped private channel); this issue adds the enforcement.

Proposal — email-based, adapter-agnostic, fail-closed

Design the allowlist around email, not Slack user IDs: email is a portable identity that resolves the same way across Slack / Teams / a web console and matches Forge's OIDC identity model, whereas U012… IDs are Slack-specific and opaque.

  1. Config — per-tool (and/or default) approver allowlist:
    security:
      defer:
        tools:
          cli_execute:
            to: channel:slack:#sec-approvals
            approvers: [alice@corp.com, oncall-lead@corp.com]   # NEW
  2. Adapter resolves email. The Slack block_actions payload carries only id/username/name/team_idno email. The adapter resolves user.id → email via users.info (needs the users:read.email scope; one cacheable call per click) and populates a new ApproverEmail field on channels.ApprovalDecision.
  3. Core check stays adapter-agnostic. The allowlist check lives in the runtime/decisions path against ApproverEmail, so Teams / web-console approvers use the same rule.
  4. Fail closed. If an email can't be resolved (scope missing, guest with no email), refuse the approval rather than fall back to the id/username. Surface a clear message on the approval UI ("could not verify your identity — approval refused").
  5. (Optional) four-eyes. Reject an approver whose email equals the task's requester identity, when the requester identity is known.

Acceptance

  • approvers (email list) configurable per DEFER tool (+ optional default).
  • Slack adapter resolves approver email via users.info (cached) into ApprovalDecision.ApproverEmail.
  • A non-allowlisted approver's click is refused (and the deferral stays pending); an allowlisted one resolves it.
  • Fail-closed when email can't be resolved.
  • The allowlist check is in the shared runtime path (adapter-agnostic), not the Slack adapter.
  • Audit records the resolved approver email + the allow/deny outcome.

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestforge-cliAffects the forge-cli command-line tool (init, run, build, mcp commands)forge-coreAffects the forge-core library (runtime, security, types, llm, mcp, auth)securitySecurity vulnerability fixes

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions