fix: additional tests for remote privileges, enforcing privileges for remote edits and deletes

This commit is contained in:
Julian Lam
2025-05-26 14:49:48 -04:00
parent fd2ae7261e
commit a888b868c7
3 changed files with 240 additions and 1 deletions

View File

@@ -95,6 +95,12 @@ inbox.update = async (req) => {
try { try {
switch (true) { switch (true) {
case isNote: { case isNote: {
const cid = await posts.getCidByPid(object.id);
const allowed = await privileges.categories.can('posts:edit', cid, activitypub._constants.uid);
if (!allowed) {
throw new Error('[[error:no-privileges]]');
}
const postData = await activitypub.mocks.post(object); const postData = await activitypub.mocks.post(object);
postData.tags = await activitypub.notes._normalizeTags(postData._activitypub.tag, postData.cid); postData.tags = await activitypub.notes._normalizeTags(postData._activitypub.tag, postData.cid);
await posts.edit(postData); await posts.edit(postData);
@@ -200,7 +206,7 @@ inbox.delete = async (req) => {
const objectHostname = new URL(pid).hostname; const objectHostname = new URL(pid).hostname;
if (actorHostname !== objectHostname) { if (actorHostname !== objectHostname) {
throw new Error('[[error:activitypub.origin-mismatch]]'); return reject('Delete', object, actor);
} }
const [isNote/* , isActor */] = await Promise.all([ const [isNote/* , isActor */] = await Promise.all([
@@ -210,6 +216,12 @@ inbox.delete = async (req) => {
switch (true) { switch (true) {
case isNote: { case isNote: {
const cid = await posts.getCidByPid(pid);
const allowed = await privileges.categories.can('posts:edit', cid, activitypub._constants.uid);
if (!allowed) {
return reject('Delete', object, actor);
}
const uid = await posts.getPostField(pid, 'uid'); const uid = await posts.getPostField(pid, 'uid');
await activitypub.feps.announce(pid, req.body); await activitypub.feps.announce(pid, req.body);
await api.posts[method]({ uid }, { pid }); await api.posts[method]({ uid }, { pid });

View File

@@ -202,3 +202,26 @@ Helpers.mocks.update = (override = {}) => {
return { activity }; return { activity };
}; };
Helpers.mocks.delete = (override = {}) => {
let actor = override.actor;
let object = override.object;
if (!actor) {
({ id: actor } = Helpers.mocks.person());
}
if (!object) {
({ id: object } = Helpers.mocks.note());
}
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: `${Helpers.mocks._baseUrl}/delete/${encodeURIComponent(object.id || object)}`,
type: 'Delete',
to: [activitypub._constants.publicAddress],
cc: [`${actor}/followers`],
actor,
object,
};
return { activity };
};

View File

@@ -0,0 +1,204 @@
'use strict';
const assert = require('assert');
const nconf = require('nconf');
const db = require('../mocks/databasemock');
const user = require('../../src/user');
const topics = require('../../src/topics');
const posts = require('../../src/posts');
const categories = require('../../src/categories');
const privileges = require('../../src/privileges');
const meta = require('../../src/meta');
const install = require('../../src/install');
const utils = require('../../src/utils');
const activitypub = require('../../src/activitypub');
const helpers = require('./helpers');
describe('Privilege logic for remote users/content (ActivityPub)', () => {
before(async () => {
meta.config.activitypubEnabled = 1;
// await install.giveWorldPrivileges();
});
describe('"fediverse" pseudo-user', () => {
describe('no privileges given', () => {
let uid;
let cid;
let topicData;
let postData;
let mainPid;
let handle;
before(async () => {
uid = await user.create({ username: utils.generateUUID() });
({ cid } = await categories.create({ name: utils.generateUUID() }));
({ topicData, postData } = await topics.post({
cid,
uid,
title: utils.generateUUID(),
content: utils.generateUUID(),
}));
handle = await categories.getCategoryField(cid, 'handle');
const privsToRemove = await privileges.categories.getGroupPrivilegeList();
await privileges.categories.rescind(privsToRemove, cid, ['fediverse']);
});
describe('incoming requests', () => {
it('should not respond to a webfinger request to a category\'s handle', async () => {
const response = await activitypub.helpers.query(`${handle}@${nconf.get('url_parsed').hostname}`);
assert.strictEqual(response, false);
});
it('should not respond to a request for the category actor', async () => {
await assert.rejects(
activitypub.get('uid', uid, `${nconf.get('url')}/category/${cid}`),
{ message: '[[error:activitypub.get-failed]]' }
);
});
it('should not respond to a request for a topic collection', async () => {
await assert.rejects(
activitypub.get('uid', uid, `${nconf.get('url')}/topic/${topicData.tid}`),
{ message: '[[error:activitypub.get-failed]]' }
);
});
it('should not respond to a request for a post', async () => {
await assert.rejects(
activitypub.get('uid', uid, `${nconf.get('url')}/post/${topicData.mainPid}`),
{ message: '[[error:activitypub.get-failed]]' }
);
});
});
describe('incoming activities', () => {
describe('Create(Note)', () => {
let note;
let activity;
before(async () => {
({ note } = helpers.mocks.note({
cc: [`${nconf.get('url')}/category/${cid}`],
}));
({ activity } = helpers.mocks.create(note));
await activitypub.inbox.create({ body: activity });
});
it('should not assert the note', async () => {
const exists = await posts.exists(note.id);
assert.strictEqual(exists, false);
});
});
describe('Update(Note)', () => {
let note;
let activity;
before(async () => {
({ note } = helpers.mocks.note({
cc: [`${nconf.get('url')}/category/${cid}`],
}));
({ activity } = helpers.mocks.create(note));
await privileges.categories.give(['groups:topics:create'], cid, ['fediverse']);
await activitypub.inbox.create({ body: activity });
});
after(async () => {
await privileges.categories.rescind(['groups:topics:create'], cid, ['fediverse']);
});
it('should assert the note', async () => {
const exists = await posts.exists(note.id);
assert.strictEqual(exists, true);
});
it('should not allow edits to the note', async () => {
const oldContent = note.content;
note.content = 'new content';
({ activity } = helpers.mocks.update({
object: note,
}));
await activitypub.inbox.update({ body: activity });
const postData = await posts.getPostData(note.id);
assert.strictEqual(postData.content, oldContent);
assert.strictEqual(postData.edited, 0);
});
});
describe('Delete(Note)', () => {
let note;
let activity;
before(async () => {
({ note } = helpers.mocks.note({
cc: [`${nconf.get('url')}/category/${cid}`],
}));
({ activity } = helpers.mocks.create(note));
await privileges.categories.give(['groups:topics:create'], cid, ['fediverse']);
await activitypub.inbox.create({ body: activity });
});
after(async () => {
await privileges.categories.rescind(['groups:topics:create'], cid, ['fediverse']);
});
it('should assert the note', async () => {
const exists = await posts.exists(note.id);
assert.strictEqual(exists, true);
});
it('should ignore remote deletion of said note', async () => {
({ activity } = helpers.mocks.delete({ object: note }));
await activitypub.inbox.delete({ body: activity });
const exists = await posts.exists(note.id);
assert.strictEqual(exists, true);
});
});
});
describe('outgoing requests', () => {
it('should not federate out a new post', async () => {
});
it('should not federate out a post edit', async () => {
});
it('should not federate out a post deletion', async () => {
});
it('should not federate out a post announce', async () => {
});
});
});
describe('regular privilege set', () => {
let cid;
let handle;
before(async () => {
({ cid } = await categories.create({ name: utils.generateUUID() }));
handle = await categories.getCategoryField(cid, 'handle');
const privsToRemove = await privileges.categories.getGroupPrivilegeList();
});
describe('groups:find', () => {
it('should return webfinger response to a category\'s handle', async () => {
const response = await activitypub.helpers.query(`${handle}@${nconf.get('url_parsed').hostname}`);
assert(response);
assert.strictEqual(response.subject, `acct:${handle}@${nconf.get('url_parsed').hostname}`);
assert.strictEqual(response.hostname, nconf.get('url_parsed').hostname);
});
});
});
});
});