152 lines
6.9 KiB
JavaScript
152 lines
6.9 KiB
JavaScript
const urlJoin = require('url-join');
|
|
const { AppService } = require('@activitypods/app');
|
|
const CONFIG = require('../config/config');
|
|
|
|
// `GET /app` has two personalities, discriminated by the request's `JsonLdContext` header.
|
|
//
|
|
// 1. Solid-OIDC Client ID Document. The pod provider's OIDC adapter (RedisAdapter.find)
|
|
// resolves an unknown `client_id` that starts with `http` by fetching it, sending
|
|
// `Accept: application/ld+json` and `JsonLdContext: https://www.w3.org/ns/solid/oidc-context.jsonld`.
|
|
// The pod provider runs the login with `client_id = <HOME_URL>/app`, so this same URL must,
|
|
// for that fetch, return a document whose `@context` literally includes the Solid-OIDC
|
|
// context, whose `client_id` equals the fetched URI exactly, and which carries `redirect_uris`.
|
|
// Anything else fails `invalid_client_metadata / client_id is mandatory property`.
|
|
//
|
|
// 2. SAI app description (the actor). The pod provider's auth-agent fetches this URL
|
|
// WITHOUT the `JsonLdContext` header (`ldp.remote.getNetwork`) to install/describe the app,
|
|
// and expects the app actor document served today (`type: [Application, interop:Application]`).
|
|
//
|
|
// The `JsonLdContext` header is the only discriminator: same path, two representations.
|
|
module.exports = {
|
|
mixins: [AppService],
|
|
settings: {
|
|
baseUrl: CONFIG.HOME_URL,
|
|
app: {
|
|
name: CONFIG.APP_NAME,
|
|
description: CONFIG.APP_DESCRIPTION,
|
|
thumbnail: urlJoin(CONFIG.FRONT_URL, 'logo.svg'),
|
|
frontUrl: CONFIG.FRONT_URL,
|
|
supportedLocales: CONFIG.APP_LANG
|
|
},
|
|
oidc: {
|
|
clientUri: CONFIG.FRONT_URL,
|
|
redirectUris: urlJoin(CONFIG.FRONT_URL, 'login'),
|
|
postLogoutRedirectUris: urlJoin(CONFIG.FRONT_URL, 'login?logout=true'),
|
|
tosUri: null
|
|
},
|
|
accessNeeds: {
|
|
required: [
|
|
{
|
|
shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Event'),
|
|
accessMode: ['acl:Read', 'acl:Write', 'acl:Control'],
|
|
preferredScope: 'interop:AllFromRegistry'
|
|
},
|
|
{
|
|
shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Profile'),
|
|
accessMode: ['acl:Read', 'acl:Write'],
|
|
preferredScope: 'interop:AllFromRegistry'
|
|
},
|
|
{
|
|
shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/vcard/Group'),
|
|
accessMode: 'acl:Read',
|
|
preferredScope: 'interop:AllFromRegistry'
|
|
},
|
|
'apods:ReadInbox',
|
|
'apods:ReadOutbox',
|
|
'apods:PostOutbox',
|
|
'apods:QuerySparqlEndpoint',
|
|
'apods:CreateWacGroup',
|
|
'apods:CreateCollection',
|
|
'apods:UpdateWebId'
|
|
],
|
|
optional: []
|
|
},
|
|
queueServiceUrl: CONFIG.QUEUE_SERVICE_URL
|
|
},
|
|
actions: {
|
|
// Overrides the mixin's `get` (which returns `this.appActor` for every request) to
|
|
// answer the Solid-OIDC Client ID Document fetch as well. `this.appActor` is set by the
|
|
// mixin's `started()`, i.e. long before any request is served.
|
|
get: {
|
|
async handler(ctx) {
|
|
// moleculer-web exposes the raw request only when the route alias sets
|
|
// passReqResToParams (see services/core/core.service.js). Internal calls
|
|
// (ctx.call('app.get')) have no `$req`, so they always get the actor.
|
|
const req = ctx.params && ctx.params.$req;
|
|
const headers = (req && req.headers) || {};
|
|
const jsonLdContextKey = Object.keys(headers).find(key => key.toLowerCase() === 'jsonldcontext');
|
|
const isOidcClientIdDocument =
|
|
jsonLdContextKey !== undefined && String(headers[jsonLdContextKey]).includes('solid/oidc-context.jsonld');
|
|
|
|
// `this.appActor` is set at the very END of the mixin's started(). During boot,
|
|
// this action is already routable (the api gateway is up before the services
|
|
// finish starting) and `@activitypods/app`'s own `awaitCreateComplete` polls the
|
|
// actor over HTTP through this very route — waiting on an action that waits on the
|
|
// boot it blocks. Read the actor straight from the triplestore in that window.
|
|
let actor = this.appActor;
|
|
if (!actor) {
|
|
try {
|
|
actor = await ctx.call('ldp.resource.get', {
|
|
resourceUri: urlJoin(CONFIG.HOME_URL, 'as/actor/app'),
|
|
accept: 'application/ld+json',
|
|
webId: 'system'
|
|
});
|
|
} catch (e) {
|
|
actor = undefined;
|
|
}
|
|
}
|
|
|
|
if (isOidcClientIdDocument) {
|
|
// The client_id must equal the fetched URI exactly. Both `/app` and `/as/actor/app`
|
|
// serve this document (the configured client_id is the actor uri; `/app` remains
|
|
// a valid alias), so echo the request path rather than hardcoding one.
|
|
// `req.url` is rewritten to the alias remainder by moleculer-web; the raw path
|
|
// survives in `originalUrl`.
|
|
const requestPath = String((req && (req.originalUrl || req.url)) || '/as/actor/app').split('?')[0];
|
|
return {
|
|
'@context': ['https://www.w3.org/ns/solid/oidc-context.jsonld'],
|
|
client_id: urlJoin(CONFIG.HOME_URL, requestPath),
|
|
redirect_uris: [urlJoin(CONFIG.FRONT_URL, 'login')],
|
|
client_name: CONFIG.APP_NAME,
|
|
client_uri: CONFIG.FRONT_URL
|
|
};
|
|
}
|
|
|
|
return actor;
|
|
}
|
|
}
|
|
},
|
|
async started() {
|
|
// Heal the actor ↔ access-need-group link. `@activitypods/app` attaches the group to the
|
|
// actor in an after.post hook of the group's creation, but the hook races the actor's own
|
|
// creation on a cold start (both run inside the broker's parallel started() phase): the
|
|
// group then exists while the actor never gets `interop:hasAccessNeedGroup`, and both the
|
|
// pod frontend (consent screen) and the pod backend (requirements discovery) read the
|
|
// needs from that link — an empty consent registers the app with zero grants and the
|
|
// register flow loops. Idempotent: only patches when the link is missing.
|
|
const actorUri = urlJoin(CONFIG.HOME_URL, 'as/actor/app');
|
|
const actor = await this.broker.call('ldp.resource.get', {
|
|
resourceUri: actorUri,
|
|
accept: 'application/ld+json',
|
|
webId: 'system'
|
|
});
|
|
const link = actor && actor['interop:hasAccessNeedGroup'];
|
|
const hasLink = Array.isArray(link) ? link.length > 0 : Boolean(link);
|
|
if (!hasLink) {
|
|
const groups = await this.broker.call('access-needs-groups.list', {
|
|
filters: {
|
|
'http://www.w3.org/ns/solid/interop#accessNecessity': 'http://www.w3.org/ns/solid/interop#AccessRequired'
|
|
},
|
|
accept: 'application/ld+json'
|
|
});
|
|
const group = groups && groups['ldp:contains'] && groups['ldp:contains'][0];
|
|
if (group) {
|
|
await this.broker.call('actors.attachAccessNeedGroup', {
|
|
accessNeedGroupUri: group.id || group['@id']
|
|
});
|
|
this.logger.info(`Healed the actor's interop:hasAccessNeedGroup link to ${group.id || group['@id']}`);
|
|
}
|
|
}
|
|
}
|
|
};
|