Summary#
A function runs as the person who called it, so it reads what they may read. That is right almost everywhere — and wrong for a figure meant for everyone. If each gardener may read only their own harvests, a function that totals a plot's harvests answers each caller a different number: the total of THEIR rows.
allow read via PlotTotal; names the function that computes the shared figure:
- its own reads of this entity see every row — no row rule, no field mask;
- it answers a value, never a row — the compiler checks the return type;
- it writes nothing itself, runs on the server, and is still called as its caller (its
[Authorize]still holds); - nothing else changes — a helper it calls, the page that shows the figure, every other read of the entity.
Signature#
security {
allow read when IsAuthenticated where Grower == user; // each person reads their own rows
allow read via PlotTotal, PlotTotals; // …and these two compute over all of them
}It names functions, not strings — a misspelt name does not compile — and as many as the entity has figures.
Description#
What does the function see?#
Every row of the entity the clause is written on, with every field, through any read its own code makes: a
collection (plot.Harvests.Sum(…)), a query (Harvest.Where(…)), a LINQ chain, a lambda, a foreach, a local
function declared inside it. That is all of "its own code".
| the read | sees |
|---|---|
the function's own plot.Harvests.Count() | every harvest on the plot |
the function's own Harvest.Where(h => h.Plot == plot) | every harvest on the plot |
| a lambda or a local function inside the function | every row — it is the function's own code |
a field a deny read masks, read by the function's own code | the stored value |
| a function it calls (a helper) | only what the caller may read |
| the same read anywhere else — before the call, after it, on the page | only what the caller may read |
| another entity read by the function | judged by that entity's rules, as anywhere |
A privacy restriction the app or the platform places on a field is not a read rule, and stays.
What may it answer?#
A value: a number, a string, a bool, a DateTime, a Guid, an enum; a class of yours whose fields are values; a
List, an array or a Dictionary of those, nested as deep as you like. A list of totals per kind is the usual shape.
An entity, an entity reference, a collection of rows, object, or a class with any of those inside is refused, and
the error names the path to the row — `Report.Detail.Rows[…]` is a `Harvest` row — so a row read past the rules
can never leave the function. A function that answers nothing (void) is refused too: its reads would have no purpose.
Why may it not write?#
Because it runs in a unit of work of its own, opened for its reads and thrown away when it returns — only its answer comes back. A write in its own code is refused at compile time. A helper it calls may write, as the caller, under the caller's rules: the helper is not elevated.
A row it hands to a helper#
A helper runs as the caller, so a row the function passes to it is the caller's row or nothing: it is read again as
the caller, and one the caller may not read is refused exactly as any call with an unreadable row is
(NotFoundException). Compute inside the function, and hand helpers values.
What it reads is what is stored#
The function reads saved rows. If the caller's unit of work holds an unsaved row of the entity — a harvest just created, an edit not yet committed — the call is refused and says so, rather than silently leaving the change out of the figure. Commit first.
It also cannot pause: a read-via function may not wait on a person or a timer.
Where does it run?#
On the server, always — a read the browser made would be the caller's own. A page calls it like any
server function; live var total = PlotTotal(plot); is asked of the server, and asked again when a row it read
changes.
Who may write it?#
The entity's owner: the package that declares it, naming its own top-level functions. A partial that adds to
another package's entity cannot name one, and neither can a function from another package.
The warning: a function that can be asked about one row#
The compiler makes sure the answer holds no row. It cannot know WHICH rows the answer is about. bool HasPicked(Plot plot, Gardener who) that filters h.Grower == who answers, for whoever calls it, a question about one person's
private rows. The lint security-read-via-asks-about-one-row warns when the function narrows the rows by its own
PARAMETER on a field the entity's read rules protect (one its where narrows on, or one a deny read masks).
Narrowing by the plot, a constant or user does not warn. When answering about one row is exactly the point, say so
on the function: [SuppressWarning("security-read-via-asks-about-one-row")].
What osy explain says#
Beside who may read the entity's rows, a line per function: `PlotTotal` may read every Harvest to compute its answer (it returns a number, never a row) — its own reads only, never a function it calls. osy model --json
lists the functions under the entity's security.readVia.
Examples#
A plot's total, the same for every gardener#
Each gardener reads only their own harvests; the plot's total and the totals per crop count everybody's — and the
weights a deny read hides from everyone are summed inside the function.
[Principal] entity Gardener {
[Unique, MaxLength(80)] string Name;
security { allow read when IsAuthenticated; }
}
enum Crop { Beans, Squash, Herbs }
entity Plot {
[Required, MaxLength(80)] string Label;
[ForeignKey(Plot)] Harvest[] Harvests;
security { allow read when IsAuthenticated; }
}
entity Harvest {
[Required] Plot Plot;
Gardener Grower;
Crop Crop;
int Grams;
security {
allow read, create when IsAuthenticated where Grower == user;
deny read Grams;
allow read via PlotTotal, PlotTotals;
}
}
class CropTotal {
public Crop Crop;
public int Grams;
}
/// The plot's whole harvest, in grams.
int PlotTotal(Plot plot) => plot.Harvests.Sum(h => h.Grams);
/// One total per crop — a List of a class of values.
List<CropTotal> PlotTotals(Plot plot) {
var totals = new List<CropTotal>();
foreach (var crop in new[] { Crop.Beans, Crop.Squash, Crop.Herbs })
totals.Add(new CropTotal { Crop = crop, Grams = plot.Harvests.Where(h => h.Crop == crop).Sum(h => h.Grams) });
return totals;
}
[TestFixture]
void Season() {
var ida = new Gardener { Name = "Ida" };
var oto = new Gardener { Name = "Oto" };
var plot = new Plot { Label = "North bed" };
new Harvest { Plot = plot, Grower = ida, Crop = Crop.Beans, Grams = 400 };
new Harvest { Plot = plot, Grower = oto, Crop = Crop.Beans, Grams = 250 };
new Harvest { Plot = plot, Grower = oto, Crop = Crop.Squash, Grams = 900 };
}
principal Ida => Gardener.Single(g => g.Name == "Ida");
[Test(Season)]
[runas(Ida)]
void The_total_counts_every_harvest_and_a_read_still_sees_only_her_own() {
var plot = Plot.Single();
Assert.Equal(1550, PlotTotal(plot));
Assert.Equal(650, PlotTotals(plot).Single(t => t.Crop == Crop.Beans).Grams);
Assert.Equal(0, PlotTotals(plot).Single(t => t.Crop == Crop.Herbs).Grams);
Assert.Equal(1, Harvest.Count()); // her own row — the function changed no other read
}Refused: an answer that could carry a row#
class Shelf { public string Label; public List<Harvest> Rows; }
Shelf Leaky(Plot plot) => new Shelf { Label = plot.Label, Rows = plot.Harvests.ToList() };
// error: `Leaky` reads every Harvest, so what it returns must be a value that can hold no row —
// and `Shelf.Rows[…]` is a `Harvest` row. …See also#
- security { } — the
security { }block this clause belongs to - deny … except via — the other way a function is named in it: the only functions that may make a write
- What may go past the caller's own rights — and what never does — every declared route past the caller's own rules, this one among them
- Security.Authorize — does THIS person satisfy a policy — who may call the function at all