Files
NodeBB/test/activitypub/privileges.js
2025-12-04 16:03:28 -05:00

443 lines
13 KiB
JavaScript

'use strict';
const assert = require('assert');
const nconf = require('nconf');
const db = require('../mocks/databasemock');
const request = require('../../src/request');
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, body } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${handle}@${nconf.get('url_parsed').host}`);
assert(response);
assert.strictEqual(response.statusCode, 200);
assert(body.links && body.links.length);
assert.strictEqual(body.subject, `acct:${handle}@${nconf.get('url_parsed').host}`);
});
});
});
});
});
describe('Privilege masking', () => {
before(async () => {
// Grant default fediverse privileges
await install.giveWorldPrivileges();
});
describe('control', () => {
let cid;
let uid;
before(async () => {
// Set up a standard mock group
({ id: cid } = helpers.mocks.group());
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
});
it('should properly assert the remote category', async () => {
const assertion = await activitypub.actors.assertGroup([cid]);
const exists = await categories.exists(cid);
assert(assertion);
assert(exists);
});
it('should not set up a privilege mask for that category', async () => {
const exists = await db.exists(`cid:${cid}:privilegeMask`);
assert(!exists);
});
it('should pass the privileges .can() check if requested', async () => {
const set = await privileges.categories.get(cid, uid);
const can = await privileges.categories.can('topics:create', cid, uid);
assert(can);
});
it('should return true in the privilege set when requested', async () => {
const set = await privileges.categories.get(cid, uid);
assert(set);
assert(set['topics:create']);
});
});
describe('postingRestrictedToMods (true on assert)', () => {
let cid;
let uid;
before(async () => {
// Set up a mock group with `postingRestrictedToMods` bit set
({ id: cid } = helpers.mocks.group({
postingRestrictedToMods: true,
}));
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
});
it('should properly assert the remote category', async () => {
const assertion = await activitypub.actors.assertGroup([cid]);
const exists = await categories.exists(cid);
assert(assertion);
assert(exists);
});
it('should set up a privilege mask for that category', async () => {
const exists = await db.exists(`cid:${cid}:privilegeMask`);
assert(exists);
});
it('should contain a single mask', async () => {
const members = await db.getSetMembers(`cid:${cid}:privilegeMask`);
assert(members);
assert.strictEqual(members.length, 1);
assert.strictEqual(members[0], 'topics:create');
});
it('should fail the privileges .can() check if requested', async () => {
const can = await privileges.categories.can('topics:create', cid, uid);
assert(!can);
});
it('should return false in the privilege set when requested', async () => {
const set = await privileges.categories.get(cid, uid);
assert(set);
assert(!set['topics:create']);
});
});
describe('postingRestrictedToMods (true on assert and re-assertion)', () => {
let cid;
let uid;
before(async () => {
// Set up a mock group with `postingRestrictedToMods` bit set
({ id: cid } = helpers.mocks.group({
postingRestrictedToMods: true,
}));
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
// Assert group, then re-assert again
await activitypub.actors.assertGroup([cid]);
await activitypub.actors.assertGroup([cid], { update: true });
});
it('should fail the privileges .can() check if requested', async () => {
const can = await privileges.categories.can('topics:create', cid, uid);
assert(!can);
});
it('should return false in the privilege set when requested', async () => {
const set = await privileges.categories.get(cid, uid);
assert(set);
assert(!set['topics:create']);
});
});
describe('postingRestrictedToMods (true on assert, false on update)', () => {
let cid;
let uid;
before(async () => {
// Set up a mock group with `postingRestrictedToMods` bit set
({ id: cid } = helpers.mocks.group({
postingRestrictedToMods: true,
}));
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
await activitypub.actors.assertGroup([cid]);
// Group updated "remotely"
helpers.mocks.group({
id: cid,
postingRestrictedToMods: false,
});
});
it('should remove the privilege mask if the bit is not present on group actor update', async () => {
let can = await privileges.categories.can('topics:create', cid, uid);
assert(!can, 'Initial state should be denied due to mask.');
// Group re-assertion
await activitypub.actors.assertGroup([cid], { update: true });
// Ensure mask is gone from db
const memberCount = await db.setCount(`cid:${cid}:privilegeMask`);
assert.strictEqual(memberCount, 0);
can = await privileges.categories.can('topics:create', cid, uid);
assert(can, 'Privilege should be restored after mask removal.');
});
});
describe('postingRestrictedToMods (true on assert, property missing on update)', () => {
let cid;
let uid;
before(async () => {
// Set up a mock group with `postingRestrictedToMods` bit set
({ id: cid } = helpers.mocks.group({
postingRestrictedToMods: true,
}));
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
await activitypub.actors.assertGroup([cid]);
// Group updated "remotely"
helpers.mocks.group({
id: cid,
});
});
it('should remove the privilege mask if the bit is not present on group actor update', async () => {
let can = await privileges.categories.can('topics:create', cid, uid);
assert(!can, 'Initial state should be denied due to mask.');
// Group re-assertion
await activitypub.actors.assertGroup([cid], { update: true });
// Ensure mask is gone from db
const memberCount = await db.setCount(`cid:${cid}:privilegeMask`);
assert.strictEqual(memberCount, 0);
can = await privileges.categories.can('topics:create', cid, uid);
assert(can, 'Privilege should be restored after mask removal.');
});
});
describe('postingRestrictedToMods (false on assert, true on update)', () => {
let cid;
let uid;
before(async () => {
// Set up a mock group with `postingRestrictedToMods` bit set to false
({ id: cid } = helpers.mocks.group({
postingRestrictedToMods: false,
}));
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
await activitypub.actors.assertGroup([cid]);
// Update group "remotely", re-assert
helpers.mocks.group({
id: cid,
postingRestrictedToMods: true,
});
await activitypub.actors.assertGroup([cid], { update: true });
});
it('should fail the privileges .can() check if requested', async () => {
const can = await privileges.categories.can('topics:create', cid, uid);
assert(!can);
});
it('should return false in the privilege set when requested', async () => {
const set = await privileges.categories.get(cid, uid);
assert(set);
assert(!set['topics:create']);
});
});
});