# Explaining your app's security

> Explains your app's declared authorization in plain English — who can read, create, update and delete each entity, which fields are masked, each page's auth posture, and where a page's access does not line up with the data it shows.

<!-- id: local-explaining-your-app · area: local · stability: stable · html: https://osysharp.com/reference/local/explaining-your-app/ -->

## Summary        {#summary}

Turns your app's declared authorization into a plain-English report — **who can do what**, per entity and per page —
so you can reason about access across the whole app without reading a single security predicate. It needs no platform
and no database: it parses and resolves, nothing else.

## Signature      {#signature}

```console
osy explain [path] [--json] [--with-findings]
```

## Description    {#description}

Your app states access as declarations — a `security { }` block on an entity, a `policy`, an `[Authorize]` on a page.
Each is a precise rule, but reading them one file at a time never adds up to the question you actually have: *across
this whole app, who can read the customer's card number? who can create an order? which page is public?* `osy explain`
answers that. It walks every declaration and writes it out in sentences.

Because access here is **declared, not coded**, the translation is exact and repeatable — the same rules always produce
the same words. It is not a summary that guesses; it is a rendering. When a rule uses a shape the report cannot state
plainly, it says so — it prints the shape faithfully and marks it **`(needs review)`** rather than paraphrasing a
meaning it cannot stand behind. A sentence with no such mark is one you can rely on.

The report has three parts.

**The data surface — per entity.** For every table-backed entity, who may **read**, **create**, **update** and
**delete** it, in one line each:

- A role check reads as *"callers who hold the Staff role."* A grant to signed-in users reads as *"any signed-in
  user."* A grant that also admits anonymous visitors reads as *"anyone, including anonymous visitors (public)."*
- A row filter reads as *"only their own rows (where Owner is the caller)."* Membership through a related table reads
  as *"only rows they belong to (via Membership)."* A membership that also demands a **role** on that record names
  it: *"only rows of an Organization where the caller's Membership has Role = Admin"* — and the same rule written
  through a parameterised policy, `where IsAdmin(Organization)`, reads as that same sentence.
- An entity that grants nothing reads as *"no one (denied by default)"* — the honest reading of a `security { }` block
  with no matching rule, or of an entity with no block at all under [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/).

Every condition a rule carries reaches the sentence. When a membership lambda holds a condition the report cannot
phrase — `&& m.Active == true`, say — the whole rule is printed as written and marked `(needs review)`; it is never
narrowed to the weaker sentence with that condition silently gone. A sentence about *less* restriction than the rule
declares is the one thing this report must not produce, because it is read in place of a review.

It also lists **field masking**: a field a rule withholds (*"the CardNumber field is never returned to any reader"*),
and a `[Classification]` mask (*"Ssn is classified Secret — masked from readers below Secret"*), noting where a field
is also redacted from the audit trail.

**The UI surface — per page.** Access is enforced at the data, but each routed page also declares its **own** posture,
and the two are independent — so the report shows both. Per page: its route; whether it is public (`[AllowAnonymous]`),
gated by a policy (`[Authorize(P)]`, stated as the same English the data surface uses), open to any signed-in user
(the secure-by-default rule), or a composable fragment that inherits its host's gate; the entities it reads and writes;
and any policy-aware controls it carries (a button or field that reflects a policy). A composable that touches no data
is left out — the report is about pages that matter to access, not every presentational primitive.

**The coverage cross-check.** The reason to put the two surfaces side by side is to catch where they **disagree**. A
page's gate opens the screen, but the row read is gated separately at the entity, and the two do not inherit — so a
page that lets in a caller the data will deny renders fine and shows an empty list, with no error to point at. The
report flags every such gap: a public page over data that is not anonymously readable, a signed-in page over data only
a role may read, or a page that reads an entity nothing grants a read to. It only flags what it can prove — a page that
plainly admits **more** than the data serves. It does not guess a direction between two different policies.

**Output.** Markdown by default — a document you can read, review, or commit. `--json` writes the whole report as
JSON, the form to hand to a coding agent or to diff between two revisions; the English sentences ride **inside** the
JSON, so the machine-readable surface is also the readable one. `--with-findings` additionally folds in the maturity
linter's security and UI advice inline (see [Checking your app](https://osysharp.com/reference/local/checking-your-app/)) — off by default, because the report's job is
to state what your rules **are**, and the findings are what a reviewer would then **suggest**. Exit is non-zero when
the source did not fully resolve, so the report is partial and says so.

For the machine model of the whole app — types, relations, function effects — see [Understanding your app](https://osysharp.com/reference/local/understanding-your-app/).
For where the app falls short of a production bar, see [Checking your app](https://osysharp.com/reference/local/checking-your-app/).

## Examples       {#examples}

```console
osy explain                    # the plain-English security report, human-readable
osy explain --json             # the same report as JSON (the English rides in it too)
osy explain --with-findings    # also fold in the linter's security / UI advice
```

The data surface for one entity reads like this:

```text
## `Order` — default-deny

- **Read** — Callers who hold the Staff role; or only their own rows (where Owner is the caller).
- **Create** — Callers who hold the Staff role.
- **Update** — No one (denied by default).
- **Delete** — No one (denied by default).

**Fields**

- The CardNumber field is never returned to any reader.
```

An organisation-scoped grant table — writable only by an admin of the row's organisation, whether the rule is
written inline or through a policy:

```osy title="an org-admin write rule, both spellings" test app=local-explain-org-admin
[Role] enum OrgRole { Admin, Member }

[Principal] entity User {
  [Required, MaxLength(200), Unique] string Email;
  security { allow read when IsAuthenticated; }
}
entity Organization { [Required, MaxLength(80)] string Name;
  security { allow read when IsAuthenticated; } }
entity Membership { [Required] User User; [Required] Organization Organization; [Required] OrgRole Role;
  security { allow read when IsAuthenticated; } }

policy IsAdmin(Organization o) => Membership.Any(m => m.User == user && m.Organization == o && m.Role == OrgRole.Admin);

entity RoleGrant {
  [Required] Organization Organization;
  [Required, MaxLength(40)] string Label;
  security {
    allow read when IsAuthenticated;
    allow create, update, delete
      where Membership.Any(m => m.User == user && m.Organization == Organization && m.Role == OrgRole.Admin);
  }
}
entity Invitation {
  [Required] Organization Organization;
  [Required, MaxLength(200)] string Email;
  security {
    allow read when IsAuthenticated;
    allow create, update, delete where IsAdmin(Organization);   // the same rule, named
  }
}
```

reads, for both entities:

```text
- **Read** — Any signed-in user.
- **Create** — Only rows of an Organization where the caller's Membership has Role = Admin.
- **Update** — Only rows of an Organization where the caller's Membership has Role = Admin.
- **Delete** — Only rows of an Organization where the caller's Membership has Role = Admin.
```

A page whose access does not line up with the data it shows is flagged in place:

```text
## `OrdersPage` @ `/orders`

- **Access** — Any signed-in user (secure-by-default; not [AllowAnonymous]).
- **Reads** — Order
  - ⚠ 'OrdersPage' admits any signed-in user, but 'Order's read is gated by a policy/role — a signed-in user outside
    that grant sees an empty 'Order'.
```

## See also       {#see-also}

[Understanding your app](https://osysharp.com/reference/local/understanding-your-app/) — the machine model of the whole app (types, relations, effects).

[Checking your app](https://osysharp.com/reference/local/checking-your-app/) — where the app falls short of a production bar; `--with-findings` folds it in.

[secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — why an entity with no security block grants nothing.

[security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block this report reads.

[principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the policies it translates into English.
