An Acme approval is decided by a permitted person who did not raise it

Table of Contents

1. Summary

An approval request names a kind, and a kind names one permission that its decider must hold. A request may also name parts — Controller, Finance, Market Risk, Operations — and each part names a permission of its own, with an answer order. The demonstration populates each of those five permissions with at least one Acme persona, and separates the two people the control exists for: the desks raise, and Operations, Market Risk, the Controller or Finance decides. A person who raised a request can never decide it, whatever they hold. That single rule is what turns a permission into four eyes.

2. Detail

2.1. The four kinds

Kind Decides what decide_permission_code Holder in Acme Approvals Hold Comment on approve Expires
trading.operational_authorisation a booked trade waiting to be released inbox::approvals:decide_operations Operations 1 yes yes never
refdata.book_change a proposed change to books inbox::approvals:decide_operations Operations 1 no no 14 days
refdata.counterparty_change a proposed change to counterparties, their identifiers and the parties that may trade with them inbox::approvals:decide_operations Operations 1 no no 14 days
marketdata.curve_publish a candidate curve becoming the one the desk prices from inbox::approvals:decide_market_risk Operations, of whom the heads of market risk 1 yes no 14 days
iam.role_grant a person asking to be given a role iam::roles:assign TenantAdmin 1 no no 14 days

An operational authorisation does not expire and requires a comment on approval, because an unreleased trade is a risk a timeout must not hide and the decision has to be readable afterwards. A role request carries a review window, because an administrator who never answers asks the member to wait forever. A curve publish allows a hold, because the approver's third answer is "not yet", and holds a review window of its own: a candidate nobody answered leaves the desk pricing from the published curve, which is the failure that costs least.

2.2. The four parts

A part is what a kind asks for when one function's sign-off does not imply another's. A request that names parts is decided by part instead of by count.

Part Answers decide_permission_code Answer order Holder in Acme
controller the first independent check on a new book inbox::approvals:decide_controller 1, before the rest Controller: 4 personas
finance the accounting identity, the ledger feed, closing a book inbox::approvals:decide_finance 2, in parallel Finance: 4 personas
market_risk classification, capital treatment, agreements, conventions inbox::approvals:decide_market_risk 2, in parallel Operations: 16 personas, of whom the heads of market risk
operations access, memberships, links, allowed currencies and products inbox::approvals:decide_operations 2, in parallel Operations: 16 personas

The controller answers first because a desk must not be able to authorise its own new book; the other three answer together, because each approves its own contribution and no one function's sign-off implies another's. Book types and the controls on each is where those four answers come from. The platform seeds the routing as policies keyed by the field a change touches, and the policies follow the same division: a book's functional currency, GL account reference, cost centre, owner unit and ledger feed go to Finance, its regulatory type, purpose, rates centre and sweepable flag to Market Risk, and its status, portfolios and memberships to Operations. An opening is the one act the policy names by operation rather than by field, because it is the act the controller checks: the seeded book.create policy names the controller for it, and a change to a live book asks for the three field-owning parts and never for the controller.

2.3. Who is who in the demonstration

The demonstration needed two functions the role catalogue had nobody for. The partition of the four parts named a Controller and a Finance decider, and no role held either code, so a book change could only be answered by a tenant administrator. Both roles now exist and both are staffed, at the group and in each office.

Function Persona Role Decide permission
the group's controller Fiona Ashdown, Group Financial Controller Controller decide_controller
London's controller Terry Freeman, Head of Financial Control Controller decide_controller
New York's controller William Carson, Head of Financial Control Controller decide_controller
Hong Kong's controller Suet Ying Wong, Head of Financial Control Controller decide_controller
the group's finance Graham Pierce, Group Finance Director Finance decide_finance
London's finance Beth Clark, Head of Finance Finance decide_finance
New York's finance Rachel Byrd, Head of Finance Finance decide_finance
Hong Kong's finance Hoi Yan Lee, Head of Finance Finance decide_finance
each office's operations the head of middle office, and the office heads Operations decide_operations
each office's market risk the head of market risk Operations decide_market_risk
a role request Tenant Admin, the account provisioning creates TenantAdmin iam::roles:assign

The controller and finance roles report into the independent lines and not into the business they check: financial control reports to the country head beside risk, and finance reports to the COO beside the middle office.

2.4. The requester-not-decider rule

acme_approval_request_and_decide.png

Figure 1: The Acme demonstration: a desk raises through the owning component's operation, the decider reads a queue filtered by the kind's permission, and the decision is refused when the person who asked is the person answering.

Source: acme_approval_request_and_decide.puml. Regenerate with plantuml -tpng doc/knowledge/demo/acme_approval_request_and_decide.puml.

The rule is enforced three times, at three levels, and each is worth knowing.

  1. The queue. The list of requests a person may decide is filtered by the permission each kind names, and by the permission each of a request's parts names while that part's turn has come, so a Controller or Finance persona finds a book change in the queue when their part is the one waiting. The person's own requests are left out by the same statement that pages the rest, so the total the page reports is the number of rows the pages walk. A person does not see their own request in their queue at all.
  2. The decision. The decisions table refuses a decision whose decider is the request's raiser, and the service answers four_eyes with the words "You asked for this request, so someone else decides it."
  3. One decision per person. A request that needs several approvals, or several parts, takes one decision from each person, so a person holding two of the deciding roles still answers once.

What the demonstration showed, on a database recreated and provisioned from the seed alone:

Who What they tried What happened
a junior trader, who raised four authorisations read the queue 0 requests: the role holds no decide code
the same trader approve a request of their own refused, not_a_decider
the head of middle office read the queue 60 waiting authorisations
the head of middle office approve an authorisation raised by the group treasurer approved; the request moved to approved and the kind's apply ran
the head of middle office approve a book change the same person had raised refused, four_eyes: "You asked for this request, so someone else decides it."
the head of financial control open a book change from the notice, then approve the controller part approved; the request stays waiting for the other parts
the group finance director approve the finance part approved
the head of market risk approve the market risk part approved
a trader holding the curve raise a publish request naming the curve configuration and the as-of raised, waiting; the subject is recorded against the request
the same trader publish the approved curve as of another instant refused: the message names the approved subject and the attempted one
the same trader publish the approved subject, then publish it again the first publish records the publisher and the instant; the second is refused and names both
the tenant administrator read the queue 63 waiting requests, including the role request a member raised

2.5. What a person still cannot do, and what that leaves undecided

The journeys and the seeds disagreed in four places. Each is now closed, and this page records the choice it took for each.

  • The controller part is asked for by an operation, not by a field. The approval policies routed a changed field to Finance, Market Risk or Operations, and the controller answers only an opening, so no seeded row produced a controller part. The policy model now names create as an operation beside put and delete: the operation is the act a change performs, and opening a row is an act of its own. A seeded policy, book.create, names the controller for that act with no field, because the opening check is of the act and not of a column the act writes. A book change handler treats a put of a book that does not exist as both acts, so an opening request names the controller beside Finance, Market Risk and Operations, and a change to a live book names the three and never the controller. The controller keeps answer order 1, so it answers first and the other three wait for it.
  • A market data publish is a request that names its subject. The kind marketdata.curve_publish is seeded with the Market Risk decide permission, one approval, a hold and a two-week window, and the publish is gated: marketdata.v1.ops.publish_curve raises the request when it is called with none, and publishes only when it is called with an approved one. The request also names its subject, as the kind's detail row: the curve configuration, the as-of instant the bootstrap reads the market at, and the party the configuration belongs to. The gate compares the publish with that subject, so a publish of another curve, another as-of or another party is refused with a message that names both. The approved subject is published once: the apply stamps the same row with the publisher and the instant, so a second attempt is refused with the words that name who moved the set and when. What the request and its record are, what they hold, and why they are one row rather than two is ores.marketdata.curve_publish.
  • A tenant-wide change is decided by the tenant's Market Risk. Decide a convention change named Market Risk as the decider of a tenant kind and did not say which party's Market Risk answers for the whole tenant. The rule, now written into that journey, is that the tenant's Market Risk decides and no party's: a tenant kind belongs to no party, a permission is held by an account in a tenant rather than by a party, and the queue a decider works from is the tenant's. The demonstration follows the rule rather than contradicting it: the group's head of market risk is the person it intends to answer, and any other account of the tenant that holds inbox::approvals:decide_market_risk sees the same request and may answer it first, which the record then names. The party an answering account works in decides nothing about it.
  • A hold takes a reason from a lookup. The hold reason lookup is seeded with the reasons the operational authorisation queue names — the questions an operator waits on, the three handover markers between regional teams, and the four exception flags that cause a hold — and the decide operation refuses a hold whose code the lookup does not hold, with the codes it does. A hold row taken before the lookup existed keeps its comment and carries no code, and still reads.

3. What this note does not cover

The permissions the roles bundle, and which persona holds which role, are the permission matrix. The record itself, its fields and its lifecycle belong to Approval requests.

4. See also

Emacs 29.3 (Org mode 9.6.15)