Lawful basis and consent¶
What it prevents: discovering, after processing a million records, that nobody can say which law permitted any of it.
Uganda's Data Protection and Privacy Act requires a lawful ground for processing. The usual failure is that the ground is a fact about the organisation — recorded in a policy document, decided once, applied to everything. Then the policy changes, and there is no way to say what ground a record collected in 2026 was collected under, because the ground was never attached to the record.
Every record states its own ground¶
lawful_basis is on the envelope, mandatory, and part of the record forever.
The permitted values are in the
Enumerations reference:
consent, legal_authorisation, public_duty, national_security,
law_enforcement, contract_performance, medical, legal_obligation,
special_data_consent.
Two carry most of the traffic:
contract_performance — the cooperative processes a member's details
because there is a membership agreement. Enrolment, plots, plantings, harvests.
special_data_consent — s.9 treats financial information as special
personal data, which needs explicit consent and cannot ride on contract
performance. Deliveries carry a price, and a price is financial information.
The dry run makes this visible at stage 3:
A record's basis constrains what can be done with it downstream. A read whose
purpose is not compatible with the record's basis is refused, and the reason
lawful_basis_forbids is recorded.
See 0019.
Consent is a record, not a flag¶
A grant is an append-only record naming:
- the subject granting it,
- the grantee it is granted to,
- the purpose it covers, such as
credit_assessment, - the record types it covers,
- when it was granted, and when it expires.
Withdrawal is a further record; nothing is edited. A grant that was revoked still exists, along with what it permitted while it was live, because "did this lender have permission on 3 June" is a question somebody will eventually ask.
See 0029.
Purpose is required, and it is a query parameter¶
Every read that discloses records must state a purpose:
A read with no purpose gets 400 purpose_required and is told so plainly. A
purpose sent as a header gets 400 with a message saying to send it as a
query parameter.
Both of those used to be indistinguishable from having no grant, which meant an integrator with a typo in a header name spent an afternoon debugging a permissions problem that did not exist. A purpose is part of what is being asked, not of how the request is transported, so it belongs in the URL.
Data subjects are natural persons¶
A delivery names a farmer and a cooperative. The consent gate used to treat both as data subjects and wait for the cooperative to consent to disclosure of its own trading activity — so the farmer's grant, given in good faith, achieved nothing.
The DPPA protects individuals. A cooperative, business, institution or agency is
a party to a record but not a data subject. Subject resolution now counts
only parties whose kind is person. Unknown kind is treated as a person,
matching the redaction default: the safe direction is to over-protect.
A record with no natural person on it is not thereby open. It is simply not the DPPA's business — no individual is protected by refusing it — and what still gates it is the organisation's own authorisation. A cooperative's lot is its own to permit; it was never its members' to consent to.
What a refusal says¶
Nothing.
No record id, no type, no party id. A 403 naming the record and the party whose grant was missing is a disclosure: it confirms that party exists and asserted something about something. The reason goes to the audit log, where a question about a refusal should be answered from.
The exceptions are refusals that leak nothing about who holds what:
| Reason | Status |
|---|---|
purpose_required |
400 |
no_verified_subject |
401 |
| everything else | 404, with no code and no identifiers |
The reasons recorded¶
Every decision, allow or deny, is recorded with a reason:
self_read, asserter_read, member_body, member_body_with_grant,
third_party_with_grant, kernel_integrity, unattributed_record,
no_attributable_party, subject_unresolvable, no_verified_subject,
purpose_required, no_grant, grant_expired, grant_revoked,
grant_wrong_purpose, grant_wrong_record_type, financial_needs_consent,
lawful_basis_forbids.
These are what makes a refusal answerable later. The caller gets a 404; the operator, reading the audit log, gets the reason.
Open, with counsel
Whether s.9(3)(c) permits a cooperative to process its members' financial
information without separate explicit consent is unresolved. The kernel
currently requires the separate consent — the stricter reading — behind
S9_CONSENT_REQUIRED_FOR_MEMBER_BODY. If counsel says otherwise the flag
flips; if counsel is never asked, the flag stays as it is.