Summary#
A member a deny read withholds from the reader reads as its type's default — never the real value. For a
non-nullable value type that default is a real value of the type (0, 0m, false, DateTime.MinValue, the enum's
zero), so the value alone cannot say whether it was withheld. Ask:
var shown = Security.IsMasked(p.Salary) ? "hidden" : p.Salary.ToString("C");Signature#
bool Security.IsMasked(<row>.<Member>) // the argument is a member read off an entity rowDescription#
What a withheld member reads as. default(T) of its declared type:
| declared | reads as when withheld |
|---|---|
int long double decimal bigdecimal | 0 |
bool | false |
TimeSpan | TimeSpan.Zero |
DateTime DateOnly TimeOnly | MinValue |
Guid | Guid.Empty |
| an enum | its zero — (E)0, the first member of a positional enum |
string, T?, a reference | null |
So p.Salary + 1 is ordinary arithmetic for every reader, and nothing needs a null check the type says is impossible.
The real value never reaches the reader: the default is a constant of the TYPE, decided before the value is read.
Everywhere the reader reads it. In a function body, in a filter, an OrderBy, an aggregate and a projection of the
reader's own query (Pay.Count(p => p.Salary == 0) counts the rows whose salary is withheld from them, exactly as reading
each row would), in a page's slot and in its action. A security { } condition is the one place a member reads its real
value, because a rule judges the data as it is ([[security-entity-security#field-mask-in-queries]]).
Security.IsMasked(p.Salary) answers true when the rules withhold that member from the reader on that row — a
blanket deny read Salary when !IsStaff, or a rule decided per row (deny read Commission where Owner != user) — and
the value read is the stand-in. It is false:
- for a reader the rules do not cover;
- for a member this unit of work assigned, and every member of a row it created — that value is the reader's own;
- for a member the reader wrote and committed, which reads back what they wrote.
The argument must be a member read off a row — Security.IsMasked(p.Salary), Security.IsMasked(o.Customer.Credit).
A string naming the member is not accepted: the compiler checks the member exists.
Writing it back is safe. A reader who may write a member but not read it reads the stand-in, and writing that stand-in (or a null) straight back changes nothing — the stored value survives. The consequence: such a reader cannot set the member to exactly its default, since that write cannot be told from a write-back.
In the browser. A served row names the members withheld from its reader, and the page answers IsMasked from that,
in-process, with no round trip — the same answer the server gives.
Not inside a query clause. Pay.Where(p => !Security.IsMasked(p.Salary)) is refused: read the rows, then ask of each.
Examples#
[Principal] entity Member {
[MaxLength(100)] string Name = "";
security { allow read when IsAuthenticated; }
}
[Role] enum AppRole { Staff }
[RoleGrant] entity RoleGrant { Member Grantee; [Required] AppRole Level; }
policy IsStaff => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Staff);
entity Pay {
[MaxLength(100)] string Label = "";
decimal Salary;
security {
allow read when IsAuthenticated;
deny read Salary when !IsStaff; // everybody else reads 0
}
}
/// What a person may be shown for one pay row.
string SalaryText(Pay p) {
return Security.IsMasked(p.Salary) ? "hidden" : p.Salary.ToString("0.00");
}See also#
- security { } —
deny readand the field mask that withholds the value - Security.MayRead — may THIS person read this row, under their own rules — the same question about a whole row, for another person
- when is who, where is which rows — a mask decided for the reader, or per row
- runas — test it as the reader the mask covers, and as one it does not