Summary#
A kit is one capability — its data, the rules over it, its pages — that any app can use with one use line. It names
nothing of the app: where it needs the app's answer (who manages it, who may see whose rows) it asks with a policy.
Description#
What does osy kits new make?#
osy kits new Lockers writes kits/lockers/: package.osy, model/ and tests/ — an app of its own that uses
only the kit. osy test kits/lockers/tests runs it; use <Owner>.Lockers@0; uses it. See package.osy.
How does a kit ask the app who may do something?#
policy ManagesLockers; — every app must answer. virtual policy ManagesLockers => false; — nobody, until the app
says. A parameterised one's default must READ its parameter (virtual policy SeesLockersOf(Principal holder) => holder == user || ManagesLockers;); one that ignores it is refused in a where, because it never reads the row.
See [[security-naming-a-policy#supplied-default]].
How do I make a field only my function changes?#
deny update Left except via Spend;, with [Server, Authorize(P)] on Spend. The deny steps aside only for
Spend's own writes — the caller still needs an allow update Left — and the kit's own workflow, running as the
system, needs neither. See deny … except via.
How does someone change a row that belongs to another person?#
Through the function the deny names — an allow … where Owner == user opens nothing past it; who may call it is its
[Authorize]. Work that must see rows the caller cannot belongs in the kit's workflow, which runs as the system. See
What may go past the caller's own rights — and what never does.
My kit calls another kit's guarded function — who may?#
Its [Authorize] is checked at every call, nested ones too. When a step must happen whoever asks, make the request a
row and let its workflow's enter take the step — as the system, in the same commit. See enter and exit (a state's arrival and departure hooks).
How do I tell the app something happened?#
Declare [Event("…")] public sealed class X { … }, Raise(new X { … }) in your function; the app hears it with
[On] void H(X e), after the commit. See [Event] — what happens to a record, declared once and [On] — hear an event, after the commit that raised it.
What can the app configure?#
A class marked [AppSetting("Name")], filled by app.Name = new … { … };, read with App.Setting<T>() on the server;
a member typed as a class with virtual methods lets the app override behaviour. See settings a package asks its app for (app settings).
How does the app plug in a rule of its own?#
Declare public interface IShiftRule { string? Refusal(Shift s, Principal p); } and ask every
App.Implementations<IShiftRule>(): the app's classes are found, never wired. When another kit can answer the same
question (a qualifications kit for a shift's requirements), ship a bridge — completes <Kit>@0, <OtherKit>@0; with the one
class — which arrives by itself in an app using both (Bridges — a package that arrives when an app uses the two it completes). A bridge may also hear one kit's event and
call the other's guarded function ([On] void Grant(PurchasePaid e) { GrantCredits(…); }): a handler runs with the
app's authority.
What happens when an app derives from my entity?#
entity Employee : User is its own type: your grants do not carry over, your DENIES do, and the app cannot widen
what your rules allow. So a field your functions write for any caller (a count of places sold) belongs on a row of
its own, not on the derivable entity. See [[entity-inheritance#security]].
How do people's data reach a privacy request?#
Say whose a row is ([DataSubject], [PersonalData(…)]) and classify every personal field, free text included
([Classification(Personal.Content)]), so the audit keeps no copy. When erasing must go through your function, ship
a bridge Osysharp.Privacy.<Kit> (completes Osysharp.Privacy@0, <Kit>@0;) with an IPersonalDataHolder
(Bridges — a package that arrives when an app uses the two it completes). See Declared personal data — say whose a row is, and the platform answers for it.
Where do its pages go?#
At the kit's own [Route], in [Layout(WorkspaceFrame)], around [Composable] screens, theme tokens only, so a look
re-skins them; an app takes one over by declaring a component of the same name. A component keeps the state it
mounted with when a parameter changes, so open one record's detail in a Dialog rather than a side panel fed a new
id. One Ui.* test per page;
osy test --pixels shots go in the README (from tests/osy-shots/ to the kit's docs/). See component.
How do I test a rule that sits behind a deny?#
Seed the first row in a fixture (it writes past security), then try the second as a person, through the function. A fixture's READS obey the rules: keep the rows you made in variables. See [[testing-fixtures#behind-a-deny]].
[Principal] entity Member { [Unique, MaxLength(80)] string Name; security { allow read when IsAuthenticated; } }
entity Tally {
[Required] Member Holder;
int Left = 3;
security {
allow read, create, update when IsAuthenticated where Holder == user;
deny update Left except via Spend;
}
}
[Server, Authorize(IsAuthenticated)]
void Spend(Tally t) { t.Left = t.Left - 1; }
void SetLeft(Tally t) { t.Left = 9; }
[TestFixture]
void OneTally() { new Tally { Holder = new Member { Name = "Ida" } }; }
principal Ida => Member.Single(m => m.Name == "Ida");
[Test(OneTally)]
[runas(Ida)]
void Only_the_function_changes_it() {
var t = Tally.Single(x => x.Holder.Name == "Ida");
Assert.Denied(() => SetLeft(t));
Spend(t);
Assert.Equal(2, Tally.Single(x => x.Id == t.Id).Left);
}Is it ready to publish?#
osy kits check kits/lockers composes it the way an app would and flags what an app would trip on
(Checking a kit composes — osy kits check); osy publish runs its tests and tags the release ([[project-package#publishing]]).
See also#
- Finding a kit, and learning one from its tests — before writing one: is there a kit already?
- Connecting a kit — each seam with the code that connects it — each seam as an app sees it
- What may go past the caller's own rights — and what never does — everything that writes past a caller's own rights