Skip to content
bitscrafter

Entitlement Assistant · Authorization and Security

  • Lab
  • Cloudflare
  • Authorization
  • AI Agents

This part is the security perspective, applied across the system: how a decision is made, which policies constrain it, the principles the design follows to keep the model from deciding anything, the chain that records every change and nearly every decision on customer data, and the test that shows the rule holds.

Authorization and security: its sections around it, in reading order

  1. Decision point — five stages; the most restrictive wins
  2. Policies — in YAML, their conditions in CEL
  3. Principles — each held by a mechanism, not the model
  4. Audit chain — changes and decisions, chained
  5. Architecture test — every data tool and API endpoint, run without permissions
  6. Sign-in — a session token that carries identity alone
  7. Abuse limits — the frames, the messages, the model calls

1. Decision point

One decision point answers, on every request, the question: may this principal — usually the representative — do this action on this subject account, in this context? It runs five stages in order — principal & subject account, principal access, contact access, entitlement and policy; the code calls the first two account and access — and combines their findings, the most restrictive winning: Deny over Conditional over Allow. Only the principal & subject account stage may stop the chain early; every other stage adds its findings (Figure 29).

The decision point's pipeline: its question, then five stages — principal & subject account, principal access, contact access, entitlement and policy — each with what it checks, their findings combined, Deny over Conditional over Allow

Figure 29 — The decision point's pipeline

Roles give permissions; policies only constrain. A principal with no role granting the permission is denied in the principal access stage — default deny lives there — and no policy can grant what no role gave. The principal & subject account stage refuses a principal whose account is on hold or whose identity is inactive, and a subject account on hold but to read it or its audit, cancel a request, or handle a case about it; contact access checks that the contact is not on hold and may ask for the change on the subject’s behalf; the entitlement stage checks the subject’s subscription covers it.

Each stage answers with findings, and a finding is more than a yes or a no. It names its stage, a reason code and an outcome, with the facts that explain it and any prerequisite that would fulfil it; the decision keeps every finding, not only the deciding one, with the policy set’s version. That is what lets an Allow say which role granted it, and a refusal say every reason at once rather than the first.

The roles are those of the standard architecture of authorization, which XACML named. The use cases are the enforcement points; the authorization subsystem is the decision point and its information point; the policy files and their registration are the administration point; and the policy set bundled into the Worker is the retrieval point.

The authorization roles, and where each stands
rolein the Entitlement Assistant
policy enforcement pointevery use case: authorize, execute, audit
policy decision pointthe authorization subsystem’s decide
policy information pointthe authorization subsystem, reading the other subsystems
policy administration pointthe policy files and their registration
policy retrieval pointthe policy set bundled into the Worker

Who learns what from a refusal is a decision of its own. A refusal that says “denied” confirms that the thing exists; so the reasons go only to a principal who holds account.read and was stopped by policies alone — a legitimate representative who needs to escalate — and anyone else gets a not-found, indistinguishable from an account that does not exist. Existence is disclosed only to someone with a reason to know it.

2. Policies

The policies are written in YAML, their conditions in CEL. Each names the actions it governs, when it applies, what it requires, and its outcome when unmet — Deny, or Conditional with the prerequisite that would fulfil it. The set is registered with a version, and every recorded decision names the version it used.

A Conditional outcome is a refusal with a way through. The policy names its prerequisite — for a long grant, an approval by someone holding approval.decide — and the decision carries it; the use case then creates the approval. When the workflow decides again after the approval, the context says the approval was granted, the policy’s requirement is met, and the same set of policies answers Allow.

The engine is the project’s own, behind a port. The project records Cedar as the replacement it would try in production, behind the same port, with the 35 cases as the suite it must pass; and it lists what a real policy practice adds — who may propose and approve a change to the set, a new set evaluated beside the active one before it is activated, and which policies fire, how often, and for whom.

2.1 The eleven policies

2.1 The eleven policies

Five of them govern changing a subscription, a feature not built; their test cases run all the same.

policyoutcome unmetwhat it requires
grants-up-to-14-daysConditionalgrants longer than 14 days need a supervisor’s approval
grants-up-to-90-daysDenygrants last at most 90 days
grant-quota-up-to-100Denygrants add at most 100 to a quota at any time, counted together
no-self-approvalDenynobody approves a change they requested or confirmed
enterprise-accounts-need-level-2DenyEnterprise accounts need job level 2 or a supervisor
contact-is-authenticatedDenya change the customer asks for needs the contact authenticated
cancel-needs-contact-or-approvalConditionala cancellation needs the customer’s request or a supervisor’s approval
upgrade-beyond-pro-needs-contact-or-approvalConditionalan upgrade beyond Pro needs the customer’s request or an approval
downgrade-fits-seatsDenya plan change must leave seats for the members holding a role
trial-coverage-up-to-14-daysDenytrial coverage lasts at most 14 days
courtesy-up-to-14-daysDenycourtesy coverage lasts at most 14 days

Thirty-five test cases hold the set: three each for most policies, five for the Enterprise rule, four for upgrades and two for the 90-day limit, each run through the policy engine as the Worker runs it.

2.2 Two policies, whole

2.2 Two policies, whole

The policy a long grant meets, and the one a supervisor’s approval meets. The first is Conditional, naming its prerequisite; the second is a plain Deny.

policy_key: grants-up-to-14-days
description: Grants longer than 14 days need a supervisor's approval.
governed_actions:
  - grant.create
applies_when: "true"
requires: "context.duration_days <= 14 || context.approval_granted"
unmet_outcome: Conditional
prerequisites:
  - prerequisite_kind: Approval
    fulfilled_by_permission: approval.decide
policy_key: no-self-approval
description: Nobody approves a change they requested or confirmed.
governed_actions:
  - approval.decide
applies_when: "true"
requires: "principal.account_id != context.requester_account_id && principal.account_id != context.confirmer_account_id"
unmet_outcome: Deny

3. Principles

Each principle below is held by a mechanism, not by the model’s good behaviour, and each has its record in the project.

The principles followed, and the mechanism holding each
principleits mechanism
the model proposes, never authorizestools only read or prepare; a person’s click confirms; a supervisor approves
the principal is the session’sreloaded from the database on every request and every message
every text has its provenancestored names quoted «…» and trimmed; events facts, never instructions
text renders as textthe UI renders no markup; script-src 'self'
every change and upgrade checks Originexactly the application’s Origin, no CORS
refusals reveal nothingreasons only to a holder of account.read stopped by policies alone; otherwise an unknown account’s answer
least privilege in the databasethe application’s role has no DELETE; a record of the chain no UPDATE
nearly every decision on customer data recordedthe audit chain, in the action’s transaction

The Origin check is what keeps another site from acting with a representative’s session. A browser sends the session’s cookie with any request to the application, whichever page made it; the application refuses every change and every WebSocket upgrade whose Origin is not exactly its own, so a page elsewhere can make the browser send a request but not have it obeyed.

A stored name is the one text a customer controls that reaches the model inside the server’s own words, so it is quoted in guillemets, its control characters and look-alike quotation marks stripped, and cut at 80 characters. The model is told that what arrives so is a name, and that the case’s events are what happened, never what to do.

The model’s texts that are stored — a draft summary, a change’s reason — never enter the audit chain, whose detail keeps no free text. A case summary may not contain an email address, whether the assistant drafts it or the representative submits it; names are not caught, and the representative reviews the summary before closing.

4. Audit chain

Every change, and nearly every decision on customer data, is recorded in a chain. A record holds its id, its sequence number, its actor, the action, its outcome, its reasons, its detail, the policy set’s version and the previous record’s hash; its own hash is an HMAC-SHA-256, under a key the record names, of its canonical JSON by RFC 8785. For anyone without the key, altering, removing or reordering any record breaks every hash after it (Figure 30).

The audit chain: each record holding the previous hash, its own hash an HMAC of its canonical JSON, the chain's head locked while a record joins

Figure 30 — The audit chain

A record is written in the action’s own transaction, so no action commits without its record. One exception is recorded as such: a refused confirmation’s record is written in a transaction of its own, since the refusal rolls the action back. Not every decision is recorded: an allowed listing of the approvals is not, and explaining a person’s access audits the representative’s read of the account, not the decision about the person. The chain holds no personal data — ids, codes, roles and keyed hashes, never a name or an email — so it can be kept while a person’s details are erased.

The chain has a cost and a limit, both recorded. Every read writes a record under one lock on the chain’s head, so the head serializes the writes and, under load, would limit throughput. And a keyed chain resists rewriting only while its key is safe: the project lists checkpoints published where database writers cannot reach, signed so that auditors can verify them with a public key, as what production adds.

4.1 A record of the chain

4.1 A record of the chain

The record’s fields, as the hash covers them.

fieldwhat it holds
record id, sequence numberits id; its place in the chain
detailchange parameters, the authentication’s kind and method, keyed hashes of queries
occurred atthe instant, to the microsecond
actor, session, tracewho acted, in which session, under which trace
action, outcomewhat was decided or done: Allow, Conditional, Deny, Succeeded or Failed
policy set versionthe policies in force
subject, case, changesoft references to what it concerns
participants, reasonsthe accounts it names with their roles; its reason codes
hash key id, previous hashthe key of its HMAC; the record before it

5. Architecture test

The principle has a guard of its own. The project’s architecture test runs every tool and API endpoint that reaches data — eight tools, thirteen API endpoints — as Drew, an employee who holds none of the support permissions, so every decision refuses. Each must answer a refusal, or a view with what Drew may not read left out, that names no customer data, and leave a Deny record by Drew — proof that the decision point was asked. A tool or API endpoint the test does not list fails it.

The eight tools that reach data and the thirteen data API endpoints are covered. The exemptions are named with their reasons: draft_case_summary, which reads no data and stores nothing; the four API endpoints of a person’s own session; and the agent’s WebSocket upgrade, whose every tool is covered here.

Drew is chosen for what Drew lacks. A finance analyst holds none of the support permissions, so every decision for Drew refuses; a tool or an API endpoint that answered with data could only have done so without asking. The application lists its API endpoints for the test, so one added later and not covered fails it the day it is added.

The test found something: refusing to list the approvals was not recorded. Every decision is meant to be audited, so a refused listing now is; an allowed one is not, since the panel lists every 30 seconds and each approval it shows is decided again as a read of its account.

5.1 Its mutations

5.1 Its mutations

The test was itself tested, by breaking the code it guards.

A use case that never asks the decision point is caught: its tool returns data and leaves no Deny record. Removing only one of a read’s two decisions is not caught, and the project says so: every read is decided twice — once as the case’s action, once as the account’s read — so one decision removed still leaves the other refusing. That is the defense in depth the test shows, stated rather than hidden.

6. Sign-in

Employees sign in to the application itself. The session is a token signed with ES256 that carries identity alone: it lives 30 minutes, is renewed only past half its life, and no renewal reaches beyond eight hours from the sign-in. An unknown or suspended employee receives the same answer as a wrong password, so the answer reveals nothing.

The application signs its employees in itself, rather than behind Cloudflare Access, so the demo’s reviewers need no organization of their own. The token travels in a cookie scripts cannot read, marked HttpOnly, Secure and SameSite=Strict, and that cookie reaches the WebSocket upgrade, which a browser cannot give a header. No store holds sessions; the price is that a token cannot be revoked before it expires, which is why it lives 30 minutes.

A renewal is decided too: an employee placed on hold, or no longer active, is refused a new token, so a session ends at its next renewal at the latest.

Passwords are hashed with PBKDF2-SHA256 at 100,000 iterations, the most Workers’ WebCrypto allows, below the 600,000 current guidance recommends; the project lists a memory-hard algorithm among what production would add. Sign-in attempts are limited to ten a minute from one address.

The session token is a bearer token: whoever holds it acts, until it expires. The project lists binding it to the client — so a stolen token is worthless elsewhere — and rotating the signing key with key ids among what production would add. The principal it names is still reloaded on every request, so a token of an employee suspended since it was issued acts no more.

7. Abuse limits

The demo is public and its model costs money, so it caps both. A frame over 16 KiB closes the connection; one person may send 20 messages a minute and 40 a day; everyone together may use about 100 model calls a day. A turn has five steps at most, 1,024 tokens a call, and the last 20 messages of the conversation; a budget of zero turns the assistant off.

Each limit protects something different. The frame’s size and the minute’s rate protect the agent from being flooded; the day’s messages share the model fairly among people; the day’s calls cap what the demo costs; the turn’s steps and tokens keep one question from running on; the sign-in limit slows guessing at a password. Past any of them, the answer says which, and the case’s records stay untouched.

The daily allowance is derived, not guessed. A budget of Workers AI’s neurons a day, divided by an estimate of the neurons a call uses, gives the model calls a day; each person may use 40 % of them. When it is spent, the chat says so until midnight UTC, and nothing but the assistant stops.

References

  • XACML 3.0 — the standard that named the enforcement, decision, information, administration and retrieval points.
  • CEL — the Common Expression Language the policies’ conditions are written in.
  • RFC 8785 — the JSON Canonicalization Scheme.
  • OWASP’s password storage guidance.