---
title: "Not every authorization decision is allow or deny"
description: "Not every authorization decision is allow or deny. Some denials are conditional, and the policy can return what would unblock them. Covers Cerbos policy outputs, the conditionNotMet trigger, the AuthZEN ARAP draft profile for requestable denials, and why an approval is an input to a new decision."
author: "Emre Baran"
date: "2026-08-26T10:14:01.033Z"
canonical: "https://www.cerbos.dev/blog/not-every-authorization-decision-is-allow-or-deny"
image: "https://stylish-appliance-1c1cc1c30d.media.strapiapp.com/Not_every_authorization_decision_is_allow_or_deny_35649a7b5c.png"
tags: ["guide","engineering"]
source: "https://www.cerbos.dev/blog/not-every-authorization-decision-is-allow-or-deny"
---

# Not every authorization decision is allow or deny

Most authorization interfaces return a boolean. An enforcement point asks whether a subject may perform an action on a resource, and it gets back true or false. That framing is common to most [authorization models](https://www.cerbos.dev/blog/how-does-authorization-work). It is easy to implement, easy to test, and it covers a large amount of real policy. It does not cover all of it.

Think about a refund above a threshold that needs a manager to sign off on it. A production change that needs a second approver. A bulk export of customer records that needs a fresh authentication factor first. None of these actions are forbidden. Each is not permitted under the conditions holding right now, and in each case a specific, nameable event would change the answer. Collapsing that into false discards the only part of the decision the caller could have acted on.

## **A deny that says nothing pushes authorization policy back into application code**

The distinction that matters is not between allow and deny. It is between a denial that carries what would unblock it and a denial that carries nothing.

When a denial carries nothing, the calling system has to work out why on its own. It has to know that refunds over a certain amount go to a manager, that production changes need a second approver, that exports need step up. That knowledge is policy, and it now lives in application code, duplicated across every service that handles a rejection. Two implementations drift. A threshold moves in the policy and stays stale in the service written first.

When a denial carries what would unblock it, the calling system needs none of that. It reads a structured value off the response and routes to the workflow the policy named. The rule stays in one place, the workflow stays where workflows belong, and the enforcement point holds no policy logic of its own. This is the argument for [externalizing authorization](https://www.cerbos.dev/blog/why-external-authorization) applied to the part of the decision that usually gets thrown away.

## Cerbos policy outputs return a payload alongside the allow or deny

Cerbos rules can carry an `output` block, a [CEL expression](https://www.cerbos.dev/features-benefits-and-use-cases/flexible-policy-conditions-powered-by-google-cel) evaluated during policy evaluation whose result is returned in the API response next to the effect.

```
- name: refund-approval-threshold
  actions: ['issue']
  effect: EFFECT_ALLOW
  roles: ['support_agent']
  condition:
    match:
      expr: R.attr.amount <= P.attr.refundLimit
  output:
    when:
      conditionNotMet: |-
        {"requirement": "manager_approval",
         "limit": P.attr.refundLimit,
         "workflow": "refunds.escalation"}
```

Two triggers exist. `output.when.ruleActivated` fires when the rule matched fully, meaning the action, the roles and any [derived roles](https://docs.cerbos.dev/cerbos/latest/policies/derived_roles.html) matched and the condition was satisfied. `output.when.conditionNotMet` fires when the action and roles matched but the condition evaluated false. The second trigger is the interesting one here, because it is precisely the state where a rule applies to the caller and the caller has not met the bar yet.

The value arrives in an `outputs` array on the result, each entry tagged with `src` naming the policy and the rule that produced it, and `val` holding the expression result.

```json
"outputs": [
  {
    "src": "resource.refund.vdefault#refund-approval-threshold",
    "val": {
      "requirement": "manager_approval",
      "limit": 500,
      "workflow": "refunds.escalation"
    }
  }
]
```

Because the source is tagged, an enforcement point evaluating several rules across several [resource policies](https://docs.cerbos.dev/cerbos/latest/policies/resource_policies.html) can tell which requirement it is looking at. The [outputs](https://docs.cerbos.dev/cerbos/latest/policies/outputs.html) documentation covers the rest, including the caution that heavy use of output expressions costs evaluation time.

## Policy outputs are informational and never change the authorization decision

One constraint shapes every integration built on this mechanism, so state it plainly. An output does not modify the effect. A deny with an output attached is still a deny, and the enforcement point has to honor it.

That is a design choice rather than a gap. If a returned value could soften a denial, the decision would stop being unambiguous and the audit record would stop being a straight answer. Keeping outputs off the decision path means the log says deny, and the reason sits beside it where nobody can mistake it for the answer. [Audit records](https://www.cerbos.dev/blog/why-audit-logs-are-important) stay legible.

The cost lands on the enforcement point, which has to read the output rather than treat the boolean as the whole response. Returning structured data alongside a decision is covered generally in our earlier piece on [context aware](https://www.cerbos.dev/blog/making-application-authorization-context-aware-cerbos-outputs) authorization. What is worth separating out is this narrower use, where the value is not an explanation for a human but an instruction for a machine about which process to start.

## The AuthZEN ARAP profile standardizes the requestable denial

The AuthZEN working group has a draft profile for exactly this shape. ARAP, the [AuthZEN Access Request and Approval Profile](https://openid.github.io/authzen/authzen-access-request-approval-profile-1_0.html), extends the [AuthZEN Authorization API](https://openid.net/specs/authorization-api-1_0.html) so that a decision point can return a denial marked as remediable, carrying where and how to request access.

The wire format is pinned down in a way hand rolled versions never are. The denial carries an access request endpoint, an expiry after which the request can no longer be raised, a binding token tying a later submission back to the evaluation that produced it, and an evaluation identifier for the audit trail. The enforcement point submits to that endpoint, and on approval the service returns an approval object with an identifier, an expiry, and optional proof material.

Be clear about status. ARAP is Standards Track Draft 1, dated July 27, 2026, authored by K. McGuinness. Nothing implements it today and it may change before it stabilizes. The reason to read it now is that it names a shape teams are already building by hand, usually differently in each service. The base Authorization API is a separate document and further along, having reached Final Specification in January 2026\. We have written elsewhere about what it [standardizes](https://www.cerbos.dev/blog/authzen-standards-based-authorization-for-enterprises) and where [Cerbos stands](https://www.cerbos.dev/blog/openid-authzen-is-official-cerbos-is-ready) against it.

## An approval is an input to a new authorization decision, not a permission

The design decision that matters most is what happens after an approver says yes. The enforcement point does not treat the approval as permission. It issues a fresh evaluation carrying the approval as an input. The profile is direct about this. The enforcement point "MUST include the `approval` object unchanged at `context.approval` inside the AuthZEN Authorization API re-evaluation request."

The decision point evaluates that new request against current policy, with the approval as one more attribute. It can still deny. Policy may have changed, the subject's employment status may have changed, risk signals may have moved. Nothing is quietly promoted into a permission that outlives the request that prompted it, and the decision point stays authoritative throughout. NIST describes the same separation in [SP 800-162](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-162.pdf), where the decision point, the enforcement point and the context handler that assembles attributes are distinct components.

In Cerbos terms the approval is another attribute in the request, readable in a [condition](https://docs.cerbos.dev/cerbos/latest/policies/conditions.html) like any other. If it arrives as a signed token, [aux data](https://docs.cerbos.dev/cerbos/latest/configuration/auxdata.html) exposes its claims to the policy without the caller unpacking them first, though the docs are explicit that PDP side verification is a precaution against tampering in flight, not the authoritative check. The rule consuming an approval sits in the same file as the rule that produced the requirement, so both halves get reviewed together.

An approval frequently produces a grant that is delegated and time bounded rather than permanent, a related pattern that deserves separate treatment. The point here is narrower. An approval is evidence presented to a decision, not a decision made elsewhere.

## **Approval workflows belong in the workflow engine, not the policy engine**

Approvals already have a home in most organizations. A ticketing system, an access request tool, a chat based flow, a change management process. The policy's job is to state what is required, not to run the process or know who the approvers are.

That separation is load bearing. Once a policy engine holds approver lists, escalation paths, reminder schedules and request state, it is a workflow engine with a policy language attached, and inherits the availability and durability requirements of one. Keeping [policy design](https://docs.cerbos.dev/cerbos/latest/policies/best_practices.html) free of that state is what makes a decision point cheap to run next to every service.

The two systems meet at attribute lookup. The policy needs to know whether an approval exists and what it covers, and that lookup sits on the request path, which puts latency and cache behavior in scope. [Cerbos Synapse](https://www.cerbos.dev/product-cerbos-synapse) covers this seam with cached data sources in front of the PDP, so an attribute fetch does not become a per request call to a ticketing API.

## Three failure modes to design against when a denial carries a requirement

An output the caller ignores is unenforced policy. The effect is honored, so nothing looks broken, but a requirement the policy stated has no bearing on behavior. On a denial it means a user stuck at a dead end. On an allow carrying an obligation, such as recording the access or prompting for a second factor, it means an obligation nobody performed. Treating outputs as optional decoration is one of the [blind spots](https://www.cerbos.dev/blog/5-authorization-blind-spots-auditors-find-and-how-to-fix-them) that surfaces during an audit rather than during development.

An approval that is cached and reused becomes the standing grant the design was avoiding. If the enforcement point stores the approval object and replays it against later requests, the expiry and the fresh evaluation both stop meaning anything. Approvals should be scoped to the request that prompted them, and the decision point should be able to tell a first presentation from a replay.

A denial that explains itself too well is an information disclosure. "Requires approval from the account owner" tells an unauthorized caller that the resource exists and who owns it. Reason codes need the same review as any other response content, which usually means a stable machine readable code plus a display string chosen for the audience.

## What changes in the policy and what changes in the enforcement point

The change is small in the policy and larger in the enforcement point. In the policy it means writing a `conditionNotMet` output next to any condition a caller could plausibly satisfy later. In the enforcement point it means reading the whole result rather than the effect, and having somewhere to send a request when the policy names one.

What comes out is a system where the rule about who approves what exists once, in a file that can be reviewed and tested, and every service behaves the same way without knowing anything about approvals. The decision stays binary, which keeps the audit trail honest, and the path out of a denial stops being something each team reconstructs from memory.

[**Try Cerbos**](https://hub.cerbos.cloud) to see how this works in practice, or [**book a call**](https://www.cerbos.dev/workshop) to talk through your architecture with the team.

**Go deeper:**

- [How to adopt externalized authorization](https://solutions.cerbos.dev/how-to-adopt-externalized-authorization) (eBook) for a step by step playbook on moving policy out of application code

## FAQ

### What does it mean when an authorization decision is not simply allow or deny?

An authorization decision that is not simply allow or deny is one where the action is not forbidden but is not permitted under the conditions holding right now, and a specific, nameable event would change the answer. A refund above a threshold waiting on manager sign off, a production change waiting on a second approver, and a bulk export waiting on a fresh authentication factor are all examples. The decision itself stays binary in most [authorization models](https://www.cerbos.dev/blog/how-does-authorization-work), but the response can carry what would unblock it instead of returning a bare false.

### What are Cerbos policy outputs and when do they fire?

Cerbos policy outputs are CEL expressions attached to a rule whose result is returned in the Cerbos API response alongside the effect. Two triggers exist. output.when.ruleActivated fires when the rule matched fully, meaning the action, the roles and any [derived roles](https://docs.cerbos.dev/cerbos/latest/policies/derived_roles.html) matched and the condition was satisfied. output.when.conditionNotMet fires when the action and roles matched but the condition evaluated false, which is the state where a rule applies to the caller and the caller has not met the bar yet.

### Can a Cerbos policy output change the authorization decision?

A Cerbos policy output cannot change the authorization decision. An output is informational, so a deny with an output attached is still a deny and the enforcement point has to honor it. That is a deliberate design choice, because if a returned value could soften a denial the decision would stop being unambiguous and the audit record would stop being a straight answer. Keeping outputs off the decision path is what keeps [audit logs](https://www.cerbos.dev/features-benefits-and-use-cases/audit-logs) legible.

### What is the AuthZEN ARAP profile and does anything implement it?

The AuthZEN Access Request and Approval Profile, known as ARAP, is a draft profile that extends the AuthZEN Authorization API so a policy decision point can return a denial marked as remediable, carrying where and how to request access. The denial carries an access request endpoint, an expiry, a binding token tying a later submission back to the evaluation that produced it, and an evaluation identifier for the audit trail. ARAP is [Standards Track Draft 1](https://openid.github.io/authzen/authzen-access-request-approval-profile-1_0.html) dated July 27, 2026, so nothing implements it today and it may change before it stabilizes. The base [AuthZEN Authorization API](https://www.cerbos.dev/blog/authzen-standards-based-authorization-for-enterprises) is further along, having reached Final Specification in January 2026\.

### Does an approval automatically grant access in an approval based authorization flow?

An approval does not automatically grant access in an approval based authorization flow. The enforcement point issues a fresh authorization evaluation carrying the approval as an input, and the policy decision point evaluates that new request against current policy with the approval as one more attribute. It can still deny, because policy may have changed, the subject's employment status may have changed, or risk signals may have moved. Nothing is quietly promoted into a permission that outlives the request that prompted it.

### Should approval workflows live inside the policy engine?

Approval workflows should not live inside the policy engine. The policy's job is to state what is required, not to run the process or know who the approvers are, because once a policy engine holds approver lists, escalation paths, reminder schedules and request state it is a workflow engine with a policy language attached. Approvals already have a home in ticketing systems, access request tools and change management processes, and keeping [policy design](https://docs.cerbos.dev/cerbos/latest/policies/best_practices.html) free of that state is what makes a policy decision point cheap to run next to every service.
