---
id: ATS-PFM-SEC-009
scope: CAP-PFM-SEC-009
product_area: platform
module: security
revision: 2
status: proposed
approved_by: null
approved_at: null
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
scenarios:
  - id: ATS-PFM-SEC-009-01
    title: Check customer user-list scope and pagination
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-01
    preconditions: Prepare console users with varied statuses and roles.
    steps: >-
      Page by surname, filter status, request another customer and call using
      platform staff authority alone.
    expected: >-
      Pagination and filters select the expected memberships; outside-customer
      access returns 404 and unrelated staff authority is refused.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#answers
        a page of the customer’s console users, by surname
      - TST-PFM-SEC-051
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#walks
        the page with the cursor it answered, and stops answering one at the end
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#narrows
        to a status, and leaves the others out
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#answers
        404 for a customer the caller does not reach, saying nothing about
        whether it exists
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#refuses
        a platform-scope caller the users of a customer they are not of, because
        this route is console
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#answers
      a page of the customer’s console users, by surname
    category: boundary
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#answers
        a page of the customer’s console users, by surname
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#walks
        the page with the cursor it answered, and stops answering one at the end
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#narrows
        to a status, and leaves the others out
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#answers
        404 for a customer the caller does not reach, saying nothing about
        whether it exists
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#refuses
        a platform-scope caller the users of a customer they are not of, because
        this route is console
  - id: ATS-PFM-SEC-009-02
    title: Check the allowed profile fields and version
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-03
    preconditions: Read a membership at version n.
    steps: >-
      Change names at n, repeat with a stale version, submit a disallowed field
      and call as a reader.
    expected: >-
      The valid change advances version; stale, malformed and unauthorized
      attempts return 409, 400 and 403 respectively.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#changes
        the names and answers the person again, at the next version
      - TST-PFM-SEC-051
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#refuses
        a version the record has moved past as 409, and changes nothing
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#refuses
        a body that states anything but the five detail fields as 400
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#refuses
        the caller who may only read as 403, before the handler runs
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#changes
      the names and answers the person again, at the next version
    category: concurrency
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#changes
        the names and answers the person again, at the next version
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#refuses
        a version the record has moved past as 409, and changes nothing
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#refuses
        a body that states anything but the five detail fields as 400
      - >-
        services-platform:modules/security/tests/e2e/console-users.e2e.spec.js#refuses
        the caller who may only read as 403, before the handler runs
  - id: ATS-PFM-SEC-009-03
    title: Check suspension and self-protection
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-04
    preconditions: Prepare an active member and the administrator’s own identity.
    steps: 'Suspend the member, inspect the lifecycle result and try self-suspension.'
    expected: >-
      Outstanding invitations are withdrawn, successful pool disablement is
      reported and a flush job is submitted; self-suspension fails. Job
      submission does not establish cache eviction.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#suspends
        an account, revoking its invitation with it, disabling its pool account
        and flushing the caches
      - TST-PFM-SEC-060
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        an administrator who tries to suspend or revoke their own account
      - >-
        services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#refuses
        a caller suspending their own account, writing nothing
      - TST-PFM-SEC-212
      - >-
        react-console:e2e/security/users.spec.ts#deactivating really shuts
        somebody out, and reactivating lets them back in
      - TST-PFM-SEC-035
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#suspends
      an account, revoking its invitation with it, disabling its pool account
      and flushing the caches
    category: transition
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#suspends
        an account, revoking its invitation with it, disabling its pool account
        and flushing the caches
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        an administrator who tries to suspend or revoke their own account
      - >-
        services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#refuses
        a caller suspending their own account, writing nothing
      - >-
        react-console:e2e/security/users.spec.ts#deactivating really shuts
        somebody out, and reactivating lets them back in
  - id: ATS-PFM-SEC-009-04
    title: Check reinstatement lifecycle restrictions
    level: unit
    proves:
      - BR-CAP-PFM-SEC-009-05
    preconditions: Prepare suspended and revoked memberships.
    steps: Attempt reinstatement for each.
    expected: >-
      The suspended membership becomes active and pool enablement is attempted;
      the revoked membership must return through invitation and is refused.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#re-enables
        the pool account and makes the membership active again
      - TST-PFM-SEC-212
      - >-
        services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#refuses
        an account that is not suspended (a revoked one comes back as a fresh
        invitation)
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#re-enables
      the pool account and makes the membership active again
    category: transition
    test_references:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#re-enables
        the pool account and makes the membership active again
      - >-
        services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#refuses
        an account that is not suspended (a revoked one comes back as a fresh
        invitation)
  - id: ATS-PFM-SEC-009-05
    title: Check membership revocation cascades
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-06
    preconditions: Prepare a member holding assignments.
    steps: Revoke the membership.
    expected: >-
      Membership, invitations and assignments end; reinstatement cannot undo
      revocation, while reinvitation remains the return path.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#revokes
        a membership with every assignment it held, and nothing undoes it
      - TST-PFM-SEC-060
      - >-
        services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#ends
        the membership, its invitations and every assignment it held in one
        transaction
      - TST-PFM-SEC-212
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#revokes
      a membership with every assignment it held, and nothing undoes it
    category: transition
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#revokes
        a membership with every assignment it held, and nothing undoes it
      - >-
        services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#ends
        the membership, its invitations and every assignment it held in one
        transaction
  - id: ATS-PFM-SEC-009-06
    title: Check rollback when the last administrator would be lost
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-07
      - INV-DOM-PFM-SEC-003-12
    preconditions: Prepare a customer with one qualifying administrator.
    steps: >-
      Try suspension, revocation, role withdrawal and deletion of that
      administrator.
    expected: >-
      Each guarded reduction returns the last-administrator refusal, rolls back
      and records the refused attempt.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#refuses
        a suspension that would leave the customer with no security
        administrator, and suspends nobody
      - TST-PFM-SEC-212
      - >-
        services-platform:modules/security/tests/integration/services/invitations/revoke-lockout.integration.spec.js#refuses
        the revocation as a lockout, naming the guard’s machine code
      - TST-PFM-SEC-121
      - >-
        services-platform:modules/security/tests/integration/services/membership/manage-assignments.integration.spec.js#refuses
        the withdrawal that would leave the customer with no security
        administrator, and withdraws nothing
      - TST-PFM-SEC-128
      - >-
        services-platform:modules/security/tests/integration/services/invitations/delete-user.integration.spec.js#refuses
        the delete as a lockout, naming the guard’s machine code
      - TST-PFM-SEC-117
      - >-
        react-console:e2e/security/users.spec.ts#refuses to remove the
        Administrator role from the last person who holds it
      - TST-PFM-SEC-035
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#refuses
      a suspension that would leave the customer with no security administrator,
      and suspends nobody
    category: invariant
    test_references:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#refuses
        a suspension that would leave the customer with no security
        administrator, and suspends nobody
      - >-
        services-platform:modules/security/tests/integration/services/invitations/revoke-lockout.integration.spec.js#refuses
        the revocation as a lockout, naming the guard’s machine code
      - >-
        services-platform:modules/security/tests/integration/services/membership/manage-assignments.integration.spec.js#refuses
        the withdrawal that would leave the customer with no security
        administrator, and withdraws nothing
      - >-
        services-platform:modules/security/tests/integration/services/invitations/delete-user.integration.spec.js#refuses
        the delete as a lockout, naming the guard’s machine code
      - >-
        react-console:e2e/security/users.spec.ts#refuses to remove the
        Administrator role from the last person who holds it
  - id: ATS-PFM-SEC-009-07
    title: Check deletion attribution and dependency refusals
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-08
      - INV-DOM-PFM-SEC-003-21
    preconditions: >-
      Prepare unreferenced and referenced members, the caller and Rhythm’s last
      Administrator.
    steps: Attempt deletion for each.
    expected: >-
      The safe deletion retains attribution and audit history; dependent, self
      and last-staff-administrator cases fail.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#keeps the
        name behind the id, so every record the person touched can still say who
        it was
      - TST-PFM-SEC-069
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#answers
        409 and user_undeletable for somebody other records still point at
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#refuses
        an administrator deleting their own account, and says so rather than
        hiding it
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#answers
        409 for Rhythm’s last Rhythm Administrator, and deletes nobody
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/users.e2e.spec.js#keeps the
      name behind the id, so every record the person touched can still say who
      it was
    category: invariant
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#keeps the
        name behind the id, so every record the person touched can still say who
        it was
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#answers
        409 and user_undeletable for somebody other records still point at
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#refuses
        an administrator deleting their own account, and says so rather than
        hiding it
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#answers
        409 for Rhythm’s last Rhythm Administrator, and deletes nobody
  - id: ATS-PFM-SEC-009-08
    title: Check retried pool-account removal
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-09
    preconditions: Prepare a deleted membership whose pool operation fails once.
    steps: Dispatch removal work and deliver the queue item twice.
    expected: >-
      Failure remains retryable with the pool account present; subsequent
      success removes it and records completion.
    negative: false
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/integration/services/dispatch/pool-account-removals.integration.spec.js#queues
        the removal of the login the delete read before the row went
      - TST-PFM-SEC-114
      - >-
        services-platform:modules/security/tests/integration/services/dispatch/pool-account-removals.integration.spec.js#leaves
        the account in the pool when the call failed: nothing pretends it went
      - >-
        services-platform:modules/security/tests/integration/services/dispatch/pool-account-removals.integration.spec.js#removes
        the account on the second delivery, and tells SQS the item is done
      - >-
        services-platform:modules/security/tests/integration/services/dispatch/pool-account-removals.integration.spec.js#records
        the removal on the security trail, which is what lasts once the queue
        row is gone
    provenance: test
    source: >-
      services-platform:modules/security/tests/integration/services/dispatch/pool-account-removals.integration.spec.js#queues
      the removal of the login the delete read before the row went
    category: failure
    test_references:
      - >-
        services-platform:modules/security/tests/integration/services/dispatch/pool-account-removals.integration.spec.js#queues
        the removal of the login the delete read before the row went
      - >-
        services-platform:modules/security/tests/integration/services/dispatch/pool-account-removals.integration.spec.js#leaves
        the account in the pool when the call failed: nothing pretends it went
      - >-
        services-platform:modules/security/tests/integration/services/dispatch/pool-account-removals.integration.spec.js#removes
        the account on the second delivery, and tells SQS the item is done
      - >-
        services-platform:modules/security/tests/integration/services/dispatch/pool-account-removals.integration.spec.js#records
        the removal on the security trail, which is what lasts once the queue
        row is gone
  - id: ATS-PFM-SEC-009-09
    title: Check identity separation and immutable ownership
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-10
      - INV-DOM-PFM-SEC-003-01
    preconditions: Prepare a customer and workspace.
    steps: >-
      Create console and portal memberships sharing an address, duplicate within
      a boundary, edit lifecycle and attempt a cross-customer move.
    expected: >-
      Cross-boundary identities remain distinct, same-boundary duplication
      fails, lifecycle edits have no effect and moving ownership fails.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#creates a
        portal user of one of the customer’s tenants, and the same e-mail in
        another wall is a different user
      - TST-PFM-SEC-069
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#refuses a
        duplicate e-mail in the same wall (400) and a wall the access type does
        not allow (400)
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#replaces
        and patches a user in scope; the lifecycle is untouchable by the generic
        update and a move out of the customer is refused
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/users.e2e.spec.js#creates a
      portal user of one of the customer’s tenants, and the same e-mail in
      another wall is a different user
    category: invariant
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#creates a
        portal user of one of the customer’s tenants, and the same e-mail in
        another wall is a different user
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#refuses a
        duplicate e-mail in the same wall (400) and a wall the access type does
        not allow (400)
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#replaces
        and patches a user in scope; the lifecycle is untouchable by the generic
        update and a move out of the customer is refused
  - id: ATS-PFM-SEC-009-10
    title: Check canonical linking against verified pool facts
    level: unit
    proves:
      - BR-CAP-PFM-SEC-009-11
      - INV-DOM-PFM-SEC-003-05
    preconditions: >-
      Prepare enabled pool accounts with matching and mismatched verified
      addresses.
    steps: Attempt linking both.
    expected: >-
      The matching account and supported federated identities link; the mismatch
      fails.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/unit/services/identity/link-cognito-identity.spec.js#never
        links on the request's say-so: the pool user must exist, be enabled, and
        carry the user's verified e-mail
      - TST-PFM-SEC-204
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#links the
        Cognito account from the pool of the user's own wall, records its
        federated identities, and refuses a mismatch
      - TST-PFM-SEC-069
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/services/identity/link-cognito-identity.spec.js#never
      links on the request's say-so: the pool user must exist, be enabled, and
      carry the user's verified e-mail
    category: validation
    test_references:
      - >-
        services-platform:modules/security/tests/unit/services/identity/link-cognito-identity.spec.js#never
        links on the request's say-so: the pool user must exist, be enabled, and
        carry the user's verified e-mail
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#links the
        Cognito account from the pool of the user's own wall, records its
        federated identities, and refuses a mismatch
  - id: ATS-PFM-SEC-009-11
    title: Check console edits and lifecycle actions
    level: browser
    proves:
      - AS-JNY-PFM-SEC-003-03
    preconditions: Prepare a security administrator and colleague.
    steps: 'Save profile changes, reload, deactivate and reactivate.'
    expected: >-
      The saved profile persists across reload and the tested membership checks
      follow suspension and reinstatement.
    negative: false
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        react-console:e2e/security/users.spec.ts#edits a person's details and
        keeps them through a reload
      - TST-PFM-SEC-035
      - >-
        react-console:e2e/security/users.spec.ts#deactivating really shuts
        somebody out, and reactivating lets them back in
    provenance: test
    source: >-
      react-console:e2e/security/users.spec.ts#edits a person's details and
      keeps them through a reload
    category: interaction
    test_references:
      - >-
        react-console:e2e/security/users.spec.ts#edits a person's details and
        keeps them through a reload
      - >-
        react-console:e2e/security/users.spec.ts#deactivating really shuts
        somebody out, and reactivating lets them back in
  - id: ATS-PFM-SEC-009-12
    title: 'Membership status: active to suspended'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-04
    state_model: STATE-PFM-SEC-004
    transition:
      from: active
      to: suspended
      index: 3
    preconditions: >-
      Use an isolated membership status in active state, with the required
      authority and guard fixtures.
    steps:
      - do: >-
          Suspend the active member and inspect post-commit provider
          synchronization separately.
        expect: >-
          Read the resulting membership status as suspended; inspect the
          associated records and recorded history for this transition.
        provenance: proposed
        source: &ref_0
          - >-
            services-platform:modules/security/services/invitations/suspend-user.ts
          - >-
            services-platform at ad3f6a65: modules/security/db/users.table.ts;
            services/invitations/accept-invitation.ts, activate-user.ts,
            invite-user.ts, revoke-user.ts, suspend-user.ts,
            resend-invitation.ts, user-deletion.ts;
            services/membership/lockout.ts, last-administrator.ts.
    expected: >-
      Read the resulting membership status as suspended; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_0
    guard_cases: >-
      The target is reachable, is not the caller and last-administrator guards
      pass.
    side_effects_to_verify:
      - Revoke outstanding invitations atomically
      - >-
        After commit, attempt provider disabling and flush submission; report
        provider trouble separately
    transition_assertion:
      state: suspended
      provenance: proposed
      source: *ref_0
  - id: ATS-PFM-SEC-009-13
    title: 'Membership status: suspended to active'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-05
    state_model: STATE-PFM-SEC-004
    transition:
      from: suspended
      to: active
      index: 4
    preconditions: >-
      Use an isolated membership status in suspended state, with the required
      authority and guard fixtures.
    steps:
      - do: >-
          Reinstate the suspended member with authority covering all retained
          assignments.
        expect: >-
          Read the resulting membership status as active; inspect the associated
          records and recorded history for this transition.
        provenance: proposed
        source: &ref_1
          - >-
            services-platform:modules/security/services/invitations/suspend-user.ts
          - >-
            services-platform at ad3f6a65: modules/security/db/users.table.ts;
            services/invitations/accept-invitation.ts, activate-user.ts,
            invite-user.ts, revoke-user.ts, suspend-user.ts,
            resend-invitation.ts, user-deletion.ts;
            services/membership/lockout.ts, last-administrator.ts.
    expected: >-
      Read the resulting membership status as active; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_1
    guard_cases: The caller may widen access for every retained assignment.
    side_effects_to_verify:
      - Attempt provider enabling after commit
      - Keep withdrawn invitations withdrawn
    transition_assertion:
      state: active
      provenance: proposed
      source: *ref_1
  - id: ATS-PFM-SEC-009-14
    title: 'Membership status: active to revoked'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-06
    state_model: STATE-PFM-SEC-004
    transition:
      from: active
      to: revoked
      index: 5
    preconditions: >-
      Use an isolated membership status in active state, with the required
      authority and guard fixtures.
    steps:
      - do: >-
          Revoke the active member and inspect assignment, invitation and queued
          removal records.
        expect: >-
          Read the resulting membership status as revoked; inspect the
          associated records and recorded history for this transition.
        provenance: proposed
        source: &ref_2
          - >-
            services-platform:modules/security/services/invitations/revoke-user.ts
          - >-
            services-platform at ad3f6a65: modules/security/db/users.table.ts;
            services/invitations/accept-invitation.ts, activate-user.ts,
            invite-user.ts, revoke-user.ts, suspend-user.ts,
            resend-invitation.ts, user-deletion.ts;
            services/membership/lockout.ts, last-administrator.ts.
    expected: >-
      Read the resulting membership status as revoked; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_2
    guard_cases: Self-revocation is refused and last-administrator checks pass.
    side_effects_to_verify:
      - >-
        Revoke invitations and assignments, queue disabling, then nudge dispatch
        and submit a flush
    transition_assertion:
      state: revoked
      provenance: proposed
      source: *ref_2
  - id: ATS-PFM-SEC-009-15
    title: 'Membership status: suspended to revoked'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-06
    state_model: STATE-PFM-SEC-004
    transition:
      from: suspended
      to: revoked
      index: 6
    preconditions: >-
      Use an isolated membership status in suspended state, with the required
      authority and guard fixtures.
    steps:
      - do: Revoke the suspended member and inspect queued provider removal.
        expect: >-
          Read the resulting membership status as revoked; inspect the
          associated records and recorded history for this transition.
        provenance: proposed
        source: &ref_3
          - >-
            services-platform:modules/security/services/invitations/revoke-user.ts
          - >-
            services-platform at ad3f6a65: modules/security/db/users.table.ts;
            services/invitations/accept-invitation.ts, activate-user.ts,
            invite-user.ts, revoke-user.ts, suspend-user.ts,
            resend-invitation.ts, user-deletion.ts;
            services/membership/lockout.ts, last-administrator.ts.
    expected: >-
      Read the resulting membership status as revoked; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_3
    guard_cases: The target cannot be the caller’s membership.
    side_effects_to_verify:
      - Revoke invitations and assignments and persist provider-disable work
    transition_assertion:
      state: revoked
      provenance: proposed
      source: *ref_3
  - id: ATS-PFM-SEC-009-16
    title: 'Invitation: live to revoked'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-04
    state_model: STATE-PFM-SEC-005
    transition:
      from: live
      to: revoked
      index: 6
    preconditions: >-
      Use an isolated invitation in live state, with the required authority and
      guard fixtures.
    steps:
      - do: >-
          Suspend or revoke the membership and attempt to use its outstanding
          link.
        expect: >-
          Read the resulting invitation as revoked; inspect the associated
          records and recorded history for this transition.
        provenance: proposed
        source: &ref_4
          - >-
            services-platform:modules/security/services/persistence/invitations.ts:revokeOutstandingInvitations
          - >-
            services-platform at ad3f6a65:
            modules/security/db/user-invitations.table.ts;
            services/persistence/invitations.ts;
            services/invitations/invitation-lifetime.ts, resend-invitation.ts,
            accept-invitation.ts; services/provisioning/provision-user.ts.
    expected: >-
      Read the resulting invitation as revoked; inspect the associated records
      and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_4
    guard_cases: The membership capability controls withdrawal.
    side_effects_to_verify: []
    transition_assertion:
      state: revoked
      provenance: proposed
      source: *ref_4
  - id: ATS-PFM-SEC-009-17
    title: Maintain console memberships with offer and manage an invitation
    category: interaction
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-03
      - BR-CAP-PFM-SEC-009-05
    exercises:
      - CAP-PFM-SEC-009
      - CAP-PFM-SEC-008
    preconditions: A console member has a pending invitation and offered assignments.
    steps: >-
      Revoke the membership, inspect the old offer, and invite the same address
      again.
    expected: >-
      The old offer cannot activate access; reinvitation reuses membership
      identity with a new offer.
    negative: true
    data_needs: >-
      An isolated customer with two workspaces, ordinary and staff identities,
      and records owned by this run.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
        somebody already invited to that wall as already_invited, telling the
        administrator to resend
      - >-
        services-platform:modules/security/services/invitations/suspend-user.ts:reinstateUser
  - id: ATS-PFM-SEC-009-18
    title: Maintain console memberships with assign roles within a controlled scope
    category: interaction
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-07
      - BR-CAP-PFM-SEC-010-07
    exercises:
      - CAP-PFM-SEC-009
      - CAP-PFM-SEC-010
    preconditions: Only one active qualifying customer administrator remains.
    steps: >-
      Try withdrawing the qualifying assignment and suspending its holder; read
      membership and assignments after each refusal.
    expected: >-
      Both operations leave the active administrator and qualifying assignment
      intact.
    negative: false
    data_needs: >-
      An isolated customer with two workspaces, ordinary and staff identities,
      and records owned by this run.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-platform:modules/security/services/membership/lockout.ts:assertNotLockedOut
      - >-
        services-platform:modules/security/tests/integration/services/membership/manage-assignments.integration.spec.js#revokes
        the grant, keeps the row, and audits what was withdrawn
  - id: ATS-PFM-SEC-009-19
    title: >-
      Maintain console memberships with review security changes and refused
      attempts
    category: interaction
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-08
      - BR-CAP-PFM-SEC-011-07
    exercises:
      - CAP-PFM-SEC-009
      - CAP-PFM-SEC-011
    preconditions: A reachable member has a retained history and can be deleted.
    steps: Delete the member and read security history with explanation permission.
    expected: >-
      The successful deletion event remains attributable through the former-user
      identity.
    negative: false
    data_needs: >-
      An isolated customer with two workspaces, ordinary and staff identities,
      and records owned by this run.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/user-deletion.spec.js#refuses
        a delete that would leave the customer with nobody who can administer
        its security
      - >-
        services-platform:modules/security/tests/e2e/users.e2e.spec.js#writes
        the deletion to the security trail, naming who deleted whom
  - id: ATS-PFM-SEC-009-20
    title: Maintain console memberships with administer rhythm staff memberships
    category: interaction
    level: api
    proves:
      - BR-CAP-PFM-SEC-009-07
      - BR-CAP-PFM-SEC-016-06
    exercises:
      - CAP-PFM-SEC-009
      - CAP-PFM-SEC-016
    preconditions: >-
      A customer administrator and a sole Rhythm Administrator each have active
      memberships.
    steps: Attempt a removal that would strand each administration boundary.
    expected: >-
      Each removal is refused within its own boundary without treating one
      administrator as the other boundary’s replacement.
    negative: true
    data_needs: >-
      An isolated customer with two workspaces, ordinary and staff identities,
      and records owned by this run.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-platform:modules/security/services/membership/lockout.ts:assertNotLockedOut
      - >-
        services-platform:modules/security/tests/integration/services/invitations/delete-user.integration.spec.js#refuses
        the delete as the staff guard does, naming its machine code
evidence:
  - >-
    Pinned services-platform references for the structured rows:
    modules/security/tests/e2e/console-users.e2e.spec.js;
    modules/security/tests/e2e/invitations.e2e.spec.js;
    modules/security/tests/e2e/users.e2e.spec.js;
    modules/security/tests/integration/services/dispatch/pool-account-removals.integration.spec.js;
    modules/security/tests/integration/services/invitations/delete-user.integration.spec.js;
    modules/security/tests/integration/services/invitations/revoke-lockout.integration.spec.js;
    modules/security/tests/integration/services/membership/manage-assignments.integration.spec.js;
    modules/security/tests/unit/services/identity/link-cognito-identity.spec.js;
    modules/security/tests/unit/services/invitations/suspend-revoke.spec.js.
  - >-
    Pinned react-console references for the structured rows:
    e2e/security/users.spec.ts.
  - >-
    Testing back-fill reviews the pinned suite assertions, related capabilities,
    domain invariants, state transitions and workflow failure boundaries. New
    unbound rows remain proposed.
coverage:
  rule: 'gap: FND-PFM-SEC-066'
  transition: covered
  validation: covered
  permission: 'gap: FND-PFM-SEC-066'
  edge: 'gap: FND-PFM-SEC-066'
  failure: covered
  boundary: covered
  concurrency: covered
  invariant: covered
  interaction: covered
coverage_notes:
  rule: >-
    No dedicated scenario isolates this category; the coverage finding records
    the missing proof.
  permission: >-
    No dedicated scenario isolates this category; the coverage finding records
    the missing proof.
  edge: >-
    No dedicated scenario isolates this category; the coverage finding records
    the missing proof.
invariant_proofs:
  - scenario: ATS-PFM-SEC-009-06
    proves:
      - INV-DOM-PFM-SEC-003-12
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/services/invitations/suspend-revoke.spec.js#refuses
      a suspension that would leave the customer with no security administrator,
      and suspends nobody
  - scenario: ATS-PFM-SEC-009-07
    proves:
      - INV-DOM-PFM-SEC-003-21
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/users.e2e.spec.js#keeps the
      name behind the id, so every record the person touched can still say who
      it was
  - scenario: ATS-PFM-SEC-009-09
    proves:
      - INV-DOM-PFM-SEC-003-01
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/users.e2e.spec.js#creates a
      portal user of one of the customer’s tenants, and the same e-mail in
      another wall is a different user
  - scenario: ATS-PFM-SEC-009-10
    proves:
      - INV-DOM-PFM-SEC-003-05
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/services/identity/link-cognito-identity.spec.js#never
      links on the request's say-so: the pool user must exist, be enabled, and
      carry the user's verified e-mail
permission_proof_review:
  - key: platform.users.read
    scenario_candidates:
      - ATS-PFM-SEC-009-04
      - ATS-PFM-SEC-009-13
      - ATS-PFM-SEC-009-17
    qualification: >-
      Permission-rule scenarios cover the declared authority boundaries;
      exhaustive operation-by-key combinations remain an executable coverage
      gap.
    gap: FND-PFM-SEC-066
  - key: platform.security-access.manage
    scenario_candidates:
      - ATS-PFM-SEC-009-04
      - ATS-PFM-SEC-009-13
      - ATS-PFM-SEC-009-17
    qualification: >-
      Permission-rule scenarios cover the declared authority boundaries;
      exhaustive operation-by-key combinations remain an executable coverage
      gap.
    gap: FND-PFM-SEC-066
  - key: platform.users.create
    scenario_candidates:
      - ATS-PFM-SEC-009-04
      - ATS-PFM-SEC-009-13
      - ATS-PFM-SEC-009-17
    qualification: >-
      Permission-rule scenarios cover the declared authority boundaries;
      exhaustive operation-by-key combinations remain an executable coverage
      gap.
    gap: FND-PFM-SEC-066
  - key: platform.users.update
    scenario_candidates:
      - ATS-PFM-SEC-009-04
      - ATS-PFM-SEC-009-13
      - ATS-PFM-SEC-009-17
    qualification: >-
      Permission-rule scenarios cover the declared authority boundaries;
      exhaustive operation-by-key combinations remain an executable coverage
      gap.
    gap: FND-PFM-SEC-066
  - key: platform.users.delete
    scenario_candidates:
      - ATS-PFM-SEC-009-04
      - ATS-PFM-SEC-009-13
      - ATS-PFM-SEC-009-17
    qualification: >-
      Permission-rule scenarios cover the declared authority boundaries;
      exhaustive operation-by-key combinations remain an executable coverage
      gap.
    gap: FND-PFM-SEC-066
  - key: platform.users.bulk
    scenario_candidates:
      - ATS-PFM-SEC-009-04
      - ATS-PFM-SEC-009-13
      - ATS-PFM-SEC-009-17
    qualification: >-
      Permission-rule scenarios cover the declared authority boundaries;
      exhaustive operation-by-key combinations remain an executable coverage
      gap.
    gap: FND-PFM-SEC-066
  - key: platform.users.manage-identities
    scenario_candidates:
      - ATS-PFM-SEC-009-04
      - ATS-PFM-SEC-009-13
      - ATS-PFM-SEC-009-17
    qualification: >-
      Permission-rule scenarios cover the declared authority boundaries;
      exhaustive operation-by-key combinations remain an executable coverage
      gap.
    gap: FND-PFM-SEC-066
---
# Maintain console memberships: Capability Test Specification

## Coverage

| Rule | Proved by |
|---|---|
| `BR-CAP-PFM-SEC-009-01` | `ATS-PFM-SEC-009-01` |
| `BR-CAP-PFM-SEC-009-03` | `ATS-PFM-SEC-009-02` |
| `BR-CAP-PFM-SEC-009-04` | `ATS-PFM-SEC-009-03`, `ATS-PFM-SEC-009-12`, `ATS-PFM-SEC-009-16` |
| `BR-CAP-PFM-SEC-009-05` | `ATS-PFM-SEC-009-04`, `ATS-PFM-SEC-009-13`, `ATS-PFM-SEC-009-17` |
| `BR-CAP-PFM-SEC-009-06` | `ATS-PFM-SEC-009-05`, `ATS-PFM-SEC-009-14`, `ATS-PFM-SEC-009-15` |
| `BR-CAP-PFM-SEC-009-07` | `ATS-PFM-SEC-009-06`, `ATS-PFM-SEC-009-18`, `ATS-PFM-SEC-009-20` |
| `BR-CAP-PFM-SEC-009-08` | `ATS-PFM-SEC-009-07`, `ATS-PFM-SEC-009-19` |
| `BR-CAP-PFM-SEC-009-09` | `ATS-PFM-SEC-009-08` |
| `BR-CAP-PFM-SEC-009-10` | `ATS-PFM-SEC-009-09` |
| `BR-CAP-PFM-SEC-009-11` | `ATS-PFM-SEC-009-10` |

The coverage map distinguishes a dedicated scenario from an unresolved category. Proposed scenarios define proof to build; bindings retain the exact assertion references and identify their executable suite.

## Interactions With Related Capabilities

- Offer and manage an invitation: Revoke the membership, inspect the old offer, and invite the same address again.
- Assign roles within a controlled scope: Try withdrawing the qualifying assignment and suspending its holder; read membership and assignments after each refusal.
- Review security changes and refused attempts: Delete the member and read security history with explanation permission.
- Administer Rhythm staff memberships: Attempt a removal that would strand each administration boundary.

