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
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.
- 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.
- The decision. The decisions table refuses a decision whose decider is the
request's raiser, and the service answers
four_eyeswith the words "You asked for this request, so someone else decides it." - 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 aputof 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_publishis 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_curveraises 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_risksees 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
- The Acme demonstration's market data and its approvals — the structure note that orders this cluster.
- Approval requests — completes: the record, the queue and the four-eyes rule in general.
- Book types and the controls on each — answers: why four functions answer a book change, and in that order.
- Operational Authorisation — completes: why an unreleased trade is a control, and why the decision is metadata.
- Operational Authorisation Queue — exemplifies: the screen the operations decision belongs on.
- A persona may act because of its role, and no other reason — completes: where each decide permission reaches a person.
- Ask for more access — exemplifies: the role request whose decider is the administrator.
- Decide a convention change — answers: the market-risk decision this demonstration uses for the third part.
- Decide a publish — contradicts: a journey that waits on a kind the platform does not seed.
- Knowledge — the index of every knowledge hub.