After this module you will be able to:
Our running example is a university. Its student information system has a Student table, enrollments, grades,
staff records, uploaded documents and a lot of configuration. Sooner or later someone asks questions like:
If the answer lives in a spreadsheet maintained by hand, it is out of date the day after a developer adds a column. Linneo puts the answer next to the field, in the source code, and checks it in the build:
@Category(USER_HEALTH_AND_MEDICAL_DISABILITY)
private Integer disabilityDegree;
The annotation says what the field holds. Everything else — whether it is personal data, how sensitive it is, how it must be protected — follows from the category, as the rest of this module explains.
Every data category in Linneo has a LegalNature, an enum with four values:
LegalNature |
Meaning | University examples |
|---|---|---|
NONE |
Not personal data: system, organisation or reference data | menu configuration, list of countries, building names |
ORDINARY |
Personal data (GDPR art. 4.1): any information relating to an identified or identifiable natural person | student name, email, enrollment, grade, IBAN of a staff member |
SPECIAL_CATEGORY |
Special categories of personal data (GDPR art. 9): racial or ethnic origin, political opinions, religious or philosophical beliefs, trade union membership, genetic, biometric, health, sex life and sexual orientation | degree of disability, a medical certificate, a fingerprint template |
CRIMINAL_OFFENCE |
Personal data relating to criminal convictions and offences (GDPR art. 10) | the certificate that staff working with minors have no sexual offence convictions |
LegalNature.isPersonal() returns false only for NONE. Special category and criminal offence data are
personal data with reinforced protection: processing them needs an additional legal basis, and a breach is
treated more seriously.
Categories form a tree (module 2). A child category cannot be less personal than its parent. The rule is
encoded in LegalNature.admits(child):
| Parent nature | Child may be |
|---|---|
NONE |
only NONE |
ORDINARY |
ORDINARY, SPECIAL_CATEGORY or CRIMINAL_OFFENCE (anything but NONE) |
SPECIAL_CATEGORY |
only SPECIAL_CATEGORY |
CRIMINAL_OFFENCE |
only CRIMINAL_OFFENCE |
LegalNature.ORDINARY.admits(LegalNature.SPECIAL_CATEGORY); // true: user -> user.health_and_medical
LegalNature.SPECIAL_CATEGORY.admits(LegalNature.ORDINARY); // false: health data never becomes ordinary
LegalNature.NONE.admits(LegalNature.ORDINARY); // false: a system subtree never holds personal data
This is why, under user (ordinary personal data), you find user.health_and_medical (special category) and
user.criminal_history (criminal offence), and why nothing under them can be “downgraded”.
Confidentiality measures how much harm disclosing the data would cause. It has four levels, declared from least
to most strict, so compareTo orders them by strictness:
| Level | Meaning |
|---|---|
PUBLIC |
Meant to be public; disclosure causes no harm |
LOW |
Disclosure would cause limited harm |
MEDIUM |
Disclosure would cause serious harm |
HIGH |
Disclosure would cause severe harm |
LOW, MEDIUM and HIGH follow the usual three-level scales of information security frameworks, such as the
Spanish ENS or FIPS 199. PUBLIC is for data that is meant to be published — the public course catalogue, the
list of countries.
Confidentiality.max(other) returns the stricter of two levels, which is handy when you combine several fields:
Confidentiality.LOW.max(Confidentiality.HIGH); // HIGH
Integrity measures how much harm an unauthorised or undetected change would cause: LOW, MEDIUM, HIGH, again
in increasing order and with a max method.
Integrity is independent of confidentiality. Two university examples:
user.education.grade is confidentiality MEDIUM, integrity HIGH.system.security) may be harmless to disclose, but an unwanted change grants access to
the wrong people: confidentiality LOW, integrity HIGH.Keeping both scales lets the classification drive both kinds of control: access restrictions and encryption for confidentiality; audit trails, signatures and change approval for integrity.
The same kind of data can be stored in very different ways. A grade stored next to the student’s id identifies the
student; the average grade of a course with 120 students does not describe any one of them. Identifiability
captures this, and it belongs to the field, not to the category:
Identifiability |
Meaning | Personal data? |
|---|---|---|
IDENTIFIED |
The value identifies the person directly or through other data the controller holds | yes |
PSEUDONYMIZED |
The value can be linked to the person only with additional information kept separately (GDPR art. 4.5) | yes (GDPR recital 26) |
ANONYMIZED |
The value can no longer be linked to the person | no |
AGGREGATED |
The value summarises several people and does not describe any one of them | no |
Note the common misconception: pseudonymised data is still personal data. Only anonymised and aggregated values leave the scope of the GDPR, and only as long as nobody can re-identify the person.
In module 3 you will declare identifiability on a field:
@Category(value = USER_UNIQUE_ID_PSEUDONYMOUS, identifiability = Identifiability.PSEUDONYMIZED)
private String analyticsId;
Some data does not just describe someone: it grants access. Passwords, password hashes, API keys, signing keys,
tokens. Linneo marks these categories as secret. A secret must never be read back, exported or published — only
verified. In the standard taxonomy a person’s credentials (user.authorization.credentials, .password,
.biometric), session IDs (user.device.session_id, which grant access while the session is valid) and
system.authentication.secret are secrets. user.authorization itself, the scope of a person’s permissions, is
confidential but not secret. Module 2 shows how the flag is inherited, and module 5 uses it to keep secrets out of
logs.
The most important design decision in Linneo is that it fails closed:
The opposite approach — classify what you remember to classify, assume the rest is harmless — is how health data ends up in a debug log. With Linneo, a developer who adds a column without classifying it breaks the build, and the fix is a one-line annotation reviewed together with the column (module 4).
ANONYMIZED
and AGGREGATED values are not.CRIMINAL_OFFENCE is its own nature. (When exported to OpenMetadata both map to the same built-in tag; see
module 7.)Student table holds ordinary data (name), special category data
(disability) and pseudonymous identifiers side by side. The classification is per field.Exercise 1.1 — Nature. For each item held by the university, give its LegalNature:
Exercise 1.2 — Two scales. For each item, say whether confidentiality or integrity is the main concern, and why: (a) the final mark of a student; (b) the hash chain that protects the audit log; (c) a staff member’s home address; (d) the role that grants access to the grading module.
Exercise 1.3 — Identifiability. Choose the Identifiability of each stored value and say whether it is personal
data: (a) a student id in the enrollment table; (b) a random analytics id whose mapping to students is kept in a
separate, restricted table; (c) the average mark of a course; (d) a survey answer whose link to the respondent was
irreversibly destroyed.
Exercise 1.4 — Code. Without running it, predict the output, then run it to check:
System.out.println(LegalNature.CRIMINAL_OFFENCE.isPersonal());
System.out.println(LegalNature.ORDINARY.admits(LegalNature.CRIMINAL_OFFENCE));
System.out.println(LegalNature.CRIMINAL_OFFENCE.admits(LegalNature.SPECIAL_CATEGORY));
System.out.println(Confidentiality.PUBLIC.compareTo(Confidentiality.LOW) < 0);
System.out.println(Integrity.MEDIUM.max(Integrity.LOW));
LegalNature values are personal data?SPECIAL_CATEGORY category be ORDINARY?LegalNature, Confidentiality,
Integrity, Identifiability in detail.← Course overview · Next: Module 2 — The taxonomy →
Exercise 1.1
ORDINARY. 2. NONE. 3. SPECIAL_CATEGORY (health). 4. CRIMINAL_OFFENCE (GDPR art. 10). 5. The category is
about grades (personal), but the stored value is aggregated, so it is not personal data — in module 3 you will
classify it as a grade with identifiability = AGGREGATED. 6. SPECIAL_CATEGORY (religious beliefs). 7. NONE.Exercise 1.2
(a) Both matter, but integrity is critical: an altered mark changes the academic record (the standard policy gives
grades confidentiality MEDIUM, integrity HIGH). (b) Integrity: the hash chain is the evidence that the log was
not altered; disclosing it is harmless. (c) Confidentiality: disclosure exposes the person. (d) Integrity: changing
who holds the role grants access to the wrong people.
Exercise 1.3
(a) IDENTIFIED, personal. (b) PSEUDONYMIZED, personal (the mapping exists, even if kept apart). (c) AGGREGATED,
not personal. (d) ANONYMIZED, not personal, as long as nobody can re-identify the respondent.
Exercise 1.4
true
true
false
true
MEDIUM
Quiz
ORDINARY, SPECIAL_CATEGORY and CRIMINAL_OFFENCE (everything but NONE).SPECIAL_CATEGORY.admits(...) only accepts SPECIAL_CATEGORY.