Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

[Authorize] on an action

[Authorize] action Name(…) { … } · [Authorize(Policy)] action Name(…) { … }

`[Authorize]` on one action (or method) of a component makes that action members-only, on any page — a public `[AllowAnonymous]` page included. A caller who is not signed in (or, with `[Authorize(Policy)]`, does not satisfy the policy) is not shown the controls that run it, and the server refuses to run it for them. The rest of the page is unchanged. It is ASP.NET's `[Authorize]` on one action of an anonymous controller.

stable1 example compiled by CIuisecurityauthorization

Summary#

A public page often has one or two things only a signed-in visitor may do — Add to order, Message the seller, Book. Put [Authorize] on the action that does it:

[Route("/wines")] [AllowAnonymous]
component Wines() {
  [Authorize] action Add(Wine w) { AddToOrder(w); }             // any signed-in caller
  [Authorize(Sommeliers)] action Taste(Wine w) { BookTasting(w); } // a declared policy
  render { … Button("Add to order", onPress: () => Add(w)); … }
}

Everyone sees the catalogue. Only a signed-in visitor sees the Add to order button, and only someone who satisfies Sommeliers sees Book a tasting. Everyone else sees nothing where those buttons sit — no disabled button, no error.

Signature#

[Authorize] action Name(<params>) { … }            // any signed-in caller
[Authorize(Policy)] action Name(<params>) { … }    // a signed-in caller who satisfies the declared `policy`
[Authorize(Policy)] <Type> Name(<params>) { … }    // the same on a component method
  • Policy — a declared, no-argument policy (see page authorization (policies)). It is checked like any other policy reference: a name that is not declared is a compile error. Because the page decides whether to show the button from the policy, it must be one a control can reflect — the same rule as canSee: (canPress / canEdit / canSee).
  • Bare [Authorize] means "anyone signed in". [Authorize(IsAuthenticated)] says the same thing out loud.

Description#

[Authorize] on an action does two things, and you need both:

  • The page does not show it. Any control whose event runs the action — onPress: Add, onPress: () => Add(w) — is rendered only for a caller who passes the gate. The smallest element that names the action is the one hidden, so a Stack around the button stays. Reading the action's .Pending flag (a busy label) does not count as running it.
  • The server does not run it. Every server call the action makes — and its UnitOfWork.Commit() — is refused for a caller who does not pass the gate, whatever the browser sends. A signed-out visitor is told to sign in; a signed-in caller without the policy is told their account does not satisfy it. Nothing in the action runs.

This is how the members-only component behaves too ([Authorize(Policy)] component … placed on a public page — see page authorization (policies)); the attribute on the action saves moving the button and its action into a component of their own.

The functions the action calls keep their own access rules. [Authorize] on the action decides who may run the action; a caller who skips the page and names a server function directly meets that function's own grant. So a public page can leave AddToOrder gated, as every function is unless marked [AllowAnonymous], and the compiler no longer refuses the page for calling it — the action that calls it is now a signed-in caller's action.

Who may call a gated action#

The server knows a click by the action the control ran. So an action that RUNS a gated one must carry the same [Authorize] — otherwise the gated body would run under the ungated action's name — and this is a compile error. An on mount hook, an on change block or a field cannot carry a gate at all, so they cannot run a gated action; a gated action runs from a control. An inline handler (onPress: () => { Add(w); }) takes the gate of the action it runs; one handler cannot run two actions with different gates.

Examples#

A public wine list. Anyone can browse; a signed-in buyer can add a wine to their order; a sommelier can book a tasting. AddToOrder stays an ordinary, gated server function.

[Principal] entity Buyer {
  [Unique, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}
[Role] enum Level { Buyer, Sommelier }
[RoleGrant] entity Grant {
  [Required("Who the grant is for is required.")] Buyer Holder;
  Level Level;
  security { allow read when IsAuthenticated; }
}
policy Sommeliers => Grant.Any(g => g.Holder == user && g.Level == Level.Sommelier);

entity Wine {
  [Required("A wine needs a name."), MaxLength(80)] string Name;
  security { allow read when IsAuthenticated || IsAnonymous; }
}
entity OrderLine {
  [Required("Which wine is required."), MaxLength(80)] string Wine;
  security { allow read, create when IsAuthenticated; }
}

void AddToOrder(string wine) { new OrderLine { Wine = wine }; UnitOfWork.Commit(); }

[Route("/wines")]
[Title("Wines")]
[AllowAnonymous]
[Render(CSR)]
component Wines() {
  live var wines = Wine.Take(50).ToList();
  [Authorize] action Add(string name) { AddToOrder(name); }
  [Authorize(Sommeliers)] action Taste(string name) { AddToOrder("tasting: " + name); }
  render {
    Stack {
      foreach (var w in wines) {
        Text(w.Name);
        Button("Add to order", onPress: () => Add(w.Name));      // signed-in buyers only
        Button("Book a tasting", onPress: () => Taste(w.Name));  // sommeliers only
      }
    }
  }
}

See also#

Related

page authorization (policies)

A `policy` names a reusable authorization predicate over the current user — e.g. `policy Admins => UserRole.Any(r =>…

routes and pages

How a component becomes a page: it declares a route with `[Route("/catalog/{slug}")]`, and navigating to a matching…

canPress / canEdit / canSee

`canPress`/`canEdit`/`canSee: <policy>` ties a control to a declared `policy`: `canPress` disables a button, `canEdit`…

Session.CurrentUser

`Session.CurrentUser` is the person the app is being shown to, as your own `[Principal]` entity. Read it in a member…