Summary#
When you ship an entity in a capability or a package, the app that imports it can re-open it with
partial entity X { security { … } } and state its own rules. [Authority] is how you say what that is allowed to
do to yours.
The default, which you get by writing nothing, is Narrowable: the app's rules are conjoined with yours. It
may add conditions, deny things you allowed, and scope rows further — and it cannot grant a verb you withheld or
widen a row filter you wrote. A grant that would exceed yours is refused at the line the app's author wrote it on.
Signature#
[Authority(Narrowable)] // the default: the app may only tighten your rules
[Authority(Owned)] // the app may not declare security for this entity at all
[Authority(Extensible)] // the app's rules are ORed with yours — it may grant more than you didIt goes on the entity, beside sealed, and it is capability- and package-authoring surface: an app writing it on
its own entity is refused, because an entity with no consumer has no authority to hand out.
Description#
Why it is a declaration
sealed already lets you say "you may not derive a new type from me" — and says nothing about what happens to
your rules when somebody re-opens your entity. This is the same idea one axis over: sealed generalised from
extensibility to security.
Only you can answer it, because it depends on what you meant. Two capabilities in this platform mean opposite things by the same-looking block:
Osysharp.Locale'sCulturegrantsreadto everyone and withholds every write on purpose — the rows are a reflection of the culture pack declared in source, reconciled from it on every compile, so a row an app created would simply disappear. Nothing an app can say makes writing it meaningful. That isNarrowable(the default).Osysharp.SourceBudget'sUserSourceBudgetgrantsreadof a person's own cap, and the widening is the point: an app whose owners set somebody's cap adds thecreateandupdatethat do it. That isExtensible, declared.
⚠ AND THE TEST IS "CAN AN APP MEAN SOMETHING BY IT?", NOT "WOULD I DO THAT?" The per-user budget —
Osysharp.SourceBudget's UserSourceBudget — shipped Narrowable on the reading that
"a user may see their own cap and may not raise it", and that was the wrong axis: the person is not the app. The
capability ships an override table precisely so an app owner can set somebody's cap, and under Narrowable no
Osy# whatsoever could write it — the only remedy the compiler itself named, a partial entity granting
create, was refused by the next pass. It is Extensible now. A withholding whose only remedy is refused elsewhere is a dead end rather than a gate; if you
find yourself there, the question to ask is whether the thing you are protecting the app FROM is really the app.
What Narrowable actually does
The platform derives a floor from your block: the complement of what you granted, as deny rules decided after
every layer an app's own rules sit in. So the conjunction is exact rather than advisory —
- a verb you never granted is denied outright;
- a verb you granted
where Fis denied on every rowFdoes not match; - a verb you granted
when Wis denied for every callerWdoes not hold for; - and every
denyyou wrote, including a field deny, holds against any rule the app writes.
The app can still narrow further: its own deny rules sit below the floor and take effect normally.
⚠ So an allow in the app's partial admits nobody new. A verb you never granted is refused at compile; a verb
you granted to somebody else is not — partial entity Principal { security { allow update when IsSteward; } } over
a package that lets people update only their own account compiles, and stewards are still refused. osy lint names
that line (security-partial-allow-grants-nothing), says what the package does grant, and offers the ways that do
work: widen a policy the package's grant goes through when the app defines it, keep the data on a record of the
app's own, or — if the package is yours — declare it Extensible.
A type you derive from a Narrowable one inherits the floor. An app's entity Member : User { … } adds fields,
but a grant in its own block cannot reach past the package's: if the package lets a person update only their own row
(allow update where Id == user.Id), the app's desk staff cannot write a field on somebody else's row either —
however the subtype's block reads. Two ways through, and both are ordinary Osy#:
- the package names a policy for it — then the app defines that policy to include its staff, and the floor moves with it. The refusal names the policy when there is one;
- the field lives on a record of the app's own — a
Mentorship { Member Mentee; Member Mentor; }the app grants as it likes, instead of a column on the inherited type. When the write must also change the inherited row, raise an event the package listens to, or call the function it ships for that change, and let it write under its own grants.
What the consuming app is told. A verb you withhold is your decision — orders are records, so a shop package
never grants delete on one — and the app that uses your package is not warned about it for its own sake. It hears
about it only when its own code PERFORMS that verb on your table: a .Delete() on a row of it, a member set outside
an initializer, a new. Then the warning on its use line names the verb and the line that does it, because that
is the one case where the runtime refusal would surprise anybody. A table you ship with no rules at all is different:
nobody chose a posture for it, and the app is always told.
⚠ State each verb in one rule. If you grant a verb through several rules that condition on different things —
one with a when, another with a where — the complement cannot be stated exactly, and the compile is refused
rather than given a floor that is quietly wider than your block.
Owned
Nobody but you states the rules. A partial entity over it is refused by name, and the app is never asked to
declare security for it. This is the marker an organization's own package carries: rules the apps in that
organization inherit and cannot re-state.
Examples#
A package entity whose rules an app may only tighten — the default, so nothing is written:
app Ledger { }
entity Invoice {
[MaxLength(40)] string Number;
[Required] User Customer;
security { allow read where Customer == user; }
}
[Principal] entity User { [MaxLength(200)] string Email; }Saying the other two out loud:
// nobody may re-state these rules — an org package's shape
[Authority(Owned)]
public sealed entity OrgUser { … }
// the rule below is the ownership rule; an app may add a support role on top of it
[Authority(Extensible)]
public sealed entity ChatSession { … }See also#
- security { } — the
security { }block, and stating extra access withpartial entity - capability rows that belong to a user — capability tables whose rows belong to one signed-in user
- secure by default (deny-all) — why a table nobody granted is denied