Keep sign-ups unlinkable on ActivityPods, and land the pending coherence-pass work
Participation privacy (decision_2026-10-29_participation-privacy-mechanism): a sign-up is a private Join naming the meeting point, addressed to nobody, handled by the backend on emission. Attendee lists hold member URNs hashed from (WebID, event, a secret kept in the participant's profile), so counts stay public and only contacts recognise anyone; the creator has no read right and is notified anonymously. Each person's participations collection, created by the backend at the first sign-up, is readable by their /contacts WAC group only, never world-readable. The member URN is sticky per (meeting point, participant) and every collection write is verified by reading back, so a refused write leaves the act unrecorded and retryable. The EventRef mechanism and its browser-side provisioning are gone. The backend now runs with WebACL on, as the reference apps do, on its own secure datasets festipod / settings-festipod instead of the pod provider's settings dataset; pod-notifications needs it. The compose header and the deployment leaf say how to reset a stack whose datasets predate this. The rights matrix covers the new mechanism live (148 hard lines), including a forged contact Accept between throwaway accounts: the stranger enters the contacts list (platform defect, reported upstream) but is refused the profile and the participations. Tests may attack as long as they never break the dev environment (rule_tests-never-break-the-dev-environment). Also lands the coherence-pass work that was still uncommitted: contacts as the Pod's social graph and invite links, image upload to the Pod, the meeting-point form, profile completion gate, and the doctrine for them. Superseded briefs and decisions are deleted rather than annotated.
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
// removing the foaf:Agent triple alone would leave the own set empty and the PUT would be refused.
|
||||
// `revocationGraph` therefore keeps the owner as `acl:agent` of `#Read` when nothing else remains —
|
||||
// the owner already reads via the default, so no right is gained or lost — and preserves any other
|
||||
// own agent triples (e.g. a future per-contact grant) unchanged.
|
||||
// own agent triples (e.g. the `/contacts` group a participations collection grants read to) unchanged.
|
||||
//
|
||||
// NARROW: only the public READ goes. The pod keys own rules by mode (`#Read`, `#Write`, `#Append`,
|
||||
// `#Control`); the `foaf:Agent` triple is removed from the `#Read` rule alone, so a `foaf:Agent` on
|
||||
@@ -28,6 +28,15 @@
|
||||
// the compacted `foaf:Agent` the pod returns (removes nothing), and it drops any authorization
|
||||
// whose removed predicate becomes empty even when other agent predicates remain (would empty the
|
||||
// set → 400). This module PUTs a correct graph instead.
|
||||
//
|
||||
// `ownReaders` says who a resource's own `#Read` rule lets read (its agents, groups and classes):
|
||||
// participations.service.js uses it to tell whether the `/contacts` WAC group it grants read to a
|
||||
// participations collection is already granted.
|
||||
//
|
||||
// Two pod calls go with the graphs: `readAcl` (the ACL as the pod serves it to the app, a failure
|
||||
// told apart from "no public read") and `putOwnRights` (the PUT, as the app actor). Every Festipod
|
||||
// caller that reads or replaces an ACL goes through them: public-read (events and their attendee
|
||||
// lists) and participations (a person's participations collection).
|
||||
|
||||
const { arrayOf } = require('@semapps/ldp');
|
||||
|
||||
@@ -98,12 +107,13 @@ const aclContext = aclUri => ({
|
||||
});
|
||||
|
||||
/**
|
||||
* revocationGraph(authorizations, resourceUri, ownerUri) → a JSON-LD document `{ '@context', '@graph' }`
|
||||
* to PUT on the resource's ACL so it keeps every one of its OWN agent triples EXCEPT
|
||||
* `acl:agentClass foaf:Agent` on its `#Read` authorization (a `foaf:Agent` on any other own rule stays). Inherited `#Default*` rules are not emitted
|
||||
* (a PUT does not touch them). When removing the public-read triple would leave the own set empty,
|
||||
* the owner is emitted as `acl:agent` of `#Read` so the PUT is accepted without changing any right.
|
||||
* `aclUri` is where the document will be PUT (used only for the context `@base`).
|
||||
* revocationGraph(authorizations, resourceUri, ownerUri, aclUri) → a JSON-LD document `{ '@context',
|
||||
* '@graph' }` to PUT on the resource's ACL so it keeps every one of its OWN agent triples EXCEPT
|
||||
* `acl:agentClass foaf:Agent` on its `#Read` authorization (a `foaf:Agent` on any other own rule
|
||||
* stays). The other own rules (`#Write`, `#Append`, `#Control`) stay exactly as they are. Inherited
|
||||
* `#Default*` rules are not emitted (a PUT does not touch them). When removing the public-read triple
|
||||
* would leave the own set empty, the owner is emitted as `acl:agent` of `#Read` so the PUT is accepted
|
||||
* without changing any right. `aclUri` is where the document will be PUT (used only for the context `@base`).
|
||||
*/
|
||||
const revocationGraph = (authorizations, resourceUri, ownerUri, aclUri) => {
|
||||
const graph = [];
|
||||
@@ -134,13 +144,78 @@ const revocationGraph = (authorizations, resourceUri, ownerUri, aclUri) => {
|
||||
return { '@context': aclContext(aclUri), '@graph': graph };
|
||||
};
|
||||
|
||||
/**
|
||||
* ownReaders(authorizations) → `{ agent, agentClass, agentGroup }`: the values (full IRIs, without
|
||||
* duplicates) the resource's OWN `#Read` authorization grants read to — not the inherited rules.
|
||||
*/
|
||||
const ownReaders = authorizations => {
|
||||
const readers = { agent: [], agentClass: [], agentGroup: [] };
|
||||
for (const authorization of arrayOf(authorizations)) {
|
||||
if (suffixOf(authorization) !== 'Read' || !expandedValuesOf(authorization, 'mode').includes(ACL + 'Read')) continue;
|
||||
for (const name of Object.keys(readers)) {
|
||||
for (const iri of expandedValuesOf(authorization, name)) {
|
||||
if (typeof iri === 'string' && !readers[name].includes(iri)) readers[name].push(iri);
|
||||
}
|
||||
}
|
||||
}
|
||||
return readers;
|
||||
};
|
||||
|
||||
/** The ACL document URI of a pod resource: `{origin}/_acl{path}` (the pod's `getAclUriFromResourceUri`). */
|
||||
const aclUriOf = resourceUri => {
|
||||
const { origin, pathname } = new URL(resourceUri);
|
||||
return `${origin}/_acl${pathname}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* readAcl(ctx, resourceUri, actorUri) → `{ status, authorizations }`, the authorizations only when
|
||||
* the pod answered 200. Unlike `pod-permissions.get`, which answers `false` for ANY failure —
|
||||
* indistinguishable from "no public read" — a failure here is reported, so a caller can tell "not
|
||||
* public" from "unknown". Read where `putOwnRights` writes (`aclUriOf`), through the pod owner's
|
||||
* access (`actorUri`).
|
||||
*/
|
||||
async function readAcl(ctx, resourceUri, actorUri) {
|
||||
const { status, body } =
|
||||
(await ctx.call('pod-permissions.fetch', {
|
||||
url: aclUriOf(resourceUri),
|
||||
headers: { Accept: 'application/ld+json' },
|
||||
actorUri
|
||||
})) || {};
|
||||
if (status !== 200 || !body || typeof body !== 'object') return { status: typeof status === 'number' ? status : 0 };
|
||||
const graph = body['@graph'] !== undefined ? body['@graph'] : body['@id'] || body.id ? [body] : [];
|
||||
return { status, authorizations: Array.isArray(graph) ? graph : [graph] };
|
||||
}
|
||||
|
||||
/**
|
||||
* putOwnRights(ctx, resourceUri, document) → the HTTP status of the PUT of `document` (a
|
||||
* `revocationGraph`) on the resource's ACL, sent as the APP ACTOR through the platform's signature
|
||||
* proxy — the actor that holds `acl:Control` on the resources Festipod grants or revokes on.
|
||||
* Resolves the status (0 when none); the caller decides what a refusal means.
|
||||
*/
|
||||
async function putOwnRights(ctx, resourceUri, document) {
|
||||
const app = await ctx.call('app.get');
|
||||
const { status } =
|
||||
(await ctx.call('signature.proxy.query', {
|
||||
url: aclUriOf(resourceUri),
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/ld+json' },
|
||||
body: JSON.stringify(document),
|
||||
actorUri: app.id || app['@id']
|
||||
})) || {};
|
||||
return typeof status === 'number' ? status : 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
grantsPublicRead,
|
||||
hasOwnPublicRead,
|
||||
hasInheritedPublicRead,
|
||||
revocationGraph,
|
||||
ownReaders,
|
||||
isPublicRead,
|
||||
expand,
|
||||
aclUriOf,
|
||||
readAcl,
|
||||
putOwnRights,
|
||||
FOAF_AGENT,
|
||||
ACL
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user