Skip to content

Commit

Permalink
Improve resource update rights handling and address addRights perform…
Browse files Browse the repository at this point in the history
…ance #908 (#1332)

* chore: improve description in objects-watcher

* chore: address #908

* fix: deduplicate return values of `getUsersWithReadRights`

* chore: refactor activitypub `setRightsHandler`

* fix: add :z tags to docker volumes for SELinux

* add `cc` to AP object processor

* object watcher: send Update using `bto` instead of `to`

* make fake queue mixin export itself

* support passing multiple users for `webacl.resource.addRights` etc. actions
  • Loading branch information
Laurin-W authored Nov 13, 2024
1 parent 19eb214 commit 83e7a7e
Show file tree
Hide file tree
Showing 16 changed files with 337 additions and 136 deletions.
14 changes: 7 additions & 7 deletions src/jena/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ services:
image: semapps/jena-fuseki-webacl:${TAG}
container_name: fuseki
volumes:
- ./data/rdf_data:/fuseki
- ./data/rdf_data:/fuseki:z
ports:
- "3030:3030"
- '3030:3030'
expose:
- "3030"
- '3030'
environment:
ADMIN_PASSWORD: "admin"
JAVA_MX_RAM: "4G"
ADMIN_PASSWORD: 'admin'
JAVA_MX_RAM: '4G'

fuseki_compact:
build:
Expand All @@ -27,7 +27,7 @@ services:
image: semapps/jena-fuseki-webacl
container_name: fuseki_compact
volumes:
- ./data/rdf_data:/fuseki
- ./data/rdf_data:/fuseki:z

fuseki_migrate:
build:
Expand All @@ -37,4 +37,4 @@ services:
image: semapps/jena-fuseki-webacl
container_name: fuseki_migration
volumes:
- ./data/rdf_data:/fuseki
- ./data/rdf_data:/fuseki:z
2 changes: 2 additions & 0 deletions src/middleware/packages/activitypub/mixins/fake-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ const FakeQueueMixin = {
}
}
};

module.exports = FakeQueueMixin;
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const { hasType } = require('@semapps/ldp');
const { ACTIVITY_TYPES } = require('../../../../constants');

const removeReadRights = async ({ ctx, recipientUris, resourceUri, skipObjectsWatcher, anon }) => {
if (recipientUris === 0 && !anon) return;
await ctx.call(
'webacl.resource.removeRights',
{
resourceUri,
rights: {
...(anon && { anon: { read: true } }),
...(recipientUris && {
uri: recipientUris,
read: true
})
},
webId: 'system'
},
{
meta: {
skipObjectsWatcher
}
}
);
};

const addReadRights = async ({ ctx, recipientUris, resourceUri, skipObjectsWatcher, anon }) => {
if (recipientUris?.length === 0 && !anon) return;
await ctx.call(
'webacl.resource.addRights',
{
resourceUri,
additionalRights: {
...(anon && { anon: { read: true } }),
...(recipientUris && {
user: {
uri: recipientUris,
read: true
}
})
},
webId: 'system'
},
{
meta: {
skipObjectsWatcher
}
}
);
};

const setRightsHandler = {
match: '*',
priority: 1,
async onEmit(ctx, activity) {
const activityUri = activity['@id'] || activity.id;
const newRecipients = await ctx.call('activitypub.activity.getRecipients', { activity });
const activityIsPublic = await ctx.call('activitypub.activity.isPublic', { activity });
/** @type {string} */
const objectUri = typeof activity.object === 'string' ? activity.object : activity.object?.id;

// When a new activity is created, ensure the emitter has read rights as well.
// Don't do that on podProvider config, because the Pod owner already has all rights.
if (!this.settings.podProvider) {
if (!newRecipients.includes(activity.actor)) newRecipients.push(activity.actor);
}

// Give read rights to the activity's recipients.
await addReadRights({
ctx,
recipientUris: newRecipients,
skipObjectsWatcher: true,
resourceUri: activityUri,
anon: activityIsPublic
});

// If Create activity, also give rights to the created object.
if (hasType(activity, ACTIVITY_TYPES.CREATE)) {
await addReadRights({
ctx,
resourceUri: objectUri,
recipientUri: newRecipients,
skipObjectsWatcher: true
});
}
// If Update activity, we need to remove read rights of former recipients, and give read rights to new recipients.
else if (hasType(activity, ACTIVITY_TYPES.UPDATE)) {
// const rights = await ctx.call('webacl.resource.getRights', { resourceUri: objectUri });
// const wasPublic = rights?.anon?.read;
const previousRecipients = await ctx.call('webacl.resource.getUsersWithReadRights', {
resourceUri: objectUri
});

const removedRecipients = previousRecipients.filter(r => !newRecipients.includes(r));
const addedRecipients = newRecipients.filter(r => !previousRecipients.includes(r));

// We add rights to all new recipients.
// ~~*And* if the object was private before, we send out a `Create` activity instead,
// by not skipping the objectWatcher that handles this.~~
await addReadRights({
ctx,
resourceUri: objectUri,
recipientUris: addedRecipients,
skipObjectsWatcher: true, // !wasPublic, // TODO: Maybe this should be false...
anon: activityIsPublic
});

// We remove rights from all actors that aren't recipients anymore.
// *And* we do not skip the object-watcher middleware, to send `Delete` activities
// to actors that cannot see the object anymore.
if (removedRecipients.length > 0 || !activityIsPublic)
await removeReadRights({
ctx,
resourceUri: objectUri,
recipientUris: removedRecipients,
skipObjectsWatcher: false,
anon: !activityIsPublic
});
}
}
};

module.exports = setRightsHandler;
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const { Errors: E } = require('moleculer-web');
const { ControlledContainerMixin, hasType } = require('@semapps/ldp');
const { ControlledContainerMixin } = require('@semapps/ldp');
const { MIME_TYPES } = require('@semapps/mime-types');
const setRightsHandler = require('./activity-handlers/setRightsHandler');
const { objectCurrentToId, objectIdToCurrent, arrayOf } = require('../../../utils');
const { PUBLIC_URI, FULL_ACTIVITY_TYPES, ACTIVITY_TYPES } = require('../../../constants');
const { PUBLIC_URI, FULL_ACTIVITY_TYPES } = require('../../../constants');
const ActivitiesHandlerMixin = require('../../../mixins/activities-handler');

const ActivityService = {
Expand Down Expand Up @@ -117,108 +118,7 @@ const ActivityService = {
}
},
activities: {
addRights: {
match: '*',
priority: 1,
async onEmit(ctx, activity) {
const activityUri = activity['@id'] || activity.id;
const recipients = await ctx.call('activitypub.activity.getRecipients', { activity });

// When a new activity is created, ensure the emitter has read rights also
// Don't do that on podProvider config, because the Pod owner already has all rights
if (!this.settings.podProvider) {
if (!recipients.includes(activity.actor)) recipients.push(activity.actor);
}

// Give read rights to the activity's recipients
// TODO improve performances by passing all users at once
// https://github.com/assemblee-virtuelle/semapps/issues/908
for (const recipientUri of recipients) {
await ctx.call(
'webacl.resource.addRights',
{
resourceUri: activityUri,
additionalRights: {
user: {
uri: recipientUri,
read: true
}
},
webId: 'system'
},
{
meta: {
skipObjectsWatcher: true
}
}
);

// If this is a Create activity, also give rights to the created object
if (hasType(activity, ACTIVITY_TYPES.CREATE)) {
await ctx.call(
'webacl.resource.addRights',
{
resourceUri: typeof activity.object === 'string' ? activity.object : activity.object.id,
additionalRights: {
user: {
uri: recipientUri,
read: true
}
},
webId: 'system'
},
{
meta: {
skipObjectsWatcher: true
}
}
);
}
}

// If activity is public, give anonymous read right
if (await ctx.call('activitypub.activity.isPublic', { activity })) {
await ctx.call(
'webacl.resource.addRights',
{
resourceUri: activityUri,
additionalRights: {
anon: {
read: true
}
},
webId: 'system'
},
{
meta: {
skipObjectsWatcher: true // We don't want to trigger an Update
}
}
);

// If this is a Create activity, also give anonymous read right to the created object
if (hasType(activity, ACTIVITY_TYPES.CREATE)) {
await ctx.call(
'webacl.resource.addRights',
{
resourceUri: typeof activity.object === 'string' ? activity.object : activity.object.id,
additionalRights: {
anon: {
read: true
}
},
webId: 'system'
},
{
meta: {
skipObjectsWatcher: true // We don't want to trigger an Update
}
}
);
}
}
}
}
setRights: setRightsHandler
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ const ObjectService = {

// If an object is passed directly, first wrap it in a Create activity
if (Object.values(OBJECT_TYPES).includes(activityType)) {
const { to, '@id': id, ...object } = activity;
const { to, cc, '@id': id, ...object } = activity;
activityType = ACTIVITY_TYPES.CREATE;
activity = {
'@context': object['@context'],
type: activityType,
to,
cc,
actor: object.attributedTo,
object
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const OutboxService = {
activity = await ctx.call('activitypub.activity.get', { resourceUri: activityUri, webId: 'system' });

try {
// Notify listeners of activities.
await ctx.call('activitypub.side-effects.processOutbox', { activity });
} catch (e) {
await ctx.call('activitypub.activity.delete', { resourceUri: activityUri, webId: 'system' });
Expand All @@ -108,8 +109,8 @@ const OutboxService = {
item: activity
});

let localRecipients = [],
remoteRecipients = [];
const localRecipients = [];
const remoteRecipients = [];
const recipients = await ctx.call('activitypub.activity.getRecipients', { activity });

for (const recipientUri of recipients) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ module.exports = {
'processOutbox',
activity.id,
{ activity },
{ removeOnComplete: { age: 2629800 } } // Keep completed jobs during one month
{ removeOnComplete: { age: 2629800 } } // Keep completed jobs for one month
);

await job.finished();
Expand All @@ -52,7 +52,7 @@ module.exports = {
'processInbox',
activity.id,
{ activity, recipients },
{ removeOnComplete: { age: 2629800 } } // Keep completed jobs during one month
{ removeOnComplete: { age: 2629800 } } // Keep completed jobs for one month
);

await job.finished();
Expand Down
3 changes: 2 additions & 1 deletion src/middleware/packages/sync/middlewares/objects-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const ObjectsWatcherMiddleware = (config = {}) => {
}
};

/** Get owner WebID of resource (by looking at the slash URI). */
const getActor = async (ctx, resourceUri) => {
if (podProvider) {
const url = new URL(resourceUri);
Expand Down Expand Up @@ -86,7 +87,7 @@ const ObjectsWatcherMiddleware = (config = {}) => {
collectionUri: actor.outbox,
'@context': 'https://www.w3.org/ns/activitystreams',
...activity,
to: recipients
bto: recipients
},
{ meta: { webId: actor.id, doNotProcessObject: true } }
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ module.exports = {
isContainer
);

// find the difference between addedRights and currentPerms. add only what is not existant yet.
// find the difference between addedRights and currentPerms. add only what is not existent yet.
difference = addedRights.filter(x => !currentPerms.some(y => x.auth === y.auth && x.o === y.o && x.p === y.p));
if (difference.length === 0) return;

Expand All @@ -107,7 +107,7 @@ module.exports = {
// TODO: check that the resource doesn't exist. otherwise, raise an error
if (webId !== 'system')
throw new MoleculerError(
'Access denied ! only system can add permissions for a newly created resource',
'Access denied! Only system can add permissions for a newly created resource',
403,
'ACCESS_DENIED'
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ module.exports = {
}
}

return usersWithReadRights;
// Deduplicate (users might be in multiple groups).
return [...new Set(usersWithReadRights)];
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
params: {
resourceUri: { type: 'string', optional: false },
webId: { type: 'string', optional: true },
/** In nested json format (e.g. `{anon: {read: true}}`) */
rights: { type: 'object', optional: false }
},
async handler(ctx) {
Expand Down
Loading

0 comments on commit 83e7a7e

Please sign in to comment.