Refactor web access control
This commit is contained in:
		
							parent
							
								
									4ae8da84e0
								
							
						
					
					
						commit
						f98c30cac3
					
				
					 8 changed files with 77 additions and 32 deletions
				
			
		
							
								
								
									
										33
									
								
								src/web/auth.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/web/auth.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| // @ts-check
 | ||||
| 
 | ||||
| const h3 = require("h3") | ||||
| const {db} = require("../passthrough") | ||||
| const {reg} = require("../matrix/read-registration") | ||||
| 
 | ||||
| /** | ||||
|  * Combined guilds managed by Discord account + Matrix account. | ||||
|  * @param {h3.H3Event} event | ||||
|  * @returns {Promise<Set<string>>} guild IDs | ||||
|  */ | ||||
| async function getManagedGuilds(event) { | ||||
| 	const session = await useSession(event) | ||||
| 	const managed = new Set(session.data.managedGuilds || []) | ||||
| 	if (session.data.mxid) { | ||||
| 		const matrixGuilds = db.prepare("SELECT guild_id FROM guild_space INNER JOIN member_cache ON space_id = room_id WHERE mxid = ? AND power_level >= 50").pluck().all(session.data.mxid) | ||||
| 		for (const id of matrixGuilds) { | ||||
| 			managed.add(id) | ||||
| 		} | ||||
| 	} | ||||
| 	return managed | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @param {h3.H3Event} event | ||||
|  * @returns {ReturnType<typeof h3.useSession<{userID?: string, mxid?: string, managedGuilds?: string[], state?: string, selfService?: boolean}>>} | ||||
|  */ | ||||
| function useSession(event) { | ||||
| 	return h3.useSession(event, {password: reg.as_token}) | ||||
| } | ||||
| 
 | ||||
| module.exports.getManagedGuilds = getManagedGuilds | ||||
| module.exports.useSession = useSession | ||||
|  | @ -5,11 +5,13 @@ const fs = require("fs") | |||
| const {join} = require("path") | ||||
| const getRelativePath = require("get-relative-path") | ||||
| const h3 = require("h3") | ||||
| const {defineEventHandler, defaultContentType, setResponseStatus, useSession, getQuery} = h3 | ||||
| const {defineEventHandler, defaultContentType, setResponseStatus, getQuery} = h3 | ||||
| const {compileFile} = require("@cloudrac3r/pug") | ||||
| const pretty = process.argv.join(" ").includes("test") | ||||
| 
 | ||||
| const {reg} = require("../matrix/read-registration") | ||||
| const {sync} = require("../passthrough") | ||||
| /** @type {import("./auth")} */ | ||||
| const auth = sync.require("./auth") | ||||
| 
 | ||||
| // Pug
 | ||||
| 
 | ||||
|  | @ -35,8 +37,8 @@ function render(event, filename, locals) { | |||
| 			const template = compileFile(path, {pretty}) | ||||
| 			pugCache.set(path, async (event, locals) => { | ||||
| 				defaultContentType(event, "text/html; charset=utf-8") | ||||
| 				const session = await useSession(event, {password: reg.as_token}) | ||||
| 				const managed = new Set((session.data.managedGuilds || []).concat(session.data.matrixGuilds || [])) | ||||
| 				const session = await auth.useSession(event) | ||||
| 				const managed = await auth.getManagedGuilds(event) | ||||
| 				const rel = x => getRelativePath(event.path, x) | ||||
| 				return template(Object.assign({}, | ||||
| 					getQuery(event), // Query parameters can be easily accessed on the top level but don't allow them to overwrite anything
 | ||||
|  |  | |||
|  | @ -2,10 +2,12 @@ | |||
| 
 | ||||
| const assert = require("assert/strict") | ||||
| const {z} = require("zod") | ||||
| const {defineEventHandler, useSession, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect, H3Event} = require("h3") | ||||
| const {defineEventHandler, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect, H3Event} = require("h3") | ||||
| 
 | ||||
| const {as, db, sync, select} = require("../../passthrough") | ||||
| const {reg} = require("../../matrix/read-registration") | ||||
| 
 | ||||
| /** @type {import("../auth")} */ | ||||
| const auth = sync.require("../auth") | ||||
| 
 | ||||
| /** | ||||
|  * @param {H3Event} event | ||||
|  | @ -31,8 +33,8 @@ const schema = { | |||
| 
 | ||||
| as.router.post("/api/autocreate", defineEventHandler(async event => { | ||||
| 	const parsedBody = await readValidatedBody(event, schema.autocreate.parse) | ||||
| 	const session = await useSession(event, {password: reg.as_token}) | ||||
| 	if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) | ||||
| 	const managed = await auth.getManagedGuilds(event) | ||||
| 	if (!managed.has(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) | ||||
| 
 | ||||
| 	db.prepare("UPDATE guild_active SET autocreate = ? WHERE guild_id = ?").run(+!!parsedBody.autocreate, parsedBody.guild_id) | ||||
| 
 | ||||
|  | @ -51,8 +53,8 @@ as.router.post("/api/autocreate", defineEventHandler(async event => { | |||
| 
 | ||||
| as.router.post("/api/privacy-level", defineEventHandler(async event => { | ||||
| 	const parsedBody = await readValidatedBody(event, schema.privacyLevel.parse) | ||||
| 	const session = await useSession(event, {password: reg.as_token}) | ||||
| 	if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) | ||||
| 	const managed = await auth.getManagedGuilds(event) | ||||
| 	if (!managed.has(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) | ||||
| 
 | ||||
| 	const createSpace = getCreateSpace(event) | ||||
| 	const i = levels.indexOf(parsedBody.level) | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| const assert = require("assert/strict") | ||||
| const {z} = require("zod") | ||||
| const {H3Event, defineEventHandler, sendRedirect, useSession, createError, getValidatedQuery, readValidatedBody, setResponseHeader} = require("h3") | ||||
| const {H3Event, defineEventHandler, sendRedirect, createError, getValidatedQuery, readValidatedBody, setResponseHeader} = require("h3") | ||||
| const {randomUUID} = require("crypto") | ||||
| const {LRUCache} = require("lru-cache") | ||||
| const Ty = require("../../types") | ||||
|  | @ -13,6 +13,8 @@ const {discord, as, sync, select, from, db} = require("../../passthrough") | |||
| const pugSync = sync.require("../pug-sync") | ||||
| /** @type {import("../../d2m/actions/create-space")} */ | ||||
| const createSpace = sync.require("../../d2m/actions/create-space") | ||||
| /** @type {import("../auth")} */ | ||||
| const auth = require("../auth") | ||||
| const {reg} = require("../../matrix/read-registration") | ||||
| 
 | ||||
| const schema = { | ||||
|  | @ -108,13 +110,14 @@ function getChannelRoomsLinks(guildID, rooms) { | |||
| 
 | ||||
| as.router.get("/guild", defineEventHandler(async event => { | ||||
| 	const {guild_id} = await getValidatedQuery(event, schema.guild.parse) | ||||
| 	const session = await useSession(event, {password: reg.as_token}) | ||||
| 	const session = await auth.useSession(event) | ||||
| 	const managed = await auth.getManagedGuilds(event) | ||||
| 	const row = from("guild_active").join("guild_space", "guild_id", "left").select("space_id", "privacy_level", "autocreate").where({guild_id}).get() | ||||
| 	// @ts-ignore
 | ||||
| 	const guild = discord.guilds.get(guild_id) | ||||
| 
 | ||||
| 	// Permission problems
 | ||||
| 	if (!guild_id || !guild || !(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(guild_id) || !row) { | ||||
| 	if (!guild_id || !guild || !managed.has(guild_id) || !row) { | ||||
| 		return pugSync.render(event, "guild_access_denied.pug", {guild_id, row}) | ||||
| 	} | ||||
| 
 | ||||
|  | @ -159,13 +162,13 @@ as.router.get("/invite", defineEventHandler(async event => { | |||
| 
 | ||||
| as.router.post("/api/invite", defineEventHandler(async event => { | ||||
| 	const parsedBody = await readValidatedBody(event, schema.invite.parse) | ||||
| 	const session = await useSession(event, {password: reg.as_token}) | ||||
| 	const managed = await auth.getManagedGuilds(event) | ||||
| 	const api = getAPI(event) | ||||
| 
 | ||||
| 	// Check guild ID or nonce
 | ||||
| 	if (parsedBody.guild_id) { | ||||
| 		var guild_id = parsedBody.guild_id | ||||
| 		if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't invite users to a guild you don't have Manage Server permissions in"}) | ||||
| 		if (!managed.has(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't invite users to a guild you don't have Manage Server permissions in"}) | ||||
| 	} else if (parsedBody.nonce) { | ||||
| 		if (!validNonce.has(parsedBody.nonce)) throw createError({status: 403, message: "Nonce expired", data: "Nonce means number-used-once, and, well, you tried to use it twice..."}) | ||||
| 		let ok = validNonce.get(parsedBody.nonce) | ||||
|  |  | |||
|  | @ -1,11 +1,13 @@ | |||
| // @ts-check
 | ||||
| 
 | ||||
| const {z} = require("zod") | ||||
| const {defineEventHandler, useSession, createError, readValidatedBody, setResponseHeader, H3Event} = require("h3") | ||||
| const {defineEventHandler, createError, readValidatedBody, setResponseHeader, H3Event} = require("h3") | ||||
| const Ty = require("../../types") | ||||
| const DiscordTypes = require("discord-api-types/v10") | ||||
| 
 | ||||
| const {discord, db, as, sync, select, from} = require("../../passthrough") | ||||
| /** @type {import("../auth")} */ | ||||
| const auth = require("../auth") | ||||
| const {reg} = require("../../matrix/read-registration") | ||||
| 
 | ||||
| /** | ||||
|  | @ -53,12 +55,13 @@ const schema = { | |||
| 
 | ||||
| as.router.post("/api/link-space", defineEventHandler(async event => { | ||||
| 	const parsedBody = await readValidatedBody(event, schema.linkSpace.parse) | ||||
| 	const session = await useSession(event, {password: reg.as_token}) | ||||
| 	const session = await auth.useSession(event) | ||||
| 	const managed = await auth.getManagedGuilds(event) | ||||
| 	const api = getAPI(event) | ||||
| 
 | ||||
| 	// Check guild ID
 | ||||
| 	const guildID = parsedBody.guild_id | ||||
| 	if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) | ||||
| 	if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) | ||||
| 
 | ||||
| 	// Check space ID
 | ||||
| 	if (!session.data.mxid) throw createError({status: 403, message: "Forbidden", data: "Can't link with your Matrix space if you aren't logged in to Matrix"}) | ||||
|  | @ -104,14 +107,14 @@ as.router.post("/api/link-space", defineEventHandler(async event => { | |||
| 
 | ||||
| as.router.post("/api/link", defineEventHandler(async event => { | ||||
| 	const parsedBody = await readValidatedBody(event, schema.link.parse) | ||||
| 	const session = await useSession(event, {password: reg.as_token}) | ||||
| 	const managed = await auth.getManagedGuilds(event) | ||||
| 	const api = getAPI(event) | ||||
| 	const createRoom = getCreateRoom(event) | ||||
| 	const createSpace = getCreateSpace(event) | ||||
| 
 | ||||
| 	// Check guild ID or nonce
 | ||||
| 	const guildID = parsedBody.guild_id | ||||
| 	if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) | ||||
| 	if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) | ||||
| 
 | ||||
| 	// Check guild is bridged
 | ||||
| 	const guild = discord.guilds.get(guildID) | ||||
|  | @ -175,11 +178,11 @@ as.router.post("/api/link", defineEventHandler(async event => { | |||
| 
 | ||||
| as.router.post("/api/unlink", defineEventHandler(async event => { | ||||
| 	const {channel_id, guild_id} = await readValidatedBody(event, schema.unlink.parse) | ||||
| 	const session = await useSession(event, {password: reg.as_token}) | ||||
| 	const managed = await auth.getManagedGuilds(event) | ||||
| 	const createRoom = getCreateRoom(event) | ||||
| 
 | ||||
| 	// Check guild ID or nonce
 | ||||
| 	if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) | ||||
| 	if (!managed.has(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) | ||||
| 
 | ||||
| 	// Check guild exists
 | ||||
| 	const guild = discord.guilds.get(guild_id) | ||||
|  |  | |||
|  | @ -2,16 +2,18 @@ | |||
| 
 | ||||
| const {z} = require("zod") | ||||
| const {randomUUID} = require("crypto") | ||||
| const {defineEventHandler, getValidatedQuery, sendRedirect, readValidatedBody, useSession, createError, getRequestHeader, H3Event} = require("h3") | ||||
| const {defineEventHandler, getValidatedQuery, sendRedirect, readValidatedBody, createError, getRequestHeader, H3Event} = require("h3") | ||||
| const {LRUCache} = require("lru-cache") | ||||
| 
 | ||||
| const {as, db} = require("../../passthrough") | ||||
| const {as} = require("../../passthrough") | ||||
| const {reg} = require("../../matrix/read-registration") | ||||
| 
 | ||||
| const {sync} = require("../../passthrough") | ||||
| const assert = require("assert").strict | ||||
| /** @type {import("../pug-sync")} */ | ||||
| const pugSync = sync.require("../pug-sync") | ||||
| /** @type {import("../auth")} */ | ||||
| const auth = sync.require("../auth") | ||||
| 
 | ||||
| const schema = { | ||||
| 	form: z.object({ | ||||
|  | @ -54,14 +56,12 @@ as.router.get("/log-in-with-matrix", defineEventHandler(async event => { | |||
| 	const token = parsed.data.token | ||||
| 	if (!validToken.has(token)) return sendRedirect(event, `${reg.ooye.bridge_origin}/log-in-with-matrix`, 302) | ||||
| 
 | ||||
| 	const session = await useSession(event, {password: reg.as_token}) | ||||
| 	const session = await auth.useSession(event) | ||||
| 	const mxid = validToken.get(token) | ||||
| 	assert(mxid) | ||||
| 	validToken.delete(token) | ||||
| 
 | ||||
| 	const matrixGuilds = db.prepare("SELECT guild_id FROM guild_space INNER JOIN member_cache ON space_id = room_id WHERE mxid = ? AND power_level >= 50").pluck().all(mxid) | ||||
| 
 | ||||
| 	await session.update({mxid, matrixGuilds}) | ||||
| 	await session.update({mxid}) | ||||
| 
 | ||||
| 	return sendRedirect(event, "./", 302) // open to homepage where they can see they're logged in
 | ||||
| })) | ||||
|  |  | |||
|  | @ -2,14 +2,16 @@ | |||
| 
 | ||||
| const {z} = require("zod") | ||||
| const {randomUUID} = require("crypto") | ||||
| const {defineEventHandler, getValidatedQuery, sendRedirect, useSession, createError} = require("h3") | ||||
| const {defineEventHandler, getValidatedQuery, sendRedirect, createError} = require("h3") | ||||
| const {SnowTransfer} = require("snowtransfer") | ||||
| const DiscordTypes = require("discord-api-types/v10") | ||||
| const fetch = require("node-fetch") | ||||
| const getRelativePath = require("get-relative-path") | ||||
| 
 | ||||
| const {as, db} = require("../../passthrough") | ||||
| const {as, db, sync} = require("../../passthrough") | ||||
| const {id} = require("../../../addbot") | ||||
| /** @type {import("../auth")} */ | ||||
| const auth = sync.require("../auth") | ||||
| const {reg} = require("../../matrix/read-registration") | ||||
| 
 | ||||
| const redirect_uri = `${reg.ooye.bridge_origin}/oauth` | ||||
|  | @ -33,7 +35,7 @@ const schema = { | |||
| } | ||||
| 
 | ||||
| as.router.get("/oauth", defineEventHandler(async event => { | ||||
| 	const session = await useSession(event, {password: reg.as_token}) | ||||
| 	const session = await auth.useSession(event) | ||||
| 	let scope = "guilds" | ||||
| 
 | ||||
| 	const parsedFirstQuery = await getValidatedQuery(event, schema.first.safeParse) | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| const fs = require("fs") | ||||
| const {join} = require("path") | ||||
| const h3 = require("h3") | ||||
| const {defineEventHandler, defaultContentType, getRequestHeader, setResponseHeader, setResponseStatus, useSession, getQuery, handleCacheHeaders} = h3 | ||||
| const {defineEventHandler, defaultContentType, getRequestHeader, setResponseHeader, handleCacheHeaders} = h3 | ||||
| const icons = require("@stackoverflow/stacks-icons") | ||||
| const DiscordTypes = require("discord-api-types/v10") | ||||
| const dUtils = require("../discord/utils") | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue