mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 11:05:54 +01:00 
			
		
		
		
	feat: fine-grained privileges integration for fediverse users and world pseudo-category
This commit is contained in:
		| @@ -1,4 +1,5 @@ | |||||||
| { | { | ||||||
|  | 	"category.name": "World", | ||||||
| 	"no-topics": "This forum doesn't know of any other topics yet.", | 	"no-topics": "This forum doesn't know of any other topics yet.", | ||||||
|  |  | ||||||
| 	"topic-event-announce-ago": "%1 shared <a href=\"%2\">this post</a> %3", | 	"topic-event-announce-ago": "%1 shared <a href=\"%2\">this post</a> %3", | ||||||
|   | |||||||
| @@ -228,7 +228,7 @@ define('admin/manage/privileges', [ | |||||||
| 		applyPrivileges(bannedUsersPrivs, getBannedUsersInputSelector); | 		applyPrivileges(bannedUsersPrivs, getBannedUsersInputSelector); | ||||||
|  |  | ||||||
| 		// For rest that inherits from registered-users | 		// For rest that inherits from registered-users | ||||||
| 		const getRegisteredUsersInputSelector = (privs, i) => `.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="banned-users"],[data-group-name="guests"],[data-group-name="spiders"]) td[data-privilege="${privs[i]}"] input, .privilege-table tr[data-uid]:not([data-banned]) td[data-privilege="${privs[i]}"] input`; | 		const getRegisteredUsersInputSelector = (privs, i) => `.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="banned-users"],[data-group-name="guests"],[data-group-name="spiders"],[data-group-name="fediverse"]) td[data-privilege="${privs[i]}"] input, .privilege-table tr[data-uid]:not([data-banned]) td[data-privilege="${privs[i]}"] input`; | ||||||
| 		const registeredUsersPrivs = getPrivilegesFromRow('registered-users'); | 		const registeredUsersPrivs = getPrivilegesFromRow('registered-users'); | ||||||
| 		applyPrivileges(registeredUsersPrivs, getRegisteredUsersInputSelector); | 		applyPrivileges(registeredUsersPrivs, getRegisteredUsersInputSelector); | ||||||
| 	}; | 	}; | ||||||
| @@ -240,7 +240,7 @@ define('admin/manage/privileges', [ | |||||||
| 				inputSelectorFn = () => `.privilege-table tr[data-banned] td[data-privilege]:nth-child(${columnNo}) input`; | 				inputSelectorFn = () => `.privilege-table tr[data-banned] td[data-privilege]:nth-child(${columnNo}) input`; | ||||||
| 				break; | 				break; | ||||||
| 			default: | 			default: | ||||||
| 				inputSelectorFn = () => `.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="banned-users"],[data-group-name="guests"],[data-group-name="spiders"]) td[data-privilege]:nth-child(${columnNo}) input, .privilege-table tr[data-uid]:not([data-banned]) td[data-privilege]:nth-child(${columnNo}) input`; | 				inputSelectorFn = () => `.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="banned-users"],[data-group-name="guests"],[data-group-name="spiders"],[data-group-name="fediverse"]) td[data-privilege]:nth-child(${columnNo}) input, .privilege-table tr[data-uid]:not([data-banned]) td[data-privilege]:nth-child(${columnNo}) input`; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		const sourceChecked = getPrivilegeFromColumn(sourceGroupName, columnNo); | 		const sourceChecked = getPrivilegeFromColumn(sourceGroupName, columnNo); | ||||||
|   | |||||||
| @@ -189,16 +189,18 @@ module.exports = function (utils, Benchpress, relative_path) { | |||||||
| 		return states.map(function (priv) { | 		return states.map(function (priv) { | ||||||
| 			const guestDisabled = ['groups:moderate', 'groups:posts:upvote', 'groups:posts:downvote', 'groups:local:login', 'groups:group:create']; | 			const guestDisabled = ['groups:moderate', 'groups:posts:upvote', 'groups:posts:downvote', 'groups:local:login', 'groups:group:create']; | ||||||
| 			const spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups']; | 			const spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups']; | ||||||
|  | 			const fediverseEnabled = ['groups:view:users', 'groups:find', 'groups:read', 'groups:topics:read', 'groups:topics:create', 'groups:topics:reply', 'groups:topics:tag', 'groups:posts:edit', 'groups:posts:history', 'groups:posts:delete', 'groups:posts:upvote', 'groups:posts:downvote', 'groups:topics:delete']; | ||||||
| 			const globalModDisabled = ['groups:moderate']; | 			const globalModDisabled = ['groups:moderate']; | ||||||
| 			const disabled = | 			const disabled = | ||||||
| 				(member === 'guests' && (guestDisabled.includes(priv.name) || priv.name.startsWith('groups:admin:'))) || | 				(member === 'guests' && (guestDisabled.includes(priv.name) || priv.name.startsWith('groups:admin:'))) || | ||||||
| 				(member === 'spiders' && !spidersEnabled.includes(priv.name)) || | 				(member === 'spiders' && !spidersEnabled.includes(priv.name)) || | ||||||
|  | 				(member === 'fediverse' && !fediverseEnabled.includes(priv.name)) || | ||||||
| 				(member === 'Global Moderators' && globalModDisabled.includes(priv.name)); | 				(member === 'Global Moderators' && globalModDisabled.includes(priv.name)); | ||||||
|  |  | ||||||
| 			return ` | 			return ` | ||||||
| 				<td data-privilege="${priv.name}" data-value="${priv.state}" data-type="${priv.type}"> | 				<td data-privilege="${priv.name}" data-value="${priv.state}" data-type="${priv.type}"> | ||||||
| 					<div class="form-check text-center"> | 					<div class="form-check text-center"> | ||||||
| 						<input class="form-check-input float-none" autocomplete="off" type="checkbox"${(priv.state ? ' checked' : '')}${(disabled ? ' disabled="disabled"' : '')} /> | 						<input class="form-check-input float-none${(disabled ? ' d-none"' : '')}" autocomplete="off" type="checkbox"${(priv.state ? ' checked' : '')}${(disabled ? ' disabled="disabled" aria-diabled="true"' : '')} /> | ||||||
| 					</div> | 					</div> | ||||||
| 				</td> | 				</td> | ||||||
| 			`; | 			`; | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ const winston = require('winston'); | |||||||
| const nconf = require('nconf'); | const nconf = require('nconf'); | ||||||
|  |  | ||||||
| const db = require('../database'); | const db = require('../database'); | ||||||
|  | const privileges = require('../privileges'); | ||||||
| const user = require('../user'); | const user = require('../user'); | ||||||
| const posts = require('../posts'); | const posts = require('../posts'); | ||||||
| const topics = require('../topics'); | const topics = require('../topics'); | ||||||
| @@ -17,20 +18,14 @@ const inbox = module.exports; | |||||||
|  |  | ||||||
| inbox.create = async (req) => { | inbox.create = async (req) => { | ||||||
| 	const { object } = req.body; | 	const { object } = req.body; | ||||||
| 	const postData = await activitypub.mocks.post(object); |  | ||||||
|  |  | ||||||
| 	// Temporary, reject non-public notes. | 	// Temporary, reject non-public notes. | ||||||
| 	if (![...postData._activitypub.to, ...postData._activitypub.cc].includes(activitypub._constants.publicAddress)) { | 	if (![...object.to, ...object.cc].includes(activitypub._constants.publicAddress)) { | ||||||
| 		throw new Error('[[error:activitypub.not-implemented]]'); | 		throw new Error('[[error:activitypub.not-implemented]]'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (postData) { | 	const tid = await activitypub.notes.assertTopic(0, object.id); | ||||||
| 		await activitypub.notes.assert(0, [postData]); | 	winston.info(`[activitypub/inbox] Parsing note ${object.id} into topic ${tid}`); | ||||||
| 		const tid = await activitypub.notes.assertTopic(0, postData.pid); |  | ||||||
| 		winston.info(`[activitypub/inbox] Parsing note ${postData.pid} into topic ${tid}`); |  | ||||||
| 	} else { |  | ||||||
| 		winston.warn('[activitypub/inbox] Received object was not a note'); |  | ||||||
| 	} |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| inbox.update = async (req) => { | inbox.update = async (req) => { | ||||||
| @@ -43,6 +38,18 @@ inbox.update = async (req) => { | |||||||
| 		throw new Error('[[error:activitypub.origin-mismatch]]'); | 		throw new Error('[[error:activitypub.origin-mismatch]]'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	const [exists, allowed] = await Promise.all([ | ||||||
|  | 		posts.exists(object.id), | ||||||
|  | 		privileges.posts.can('posts:edit', object.id, activitypub._constants.uid), | ||||||
|  | 	]); | ||||||
|  | 	if (!exists || !allowed) { | ||||||
|  | 		winston.info(`[activitypub/inbox.update] ${object.id} not allowed to be edited.`); | ||||||
|  | 		return activitypub.send('uid', 0, actor, { | ||||||
|  | 			type: 'Reject', | ||||||
|  | 			object, | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	switch (object.type) { | 	switch (object.type) { | ||||||
| 		case 'Note': { | 		case 'Note': { | ||||||
| 			const postData = await activitypub.mocks.post(object); | 			const postData = await activitypub.mocks.post(object); | ||||||
| @@ -70,6 +77,15 @@ inbox.like = async (req) => { | |||||||
| 		throw new Error('[[error:activitypub.invalid-id]]'); | 		throw new Error('[[error:activitypub.invalid-id]]'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	const allowed = await privileges.posts.can('posts:upvote', id, activitypub._constants.uid); | ||||||
|  | 	if (!allowed) { | ||||||
|  | 		winston.info(`[activitypub/inbox.like] ${id} not allowed to be upvoted.`); | ||||||
|  | 		return activitypub.send('uid', 0, actor, { | ||||||
|  | 			type: 'Reject', | ||||||
|  | 			object, | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	winston.info(`[activitypub/inbox/like] id ${id} via ${actor}`); | 	winston.info(`[activitypub/inbox/like] id ${id} via ${actor}`); | ||||||
|  |  | ||||||
| 	await posts.upvote(id, actor); | 	await posts.upvote(id, actor); | ||||||
| @@ -172,17 +188,29 @@ inbox.follow = async (req) => { | |||||||
| 			}, | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} else if (type === 'category') { | 	} else if (type === 'category') { | ||||||
| 		const exists = await categories.exists(id); | 		const [exists, allowed] = await Promise.all([ | ||||||
|  | 			categories.exists(id), | ||||||
|  | 			privileges.categories.can('read', id, 'activitypub._constants.uid'), | ||||||
|  | 		]); | ||||||
| 		if (!exists) { | 		if (!exists) { | ||||||
| 			throw new Error('[[error:invalid-cid]]'); | 			throw new Error('[[error:invalid-cid]]'); | ||||||
| 		} | 		} | ||||||
|  | 		if (!allowed) { | ||||||
|  | 			return activitypub.send('uid', 0, req.body.actor, { | ||||||
|  | 				type: 'Reject', | ||||||
|  | 				object: { | ||||||
|  | 					type: 'Follow', | ||||||
|  | 					actor: req.body.actor, | ||||||
|  | 				}, | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		const watchState = await categories.getWatchState([id], req.body.actor); | 		const watchState = await categories.getWatchState([id], req.body.actor); | ||||||
| 		if (watchState[0] !== categories.watchStates.tracking) { | 		if (watchState[0] !== categories.watchStates.tracking) { | ||||||
| 			await user.setCategoryWatchState(req.body.actor, id, categories.watchStates.tracking); | 			await user.setCategoryWatchState(req.body.actor, id, categories.watchStates.tracking); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		await activitypub.send('cid', id, req.body.actor, { | 		activitypub.send('cid', id, req.body.actor, { | ||||||
| 			type: 'Accept', | 			type: 'Accept', | ||||||
| 			object: { | 			object: { | ||||||
| 				type: 'Follow', | 				type: 'Follow', | ||||||
| @@ -275,6 +303,16 @@ inbox.undo = async (req) => { | |||||||
| 				throw new Error('[[error:invalid-pid]]'); | 				throw new Error('[[error:invalid-pid]]'); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			const allowed = await privileges.posts.can('posts:upvote', id, activitypub._constants.uid); | ||||||
|  | 			if (!allowed) { | ||||||
|  | 				winston.info(`[activitypub/inbox.like] ${id} not allowed to be upvoted.`); | ||||||
|  | 				activitypub.send('uid', 0, actor, { | ||||||
|  | 					type: 'Reject', | ||||||
|  | 					object, | ||||||
|  | 				}); | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			await posts.unvote(id, actor); | 			await posts.unvote(id, actor); | ||||||
| 			break; | 			break; | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ const requestCache = ttl({ ttl: 1000 * 60 * 5 }); // 5 minutes | |||||||
| const ActivityPub = module.exports; | const ActivityPub = module.exports; | ||||||
|  |  | ||||||
| ActivityPub._constants = Object.freeze({ | ActivityPub._constants = Object.freeze({ | ||||||
|  | 	uid: -2, | ||||||
| 	publicAddress: 'https://www.w3.org/ns/activitystreams#Public', | 	publicAddress: 'https://www.w3.org/ns/activitystreams#Public', | ||||||
| }); | }); | ||||||
| ActivityPub._cache = requestCache; | ActivityPub._cache = requestCache; | ||||||
| @@ -163,7 +164,6 @@ ActivityPub.verify = async (req) => { | |||||||
| 		return memo; | 		return memo; | ||||||
| 	}, []).join('\n'); | 	}, []).join('\n'); | ||||||
|  |  | ||||||
|  |  | ||||||
| 	// Verify the signature string via public key | 	// Verify the signature string via public key | ||||||
| 	try { | 	try { | ||||||
| 		// Retrieve public key from remote instance | 		// Retrieve public key from remote instance | ||||||
| @@ -188,6 +188,7 @@ ActivityPub.get = async (type, id, uri) => { | |||||||
| 	const keyData = await ActivityPub.getPrivateKey(type, id); | 	const keyData = await ActivityPub.getPrivateKey(type, id); | ||||||
| 	const headers = id >= 0 ? await ActivityPub.sign(keyData, uri) : {}; | 	const headers = id >= 0 ? await ActivityPub.sign(keyData, uri) : {}; | ||||||
| 	winston.verbose(`[activitypub/get] ${uri}`); | 	winston.verbose(`[activitypub/get] ${uri}`); | ||||||
|  | 	try { | ||||||
| 		const { response, body } = await request.get(uri, { | 		const { response, body } = await request.get(uri, { | ||||||
| 			headers: { | 			headers: { | ||||||
| 				...headers, | 				...headers, | ||||||
| @@ -206,6 +207,10 @@ ActivityPub.get = async (type, id, uri) => { | |||||||
|  |  | ||||||
| 		requestCache.set(cacheKey, body); | 		requestCache.set(cacheKey, body); | ||||||
| 		return body; | 		return body; | ||||||
|  | 	} catch (e) { | ||||||
|  | 		// Handle things like non-json body, etc. | ||||||
|  | 		throw new Error(`[[error:activitypub.get-failed]]`); | ||||||
|  | 	} | ||||||
| }; | }; | ||||||
|  |  | ||||||
| ActivityPub.send = async (type, id, targets, payload) => { | ActivityPub.send = async (type, id, targets, payload) => { | ||||||
| @@ -218,7 +223,7 @@ ActivityPub.send = async (type, id, targets, payload) => { | |||||||
| 	let actor; | 	let actor; | ||||||
| 	switch (type) { | 	switch (type) { | ||||||
| 		case 'uid': { | 		case 'uid': { | ||||||
| 			actor = `${nconf.get('url')}/uid/${id}`; | 			actor = `${nconf.get('url')}${id > 0 ? `/uid/${id}` : '/actor'}`; | ||||||
| 			break; | 			break; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ const winston = require('winston'); | |||||||
| const crypto = require('crypto'); | const crypto = require('crypto'); | ||||||
|  |  | ||||||
| const db = require('../database'); | const db = require('../database'); | ||||||
|  | const privileges = require('../privileges'); | ||||||
| const user = require('../user'); | const user = require('../user'); | ||||||
| const topics = require('../topics'); | const topics = require('../topics'); | ||||||
| const posts = require('../posts'); | const posts = require('../posts'); | ||||||
| @@ -205,8 +206,17 @@ Notes.assertTopic = async (uid, id) => { | |||||||
| 		return tid; | 		return tid; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	const cid = tid ? await topics.getTopicField(tid, 'cid') : -1; | ||||||
|  |  | ||||||
|  | 	// Privilege check for local categories | ||||||
|  | 	const privilege = `topics:${tid ? 'reply' : 'create'}`; | ||||||
|  | 	const allowed = await privileges.categories.can(privilege, cid, activitypub._constants.uid); | ||||||
|  | 	console.log(privilege, cid, allowed); | ||||||
|  | 	if (!allowed) { | ||||||
|  | 		return null; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	tid = tid || utils.generateUUID(); | 	tid = tid || utils.generateUUID(); | ||||||
| 	const cid = await topics.getTopicField(tid, 'cid'); |  | ||||||
|  |  | ||||||
| 	let title = name || utils.decodeHTMLEntities(utils.stripHTMLTags(content)); | 	let title = name || utils.decodeHTMLEntities(utils.stripHTMLTags(content)); | ||||||
| 	if (title.length > 64) { | 	if (title.length > 64) { | ||||||
| @@ -229,7 +239,7 @@ Notes.assertTopic = async (uid, id) => { | |||||||
| 		db.setObject(`topic:${tid}`, { | 		db.setObject(`topic:${tid}`, { | ||||||
| 			tid, | 			tid, | ||||||
| 			uid: authorId, | 			uid: authorId, | ||||||
| 			cid: cid || -1, | 			cid: cid, | ||||||
| 			mainPid, | 			mainPid, | ||||||
| 			title, | 			title, | ||||||
| 			slug: `${tid}/${slugify(title)}`, | 			slug: `${tid}/${slugify(title)}`, | ||||||
|   | |||||||
| @@ -91,7 +91,7 @@ module.exports = function (Categories) { | |||||||
| 			['categories:name', 0, `${data.name.slice(0, 200).toLowerCase()}:${category.cid}`], | 			['categories:name', 0, `${data.name.slice(0, 200).toLowerCase()}:${category.cid}`], | ||||||
| 		]); | 		]); | ||||||
|  |  | ||||||
| 		await privileges.categories.give(result.defaultPrivileges, category.cid, 'registered-users'); | 		await privileges.categories.give(result.defaultPrivileges, category.cid, ['registered-users', 'fediverse']); | ||||||
| 		await privileges.categories.give(result.modPrivileges, category.cid, ['administrators', 'Global Moderators']); | 		await privileges.categories.give(result.modPrivileges, category.cid, ['administrators', 'Global Moderators']); | ||||||
| 		await privileges.categories.give(result.guestPrivileges, category.cid, ['guests', 'spiders']); | 		await privileges.categories.give(result.guestPrivileges, category.cid, ['guests', 'spiders']); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| const nconf = require('nconf'); | const nconf = require('nconf'); | ||||||
|  |  | ||||||
| const meta = require('../../meta'); | const meta = require('../../meta'); | ||||||
|  | const privileges = require('../../privileges'); | ||||||
| const posts = require('../../posts'); | const posts = require('../../posts'); | ||||||
| const topics = require('../../topics'); | const topics = require('../../topics'); | ||||||
| const categories = require('../../categories'); | const categories = require('../../categories'); | ||||||
| @@ -50,9 +51,10 @@ Actors.userBySlug = async function (req, res) { | |||||||
| Actors.note = async function (req, res, next) { | Actors.note = async function (req, res, next) { | ||||||
| 	// technically a note isn't an actor, but it is here purely for organizational purposes. | 	// technically a note isn't an actor, but it is here purely for organizational purposes. | ||||||
| 	// but also, wouldn't it be wild if you could follow a note? lol. | 	// but also, wouldn't it be wild if you could follow a note? lol. | ||||||
|  | 	const allowed = await privileges.posts.can('topics:read', req.params.pid, activitypub._constants.uid); | ||||||
| 	const post = (await posts.getPostSummaryByPids([req.params.pid], req.uid, { stripTags: false })).pop(); | 	const post = (await posts.getPostSummaryByPids([req.params.pid], req.uid, { stripTags: false })).pop(); | ||||||
| 	if (!post) { | 	if (!allowed || !post) { | ||||||
| 		return next('route'); | 		return res.sendStatus(404); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const payload = await activitypub.mocks.note(post); | 	const payload = await activitypub.mocks.note(post); | ||||||
| @@ -61,10 +63,11 @@ Actors.note = async function (req, res, next) { | |||||||
|  |  | ||||||
| Actors.topic = async function (req, res, next) { | Actors.topic = async function (req, res, next) { | ||||||
| 	// When queried, a topic more or less returns the main pid's note representation | 	// When queried, a topic more or less returns the main pid's note representation | ||||||
|  | 	const allowed = await privileges.topics.can('topics:read', req.params.tid, activitypub._constants.uid); | ||||||
| 	const { mainPid, slug } = await topics.getTopicFields(req.params.tid, ['mainPid', 'slug']); | 	const { mainPid, slug } = await topics.getTopicFields(req.params.tid, ['mainPid', 'slug']); | ||||||
| 	const post = (await posts.getPostSummaryByPids([mainPid], req.uid, { stripTags: false })).pop(); | 	const post = (await posts.getPostSummaryByPids([mainPid], req.uid, { stripTags: false })).pop(); | ||||||
| 	if (!post) { | 	if (!allowed || !post) { | ||||||
| 		return next('route'); | 		return res.sendStatus(404); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const payload = await activitypub.mocks.note(post); | 	const payload = await activitypub.mocks.note(post); | ||||||
| @@ -77,8 +80,11 @@ Actors.topic = async function (req, res, next) { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| Actors.category = async function (req, res, next) { | Actors.category = async function (req, res, next) { | ||||||
| 	const exists = await categories.exists(req.params.cid); | 	const [exists, allowed] = await Promise.all([ | ||||||
| 	if (!exists) { | 		categories.exists(req.params.cid), | ||||||
|  | 		privileges.categories.can('find', req.params.cid, activitypub._constants.uid), | ||||||
|  | 	]); | ||||||
|  | 	if (!exists || !allowed) { | ||||||
| 		return next('route'); | 		return next('route'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
|  |  | ||||||
| const nconf = require('nconf'); | const nconf = require('nconf'); | ||||||
|  | const winston = require('winston'); | ||||||
|  |  | ||||||
| const user = require('../../user'); | const user = require('../../user'); | ||||||
| const activitypub = require('../../activitypub'); | const activitypub = require('../../activitypub'); | ||||||
| @@ -115,6 +116,7 @@ Controller.postInbox = async (req, res) => { | |||||||
| 	// Note: underlying methods are internal use only, hence no exposure via src/api | 	// Note: underlying methods are internal use only, hence no exposure via src/api | ||||||
| 	const method = String(req.body.type).toLowerCase(); | 	const method = String(req.body.type).toLowerCase(); | ||||||
| 	if (!activitypub.inbox.hasOwnProperty(method)) { | 	if (!activitypub.inbox.hasOwnProperty(method)) { | ||||||
|  | 		winston.warn(`[activitypub/inbox] Received Activity of type ${method} but unable to handle. Ignoring.`); | ||||||
| 		return res.sendStatus(501); | 		return res.sendStatus(501); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
|  |  | ||||||
| const categories = require('../../categories'); | const categories = require('../../categories'); | ||||||
| const privileges = require('../../privileges'); | const privileges = require('../../privileges'); | ||||||
|  | const utils = require('../../utils'); | ||||||
|  |  | ||||||
| const privilegesController = module.exports; | const privilegesController = module.exports; | ||||||
|  |  | ||||||
| @@ -10,10 +11,10 @@ privilegesController.get = async function (req, res) { | |||||||
| 	const isAdminPriv = req.params.cid === 'admin'; | 	const isAdminPriv = req.params.cid === 'admin'; | ||||||
|  |  | ||||||
| 	let privilegesData; | 	let privilegesData; | ||||||
| 	if (cid > 0) { | 	if (cid === 0) { | ||||||
| 		privilegesData = await privileges.categories.list(cid); |  | ||||||
| 	} else if (cid === 0) { |  | ||||||
| 		privilegesData = await (isAdminPriv ? privileges.admin.list(req.uid) : privileges.global.list()); | 		privilegesData = await (isAdminPriv ? privileges.admin.list(req.uid) : privileges.global.list()); | ||||||
|  | 	} else if (utils.isNumber(cid)) { | ||||||
|  | 		privilegesData = await privileges.categories.list(cid); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const categoriesData = [{ | 	const categoriesData = [{ | ||||||
| @@ -24,6 +25,12 @@ privilegesController.get = async function (req, res) { | |||||||
| 		cid: 'admin', | 		cid: 'admin', | ||||||
| 		name: '[[admin/manage/privileges:admin]]', | 		name: '[[admin/manage/privileges:admin]]', | ||||||
| 		icon: 'fa-lock', | 		icon: 'fa-lock', | ||||||
|  | 	}, { | ||||||
|  | 		cid: -1, | ||||||
|  | 		name: '[[activitypub:category.name]]', | ||||||
|  | 		icon: 'fa-globe', | ||||||
|  | 		bgColor: '#0000ff', | ||||||
|  | 		color: '#ffffff', | ||||||
| 	}]; | 	}]; | ||||||
|  |  | ||||||
| 	let selectedCategory; | 	let selectedCategory; | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ require('./cache')(Groups); | |||||||
|  |  | ||||||
| Groups.BANNED_USERS = 'banned-users'; | Groups.BANNED_USERS = 'banned-users'; | ||||||
|  |  | ||||||
| Groups.ephemeralGroups = ['guests', 'spiders']; | Groups.ephemeralGroups = ['guests', 'spiders', 'fediverse']; | ||||||
|  |  | ||||||
| Groups.systemGroups = [ | Groups.systemGroups = [ | ||||||
| 	'registered-users', | 	'registered-users', | ||||||
| @@ -55,7 +55,7 @@ Groups.removeEphemeralGroups = function (groups) { | |||||||
| 	return groups; | 	return groups; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const isPrivilegeGroupRegex = /^cid:(?:\d+|admin):privileges:[\w\-:]+$/; | const isPrivilegeGroupRegex = /^cid:(?:-?\d+|admin):privileges:[\w\-:]+$/; | ||||||
| Groups.isPrivilegeGroup = function (groupName) { | Groups.isPrivilegeGroup = function (groupName) { | ||||||
| 	return isPrivilegeGroupRegex.test(groupName); | 	return isPrivilegeGroupRegex.test(groupName); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -436,6 +436,37 @@ async function giveGlobalPrivileges() { | |||||||
| 	]), 'Global Moderators'); | 	]), 'Global Moderators'); | ||||||
| 	await privileges.global.give(['groups:view:users', 'groups:view:tags', 'groups:view:groups'], 'guests'); | 	await privileges.global.give(['groups:view:users', 'groups:view:tags', 'groups:view:groups'], 'guests'); | ||||||
| 	await privileges.global.give(['groups:view:users', 'groups:view:tags', 'groups:view:groups'], 'spiders'); | 	await privileges.global.give(['groups:view:users', 'groups:view:tags', 'groups:view:groups'], 'spiders'); | ||||||
|  | 	await privileges.global.give(['groups:view:users'], 'fediverse'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function giveWorldPrivileges() { | ||||||
|  | 	// should match privilege assignment logic in src/categories/create.js EXCEPT commented one liner below | ||||||
|  | 	const privileges = require('./privileges'); | ||||||
|  | 	const defaultPrivileges = [ | ||||||
|  | 		'groups:find', | ||||||
|  | 		'groups:read', | ||||||
|  | 		'groups:topics:read', | ||||||
|  | 		'groups:topics:create', | ||||||
|  | 		'groups:topics:reply', | ||||||
|  | 		'groups:topics:tag', | ||||||
|  | 		'groups:posts:edit', | ||||||
|  | 		'groups:posts:history', | ||||||
|  | 		'groups:posts:delete', | ||||||
|  | 		'groups:posts:upvote', | ||||||
|  | 		'groups:posts:downvote', | ||||||
|  | 		'groups:topics:delete', | ||||||
|  | 	]; | ||||||
|  | 	const modPrivileges = defaultPrivileges.concat([ | ||||||
|  | 		'groups:topics:schedule', | ||||||
|  | 		'groups:posts:view_deleted', | ||||||
|  | 		'groups:purge', | ||||||
|  | 	]); | ||||||
|  | 	const guestPrivileges = ['groups:find', 'groups:read', 'groups:topics:read']; | ||||||
|  |  | ||||||
|  | 	await privileges.categories.give(defaultPrivileges, -1, ['registered-users']); | ||||||
|  | 	await privileges.categories.give(defaultPrivileges.slice(3), -1, ['fediverse']); // different priv set for fediverse | ||||||
|  | 	await privileges.categories.give(modPrivileges, -1, ['administrators', 'Global Moderators']); | ||||||
|  | 	await privileges.categories.give(guestPrivileges, -1, ['guests', 'spiders']); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function createCategories() { | async function createCategories() { | ||||||
| @@ -588,6 +619,7 @@ install.setup = async function () { | |||||||
| 		const adminInfo = await createAdministrator(); | 		const adminInfo = await createAdministrator(); | ||||||
| 		await createGlobalModeratorsGroup(); | 		await createGlobalModeratorsGroup(); | ||||||
| 		await giveGlobalPrivileges(); | 		await giveGlobalPrivileges(); | ||||||
|  | 		await giveWorldPrivileges(); | ||||||
| 		await createMenuItems(); | 		await createMenuItems(); | ||||||
| 		await createWelcomePost(); | 		await createWelcomePost(); | ||||||
| 		await enableDefaultPlugins(); | 		await enableDefaultPlugins(); | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ middleware.validate = async function (req, res, next) { | |||||||
| 	const { actor, object } = req.body; | 	const { actor, object } = req.body; | ||||||
|  |  | ||||||
| 	// Origin checking | 	// Origin checking | ||||||
| 	if (typeof object !== 'string') { | 	if (typeof object !== 'string' && object.hasOwnProperty('id')) { | ||||||
| 		const actorHostname = new URL(actor).hostname; | 		const actorHostname = new URL(actor).hostname; | ||||||
| 		const objectHostname = new URL(object.id).hostname; | 		const objectHostname = new URL(object.id).hostname; | ||||||
| 		if (actorHostname !== objectHostname) { | 		if (actorHostname !== objectHostname) { | ||||||
|   | |||||||
| @@ -154,11 +154,6 @@ privsCategories.can = async function (privilege, cid, uid) { | |||||||
| 		return false; | 		return false; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// temporary |  | ||||||
| 	if (cid === -1) { |  | ||||||
| 		return true; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	const [disabled, isAdmin, isAllowed] = await Promise.all([ | 	const [disabled, isAdmin, isAllowed] = await Promise.all([ | ||||||
| 		categories.getCategoryField(cid, 'disabled'), | 		categories.getCategoryField(cid, 'disabled'), | ||||||
| 		user.isAdministrator(uid), | 		user.isAdministrator(uid), | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ const helpers = module.exports; | |||||||
| const uidToSystemGroup = { | const uidToSystemGroup = { | ||||||
| 	0: 'guests', | 	0: 'guests', | ||||||
| 	'-1': 'spiders', | 	'-1': 'spiders', | ||||||
|  | 	'-2': 'fediverse', | ||||||
| }; | }; | ||||||
|  |  | ||||||
| helpers.isUsersAllowedTo = async function (privilege, uids, cid) { | helpers.isUsersAllowedTo = async function (privilege, uids, cid) { | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								src/upgrades/4.0.0/assign_world_privileges.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/upgrades/4.0.0/assign_world_privileges.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | // const db = require('../../database'); | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  | 	name: 'Assigning default privileges to "World" pseudo-category', | ||||||
|  | 	timestamp: Date.UTC(2024, 1, 22), | ||||||
|  | 	method: async () => { | ||||||
|  | 		const privileges = require('../../privileges'); | ||||||
|  |  | ||||||
|  | 		// should match privilege assignment logic in src/categories/create.js EXCEPT commented one liner below | ||||||
|  | 		const defaultPrivileges = [ | ||||||
|  | 			'groups:find', | ||||||
|  | 			'groups:read', | ||||||
|  | 			'groups:topics:read', | ||||||
|  | 			'groups:topics:create', | ||||||
|  | 			'groups:topics:reply', | ||||||
|  | 			'groups:topics:tag', | ||||||
|  | 			'groups:posts:edit', | ||||||
|  | 			'groups:posts:history', | ||||||
|  | 			'groups:posts:delete', | ||||||
|  | 			'groups:posts:upvote', | ||||||
|  | 			'groups:posts:downvote', | ||||||
|  | 			'groups:topics:delete', | ||||||
|  | 		]; | ||||||
|  | 		const modPrivileges = defaultPrivileges.concat([ | ||||||
|  | 			'groups:topics:schedule', | ||||||
|  | 			'groups:posts:view_deleted', | ||||||
|  | 			'groups:purge', | ||||||
|  | 		]); | ||||||
|  | 		const guestPrivileges = ['groups:find', 'groups:read', 'groups:topics:read']; | ||||||
|  |  | ||||||
|  | 		await privileges.categories.give(defaultPrivileges, -1, ['registered-users']); | ||||||
|  | 		await privileges.categories.give(defaultPrivileges.slice(3), -1, ['fediverse']); // different priv set for fediverse | ||||||
|  | 		await privileges.categories.give(modPrivileges, -1, ['administrators', 'Global Moderators']); | ||||||
|  | 		await privileges.categories.give(guestPrivileges, -1, ['guests', 'spiders']); | ||||||
|  | 	}, | ||||||
|  | }; | ||||||
		Reference in New Issue
	
	Block a user