---
id: CAP-PFM-SEC-015
title: Enforce access at request boundaries
feature: FEAT-PFM-SEC-04
product_area: platform
module: security
revision: 1
status: draft
product_intent: confirmed
implementation_evidence: observed
supersedes: []
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
intent_sources:
  - 'services-platform:docs/hq/rhythm-hq-plan.md#D1 (rule S1, accepted 2026-10-12)'
  - >-
    services-platform:docs/security/application-access.md#The application access step, enforced
    everywhere
  - 'services-platform:docs/security/grant-slice-growth.md#Andrew''s decisions (2026-10-19, Decision 1: A)'
  - >-
    services-platform:docs/security/security-administration.md#14 Acceptance checkpoints (2026-10-11
    Final acceptance)
purpose: >-
  Platform checks the caller, operation and applicable grants before a protected service request
  proceeds. Unavailable security inputs produce a refusal.
outcomes:
  - Protected person requests receive a bounded authorization decision.
  - Staff and customer authority remain distinct at request boundaries.
  - Refusal metadata explains the required access without caller identity data.
concepts:
  - CON-PFM-SEC-003
actors:
  - ACT-PFM-006
  - ACT-PFM-002
  - ACT-PFM-003
  - ACT-PFM-004
surfaces:
  - api
preconditions: []
postconditions: []
permissions:
  - key: platform.application.access
    where_checked: api
    scope: tenant
  - key: platform.security-self.read
    where_checked: api
    scope: record
rules:
  - id: BR-CAP-PFM-SEC-015-01
    proved_by: [ATS-PFM-SEC-015-01, TST-PFM-SEC-194]
    statement: >-
      A protected request requires a token issued through an active registered pool and client. Missing
      tokens, unknown issuers, inactive pools and unregistered clients are rejected before identity or
      grant lookup.
    kind: validation
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/authorizer/authorize.spec.ts#refuses an unknown
      issuer: 401, and no pool, identity or grant is read
    origin: >-
      services-platform:docs/security/console-authentication.md#Acceptance checkpoints (2026-10-10 Final
      acceptance)
  - id: BR-CAP-PFM-SEC-015-02
    proved_by: [ATS-PFM-SEC-015-02, TST-PFM-SEC-194, TST-PFM-SEC-195]
    statement: >-
      Authorization uses an existing identity belonging to an active user in the pool’s account boundary.
      It creates no identity; suspended, invited and revoked users cannot proceed.
    kind: validation
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/authorizer/authorize.spec.ts#refuses an unknown
      identity with 401 and writes nothing: the authorizer never creates one
    origin: >-
      services-platform:docs/security/console-authentication.md#Acceptance checkpoints (2026-10-10 Final
      acceptance)
  - id: BR-CAP-PFM-SEC-015-03
    proved_by: [ATS-PFM-SEC-015-03, TST-PFM-SEC-194]
    statement: >-
      Portal tokens cannot authorize console or HQ routes. Portal membership applies only in its own
      workspace; ordinary console membership does not cross the customer boundary.
    kind: permission
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/authorizer/authorize.spec.ts#refuses a portal token
      on a console route before any identity is read
    origin: >-
      services-platform:docs/security/security-administration.md#14 Acceptance checkpoints (2026-10-11
      Final acceptance)
  - id: BR-CAP-PFM-SEC-015-04
    proved_by: [ATS-PFM-SEC-015-04, TST-PFM-SEC-067]
    statement: >-
      Staff authority applies to HQ routes. Even the broadest staff grant supplies no console or portal
      authority, and customer Administrator authority supplies no staff status on HQ routes.
    kind: permission
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/staff-separation.e2e.spec.js#refuses the widest grant
      Rhythm holds, every one of them
    origin: 'services-platform:docs/hq/rhythm-hq-plan.md#D1 (rule S1, accepted 2026-10-12)'
  - id: BR-CAP-PFM-SEC-015-05
    proved_by: [ATS-PFM-SEC-013-15, ATS-PFM-SEC-015-05, TST-PFM-SEC-194, TST-PFM-SEC-196]
    statement: >-
      A release must declare each served operation. System routes require staff-only keys, staff-only
      keys belong on system routes, and protected routes require permissions. Invalid declarations
      prevent staging.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/authorizer/registry-manifest.spec.ts#refuses a
      staffOnly key below a system route
    origin: 'services-platform:docs/hq/rhythm-hq-plan.md#D1 (rule S1, accepted 2026-10-12)'
  - id: BR-CAP-PFM-SEC-015-06
    proved_by: [ATS-PFM-SEC-002-24, ATS-PFM-SEC-015-06, ATS-PFM-SEC-015-12, TST-PFM-SEC-194]
    statement: >-
      Authorization limits the request to workspaces reachable through applicable grants. An inaccessible
      workspace in the path is refused; an operation with no applicable grant is denied altogether.
    kind: permission
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/authorizer/authorize.spec.ts#returns a Deny for the
      whole route when no grant could apply anywhere
    origin: >-
      services-platform:docs/security/security-administration.md#14 Acceptance checkpoints (2026-10-11
      Final acceptance)
  - id: BR-CAP-PFM-SEC-015-07
    proved_by: [ATS-PFM-SEC-015-07, ATS-PFM-SEC-015-13, ATS-PFM-SEC-015-16, ATS-PFM-SEC-015-17, TST-PFM-SEC-194, TST-PFM-SEC-271]
    statement: >-
      The service receives authority through a signed context rather than the raw token. Its expiry
      follows the gateway cache lifetime plus thirty seconds, and it contains no caller address. A grant
      slice exceeding 6 KB, or failure to read the registry, grants or signing key, prevents
      authorization.
    kind: invariant
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/authorizer/authorize.spec.ts#refuses a grant slice
      over 6 KB rather than truncating it
    origin: >-
      services-platform:docs/security/security-administration.md#14 Acceptance checkpoints (2026-10-11
      Final acceptance)
  - id: BR-CAP-PFM-SEC-015-08
    proved_by: [ATS-PFM-SEC-015-08, TST-PFM-SEC-194]
    statement: >-
      Refusal metadata describes the cause and required permission keys without identifying the caller or
      another person.
    kind: policy
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/authorizer/authorize.spec.ts#names the refusal reason
      and the missing key, both strings, and nothing that identifies the caller
    origin: none
  - id: BR-CAP-PFM-SEC-015-09
    proved_by: [ATS-PFM-SEC-015-09, TST-PFM-SEC-047]
    statement: >-
      Application permissions require both an application-access grant and a workspace licence. These
      checks enforce in every environment; Platform and HQ are exempt. Refusals provide client-readable
      codes.
    kind: permission
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/application-access-enforced.e2e.spec.js#is refused it
      once a deny names the access key, though the allow beside it is untouched
    origin: >-
      services-platform:docs/security/application-access.md#The application access step, enforced
      everywhere
  - id: BR-CAP-PFM-SEC-015-10
    proved_by: []
    statement: >-
      Where a service enables decisions by workspace path, the cached decision and grant slice cover that
      workspace alone, avoiding growth with the customer’s workspace count.
    kind: policy
    enforced_at:
      - api
    must_prove: false
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/authorizer/authorize.spec.ts#decides and signs for
      the path workspace alone: the policy, the tenants and the slice name no other
    origin: >-
      services-platform:docs/security/grant-slice-growth.md#Andrew's decisions (2026-10-19, Decision 1:
      A)
  - id: BR-CAP-PFM-SEC-015-11
    proved_by: [ATS-PFM-SEC-015-10, TST-PFM-SEC-081]
    statement: >-
      Access changes can leave an existing gateway decision usable for at most its sixty-second cache
      lifetime. The flush workflow can report success without evicting that decision.
    kind: policy
    enforced_at:
      - api
    must_prove: true
    provenance: documented
    source: 'services-platform:docs/security/authorizer-cache-flush.md#What the system does today'
    origin: >-
      services-platform:docs/security/security-administration.md#14 Acceptance checkpoints
      (2026-10-12/13, known bound accepted 2026-10-13)
  - id: BR-CAP-PFM-SEC-015-12
    proved_by: [ATS-PFM-SEC-015-14]
    statement: >-
      A record-type grant can permit passage through the gateway; the resource service must still answer
      an unrelated record type as not found.
    kind: permission
    enforced_at:
      - api
      - service
    must_prove: false
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/authorizer/authorize.spec.ts#the gateway lets the
      holder through to an object the scope does not name, so the service answers it as not found
    origin: >-
      services-platform:docs/security/security-administration.md#12. Approved omissions and tracked gaps
      (resource-scoped grants, PLAT-M5 D10)
  - id: BR-CAP-PFM-SEC-015-13
    proved_by: [ATS-PFM-SEC-015-11, ATS-PFM-SEC-015-15, ATS-PFM-SEC-018-08, TST-PFM-SEC-198]
    statement: >-
      Service-to-service authority comes from the calling service’s cloud role. A service function must
      declare the operation it serves, and a sensitive operation requires a function marked sensitive.
    kind: permission
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/authorizer/service-function-rules.spec.ts#refuses a
      route whose catalog permission is sensitive behind a function that is not, naming the key
    origin: >-
      services-platform:docs/security/service-calls.md#Service calls: the service-principal registry and
      the kill switch
scenarios:
  - id: SC-CAP-PFM-SEC-015-01
    given: A person holds the complete HQ Administrator grant.
    when: They request a CRM contact route without customer access.
    then: HQ authority does not authorize the CRM request; a separate staff access grant is needed.
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/staff-separation.e2e.spec.js#refuses a route that
      names a tenant at the boundary, because reaching every tenant is a system claim too
  - id: SC-CAP-PFM-SEC-015-02
    given: A workspace lacks an application licence.
    when: A permission holder requests that application and then Platform.
    then: The application request returns a licensing refusal; the Platform request remains available.
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/application-access-enforced.e2e.spec.js#refuses the
      same route in the withheld tenant, under the licence step’s own code
edge_cases: []
constraints: []
depends_on:
  - CAP-PFM-SEC-013
related:
  - CAP-PFM-SEC-002
  - CAP-PFM-SEC-007
  - CAP-PFM-SEC-010
  - CAP-PFM-SEC-018
journeys:
  - JNY-PFM-SEC-005
open_questions: []
evidence:
  - >-
    Pinned services-platform references for the structured rows: docs/hq/rhythm-hq-plan.md;
    docs/security/application-access.md; docs/security/authorizer-cache-flush.md;
    docs/security/console-authentication.md; docs/security/grant-slice-growth.md;
    docs/security/security-administration.md; docs/security/service-calls.md;
    modules/security/tests/e2e/application-access-enforced.e2e.spec.js;
    modules/security/tests/e2e/staff-separation.e2e.spec.js;
    modules/security/tests/unit/authorizer/authorize.spec.ts;
    modules/security/tests/unit/authorizer/registry-manifest.spec.ts;
    modules/security/tests/unit/authorizer/service-function-rules.spec.ts.
controls: [TC-AVL-001, TC-AVL-004, TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-AZ-006, TC-CRY-001, TC-CRY-002, TC-CRY-003, TC-CRY-004, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-IAM-001, TC-IAM-002, TC-IAM-003, TC-IAM-004, TC-IAM-005, TC-IAM-006, TC-LOG-001, TC-LOG-004, TC-LOG-005, TC-MON-001, TC-PI-001, TC-PI-002, TC-PI-004, TC-PI-005, TC-PRV-001, TC-PRV-002, TC-SEC-001, TC-SEC-002, TC-SEC-003, TC-SEC-004, TC-TEN-001, TC-TEN-002, TC-TEN-003, TC-VEN-001, TC-VEN-002, TC-VEN-003]
exercised_by: [FAS-PFM-SEC-04-02, FAS-PFM-SEC-04-03, FAS-PFM-SEC-04-04, FAS-PFM-SEC-04-05]
suites: [TST-PFM-SEC-047, TST-PFM-SEC-067, TST-PFM-SEC-081, TST-PFM-SEC-194, TST-PFM-SEC-195, TST-PFM-SEC-196, TST-PFM-SEC-198, TST-PFM-SEC-271]
---
# Enforce access at request boundaries

## Purpose

Platform checks the caller, operation and applicable grants before a protected service request proceeds. Unavailable security inputs produce a refusal.

## How It Works

The gateway checks the token’s registered origin and resolves an existing membership before evaluating the requested operation. It reads the release declaration and applicable grants to establish the caller’s reach. The receiving service uses the signed result and applies any remaining resource checks. A missing security input produces a refusal that the caller can inspect. Previously authorized requests can retain a cached decision until its expiry, so reviewers must distinguish a fresh decision from one already issued.

## Relationships

- Release catalogs supply declared operations and permission definitions.
- Security context presents evaluated access to the console.
- Role assignments and staff-entry records supply the grants under evaluation.
- Workspace administration owns availability beside authorization.
