Osy#betaRun it on Osyrin: osyrin.com

Reference / Security

Declared personal data — say whose a row is, and the platform answers for it

[PersonalData(Category)] entity X { [DataSubject] Person Owner; … } · [PersonalData(Category, OnErasure.DeleteRow)] · [DataSubject(Identifier.Email)] string Email; · [FindsDataSubject] class … { List<T> Find(DataSubject s) }

An entity that says WHOSE personal data a row is — one field marked `[DataSubject]` — and which privacy category it is held under — `[PersonalData(Category)]` — needs no hand-written privacy code. The compiler writes its `Osysharp.Privacy` holder: a person's request counts its rows, exports them, keeps them restricted or erases them, and they appear by name in every plan and in the records of processing.

preview1 example compiled by CIsecurityprivacygdprkit

Summary#

Under the GDPR a person may ask what you hold about them, take it with them, have it restricted or erased. For most of an app's own tables the answer follows from two facts: whose data a row is, and why it is kept. Say both, and the platform does the rest:

[PersonalData(SupportNotes)]               // why: a privacy category, declared once as a function
entity Ticket {
  [DataSubject] Customer Customer;         // whose: the customer the ticket is for
  [Classification(Personal.Content)] string? RootCause;
}

Signature#

DataCategory SupportNotes() => new DataCategory { Key = "app.notes", Label = "…", Purpose = "…", Basis = … };

[PersonalData(SupportNotes)]                       // erasure blanks the row's own Personal.* fields; the row stays
[PersonalData(SupportNotes, OnErasure.DeleteRow)]  // erasure deletes the row — it is wholly the person's

[DataSubject] Person Owner;                        // the app's [Principal] — found by the account a request proved
[DataSubject] Ticket Ticket;                       // another [PersonalData] row — that row's person
[DataSubject] Customer Customer;                   // a type a [FindsDataSubject] class finds (a party, by its addresses)
[DataSubject(Identifier.Email)] string Email;      // the person's own address — or Identifier.Phone, a number

[FindsDataSubject]
public class CustomersOf { public List<Customer> Find(DataSubject s) { … } }

Description#

What the platform writes. One IPersonalDataHolder for the app (another for each package that declares its own), discovered like every holder, keyed by the app's name. For each category:

a request asksthe holder
what is held (every plan)counts the rows about the person — rows already kept restricted included, read inside the category's purpose read
access, portabilityexports each row's own values and every personal field it inherits, with when it was made
erasureerases — blanks the row's Personal.* fields (OnErasure.ClearPersonalFields, the default), or deletes the row (OnErasure.DeleteRow)
restriction, or a kept categorykeeps each row restricted until its period ends — counted from when it was made — or until lifted
—a row a legal hold covers as a case makes the erasure wait (Legal holds — nothing a dispute needs is erased or purged while it lasts)

Whose a row is. A request carries only what it proved — an account, addresses, numbers — never what a holder finds along the way. The [DataSubject] field says how to get from those to a row: the app's [Principal] (by the account), the person's own address or number held on the row (Identifier.Email, Identifier.Phone, matched exactly), another [PersonalData] row (an answer through its ticket, a ticket through its customer), or a type a [FindsDataSubject] class finds. Osysharp.Privacy.Conversations ships PartiesOfASubject, so a reference to a Party — or the app's own Customer : Party — is followed through the addresses on its contact points, parties merged into it included. A finder is a CLASS, not a function: a public function is callable by a request, and "who holds this address" must not be a URL.

Which fields. What is personal stays said on the field ([Classification(Personal.X)], personal data (Classification(Personal.X))). Erasure blanks every personal field the row holds, its own and the ones it inherits from a base type alike.

Order. A request erases the category that says how others find the person — their contact points (DataCategory.Identifies) — after every other one, so a declared holder still finds the tickets of a customer whose addresses go in the same request. A declared category whose rows other declared rows are found through, and which an erasure deletes, is marked so too: in the example below a member's calls go before the preference that says whose they are. Within each step, the declared holder's erasure runs before every holder you wrote by hand. So a hand-written holder that writes something in place of a blanked field keeps it. For example, a ticket's title, which was the customer's subject, can become "Erased conversation" instead of staying blank.

Refused at compile (PERSONAL_DATA_DECLARATION), each naming the fix: a [PersonalData] entity with no — or two — [DataSubject] fields; a reference no request can follow; a category that is not a parameterless DataCategory function, or whose Key is not a literal (it names the category's purpose reads and access-trail rows); a required personal field that is not text, when erasure keeps the row; a row that stays on erasure pointing at one that is deleted (the delete would be refused); [PersonalData] in an app that does not use Osysharp.Privacy; and — for now — an entity whose erasure keeps the row in a KEPT category (a legal basis with a period), because a kept row is purged by deletion when its period ends.

When to write a holder instead. Anything the declaration cannot say: a reason an erasure must wait ("an order is on its way"), a category kept with another (KeptWith), a kept complaint read only for a claim, files written about a row. A hand-written holder and a declared one live side by side in one app.

Examples#

app Club {
  use Osysharp.Identity;
  use Osysharp.WorkQueues;
  use Osysharp.Mail;
  use Osysharp.Privacy;
  use Osysharp.Workflow;
  use Osysharp.Observability;
  use Osysharp.Storage;
}

using Osysharp.Identity;
using Osysharp.Privacy;

[Role] enum ClubRole { Authenticator, Admin }
policy IsAdmin => RoleGrant.Any(g => g.Member == user && g.Role == ClubRole.Admin);
policy ManagesPrivacy => IsAdmin;
policy PlacesLegalHolds => IsAdmin;
policy WorksInbox => IsAdmin;
policy LeadsInbox => IsAdmin;
policy AdministersInbox => IsAdmin;
policy ManagesOrganizations => IsAdmin;
policy CollaboratesOnRecords => IsAdmin;

/// Why the club keeps a member's newsletter preference.
public DataCategory Preferences() {
  return new DataCategory { Key = "club.preferences", Label = "Your newsletter preference",
                            Purpose = "Sending you the newsletter", SubjectKind = "members",
                            Basis = RetentionBasis.Consent, Portable = true };
}

/// What we wrote down when a member called us — wholly about them, so an erasure deletes it.
public DataCategory CallNotes() {
  return new DataCategory { Key = "club.calls", Label = "Notes from your calls to us",
                            Purpose = "Helping you", SubjectKind = "members", Basis = RetentionBasis.Contract };
}

[PersonalData(Preferences, OnErasure.DeleteRow)]
entity Preference {
  [DataSubject(Identifier.Email), Classification(Personal.Contact), MaxLength(320)] string Email;
  bool Newsletter;
  [CreatedAt] DateTime CreatedAt;
  security { allow read, create, update, delete when IsAdmin; }
}

[PersonalData(CallNotes, OnErasure.DeleteRow)]
entity Call {
  [DataSubject] Preference Member;
  DateTime At;
  [Classification(Personal.Content), MaxLength(2000)] string? Notes;
  [CreatedAt] DateTime CreatedAt;
  security { allow read, create, update, delete when IsAdmin; }
}

// The club's own two levels, read by whoever may read the row (the admins); the kits' levels need no line.
app = new() { Classifications = [ new Classification(Personal.Contact) { Open = true },
                                  new Classification(Personal.Content) { Open = true } ] };

See also#

Related

The Privacy kit — erasure that keeps only what the company must or may

`Osysharp.Privacy` answers a person's GDPR request — access, portability, erasure, restriction — per category of what…

personal data (Classification(Personal.X))

Mark a field as personal data once, on the property, with a level of the core `Personal` enum. From then on the audit…

restricted rows and purpose reads (Restriction, [PurposeRead])

Keep a row only for a stated purpose: Restriction.Restrict stamps it with a privacy category and the end of its period…

Legal holds — nothing a dispute needs is erased or purged while it lasts

A legal hold in `Osysharp.Privacy` keeps everything about a person — or one case — from being erased or purged while a…