107 lines
4.9 KiB
JavaScript
107 lines
4.9 KiB
JavaScript
const { MIME_TYPES } = require('@semapps/mime-types');
|
|
|
|
// Version-skew compatibility shim for the app registration handshake.
|
|
//
|
|
// `@activitypods/app` 2.3.2 (app side) and the local pod provider (HEAD) disagree on where
|
|
// an AccessGrant records the access needs it satisfies. The app-side `app-registrations.verify`
|
|
// tests `accessGrant['interop:satisfiesAccessNeed']` directly on the AccessGrant — the umbrella
|
|
// grant the pod lists in `interop:hasAccessGrant`. The pod provider now writes
|
|
// `interop:satisfiesAccessNeed` on the DataGrants instead, reached through
|
|
// AccessGrant -> `interop:hasDataGrant` -> DataGrant. The original test therefore never matches,
|
|
// verify returns `accessNeedsSatisfied: false`, the inbox processor throws
|
|
// "One or more required access needs have not been granted", the local AppRegistration is never
|
|
// stored, and the consent flow loops back to `/login?register_app=true`.
|
|
//
|
|
// This shim reimplements the original `verify` verbatim, with the access-need test resolving
|
|
// through DataGrants (falling back to the grant itself when it carries no DataGrant, so it
|
|
// tolerates both the old and the new pod-side shape). It is installed as a broker middleware
|
|
// because the target action lives inside the `@activitypods/app` package: the service itself is
|
|
// registered by that package, so the handler cannot be overridden by a normal mixin. The
|
|
// middleware is added to `broker.middlewares` in `created()`, before the services that expose
|
|
// `app-registrations.verify` are constructed, so it wraps every local action; only
|
|
// `app-registrations.verify` is intercepted.
|
|
//
|
|
// This is a workaround for platform drift, not a Festipod concern: the proper fix belongs
|
|
// upstream in `@activitypods/app`, and is reported to the provider rather than worked around there.
|
|
|
|
const arrayOf = value => (value === undefined || value === null ? [] : Array.isArray(value) ? value : [value]);
|
|
|
|
module.exports = {
|
|
name: 'apods-verify-compat',
|
|
created() {
|
|
this.broker.middlewares.add({
|
|
name: 'ApodsVerifyCompat',
|
|
localAction(next, action) {
|
|
if (action.name !== 'app-registrations.verify') return next;
|
|
|
|
return async ctx => {
|
|
const { appRegistrationUri } = ctx.params;
|
|
|
|
// Use local context to get all data
|
|
const jsonContext = await ctx.call('jsonld.context.get');
|
|
|
|
// Get from remote server, not local cache, to get latest version
|
|
const appRegistration = await ctx.call('ldp.remote.getNetwork', {
|
|
resourceUri: appRegistrationUri,
|
|
jsonContext
|
|
});
|
|
|
|
const accessGrants = await Promise.all(
|
|
arrayOf(appRegistration['interop:hasAccessGrant']).map(grantUri =>
|
|
ctx.call('ldp.remote.get', {
|
|
resourceUri: grantUri,
|
|
jsonContext,
|
|
accept: MIME_TYPES.JSON
|
|
})
|
|
)
|
|
);
|
|
|
|
// Resolve each grant's DataGrants, where the pod provider now records the
|
|
// satisfied access needs (AccessGrant -> interop:hasDataGrant -> DataGrant).
|
|
const grantsWithDataGrants = await Promise.all(
|
|
accessGrants.map(async grant => ({
|
|
grant,
|
|
dataGrants: await Promise.all(
|
|
arrayOf(grant['interop:hasDataGrant']).map(dataGrantUri =>
|
|
ctx.call('ldp.remote.get', {
|
|
resourceUri: dataGrantUri,
|
|
jsonContext,
|
|
accept: MIME_TYPES.JSON
|
|
})
|
|
)
|
|
)
|
|
}))
|
|
);
|
|
|
|
const satisfiesAccessNeed = accessNeedUri =>
|
|
grantsWithDataGrants.some(
|
|
({ grant, dataGrants }) =>
|
|
dataGrants.some(dataGrant => dataGrant['interop:satisfiesAccessNeed'] === accessNeedUri) ||
|
|
grant['interop:satisfiesAccessNeed'] === accessNeedUri
|
|
);
|
|
|
|
// Get required access need group(s)
|
|
const filteredContainer = await ctx.call('access-needs-groups.list', {
|
|
filters: {
|
|
'http://www.w3.org/ns/solid/interop#accessNecessity': 'http://www.w3.org/ns/solid/interop#AccessRequired'
|
|
},
|
|
accept: MIME_TYPES.JSON
|
|
});
|
|
const requiredAccessNeedGroups = arrayOf(filteredContainer['ldp:contains']);
|
|
|
|
// Return true if all access needs and special rights of the required AccessNeedGroup(s) are granted
|
|
const accessNeedsSatisfied = requiredAccessNeedGroups.every(
|
|
group =>
|
|
arrayOf(group['interop:hasAccessNeed']).every(accessNeedUri => satisfiesAccessNeed(accessNeedUri)) &&
|
|
arrayOf(group['interop:hasSpecialRights']).every(specialRightUri =>
|
|
appRegistration['apods:hasSpecialRights'].some(sr => sr === specialRightUri)
|
|
)
|
|
);
|
|
|
|
return { accessNeedsSatisfied, appRegistration, accessGrants };
|
|
};
|
|
}
|
|
});
|
|
}
|
|
};
|