From ac6d3401da9924cf3f8862e4c16e5c703d909d22 Mon Sep 17 00:00:00 2001 From: "Christopher S. Case" Date: Tue, 21 May 2024 23:03:19 -0500 Subject: [PATCH] initial suppliments conversion and move of perms manager to a utility. --- src/server/managers/supplement.ts | 243 +-------------- src/server/resource-access/supplement.ts | 290 ++++++++++++++++++ src/server/routes/accounts.ts | 2 +- src/server/routes/characters.ts | 2 +- src/server/routes/notebook.ts | 2 +- src/server/routes/systems/index.ts | 2 +- src/server/server.ts | 6 +- src/server/{managers => utils}/permissions.ts | 10 +- 8 files changed, 312 insertions(+), 245 deletions(-) create mode 100644 src/server/resource-access/supplement.ts rename src/server/{managers => utils}/permissions.ts (81%) diff --git a/src/server/managers/supplement.ts b/src/server/managers/supplement.ts index cac01e7..518fbb2 100644 --- a/src/server/managers/supplement.ts +++ b/src/server/managers/supplement.ts @@ -2,146 +2,21 @@ // SupplementManager //---------------------------------------------------------------------------------------------------------------------- -import { inspect } from 'node:util'; -import { Knex } from 'knex'; -import logging from '@strata-js/util-logging'; - -// Managers -import * as permMan from './permissions'; - // Models import { Account } from '../../common/interfaces/models/account'; import { Supplement } from '../models/supplement'; +// Resource Access +import * as suppRA from '../resource-access/supplement'; + // Utilities -import { getDB } from '../utils/database'; -import { applyFilters } from '../knex/utils'; import { FilterToken } from '../routes/utils'; -import { camelCaseKeys } from '../utils/misc'; - -// Errors -import { MultipleResultsError, DuplicateSupplementError, NotFoundError, NotAuthorizedError } from '../errors'; - -//---------------------------------------------------------------------------------------------------------------------- - -const logger = logging.getLogger(module.filename); - -//---------------------------------------------------------------------------------------------------------------------- - -async function $checkViewAccess( - query : Knex.QueryBuilder, - systemPrefix ?: string, - account ?: Account -) : Promise -{ - if(account && systemPrefix) - { - // Generally, this is just going to be admins; but hey, why not let admins see everything? - if(!permMan.hasPerm(account, `${ systemPrefix }/canViewContent`)) - { - // Add scoping in - query = query.where(function() - { - this.where({ scope: 'public' }).orWhere({ scope: 'user', owner: account.id }); - }); - } - } - - return query; -} - -async function $checkModAccess( - supplement : Supplement, - systemPrefix : string, - type : string, - account ?: Account -) : Promise -{ - if(account) - { - // Check if we have permission to remove - const hasRight = permMan.hasPerm(account, `${ systemPrefix }/canModifyContent`); - const isOwner = supplement.scope === 'user' && account.id === supplement.owner; - if(!hasRight && !isOwner) - { - throw new NotAuthorizedError( - 'modify', - `${ systemPrefix }/${ type }/${ supplement.name }/${ supplement.id }` - ); - } - } -} - -async function $ensureOfficialAllowed( - supplement : Supplement, - systemPrefix : string, - account ?: Account -) : Promise -{ - if(account) - { - // Check if we have permission to set official - const hasRight = permMan.hasPerm(account, `${ systemPrefix }/canSetOfficial`); - if(!hasRight) - { - supplement.official = false; - } - } -} - -async function $ensureCorrectOwner( - supplement : Supplement, - systemPrefix ?: string, - account ?: Account -) : Promise -{ - if(account && systemPrefix) - { - const hasRight = permMan.hasPerm(account, `${ systemPrefix }/canModifyContent`); - const isOwner = account.id === supplement.owner; - - // If we're not an admin user, and therefore allowed to add/edit content for other people, we have to make sure - // that the owner is set to the account making this call. (Assuming one was passed in, of course.) - if(supplement.scope === 'user' && (isOwner || hasRight)) - { - supplement.owner = account.id; - } - - if(supplement.scope === 'public') - { - supplement.owner = undefined; - } - } - - return supplement; -} //---------------------------------------------------------------------------------------------------------------------- export async function get(id : number, type : string, systemPrefix : string, account ?: Account) : Promise { - const tableName = `${ systemPrefix }_${ type }`; - const db = await getDB(); - const query = db(`${ tableName } as t`) - .select('t.*', 'a.account_id as ownerHash') - .leftJoin('account as a', 'a.account_id', '=', 't.owner') - .where({ id }); - - // Handle retrieval - const supplements = await $checkViewAccess(query, systemPrefix, account); - if(supplements.length > 1) - { - throw new MultipleResultsError(type); - } - else if(supplements.length === 0) - { - throw new NotFoundError(`No ${ type } with id '${ id }' found.`); - } - else - { - const { ownerHash, ...restSupp } = supplements[0]; - return Supplement.fromDB(systemPrefix, type, { ...camelCaseKeys(restSupp), owner: ownerHash }); - } + return suppRA.get(id, type, systemPrefix, account); } export async function list( @@ -150,34 +25,12 @@ export async function list( account ?: Account ) : Promise { - const tableName = `${ systemPrefix }_${ type }`; - const db = await getDB(); - let query = db(`${ tableName } as t`) - .select('t.*', 'a.account_id as ownerHash') - .leftJoin('account as a', 'a.account_id', '=', 't.owner'); - - // Add filters for only what we have access to - query = await $checkViewAccess(query, systemPrefix, account); - - // Apply any filters - query = applyFilters(query, filters); - - return (await query).map((supp) => - { - const { ownerHash, ...restSupp } = supp; - return Supplement.fromDB(systemPrefix, type, { ...camelCaseKeys(restSupp), owner: ownerHash }); - }); + return suppRA.list(filters, type, systemPrefix, account); } export async function exists(id : number, type : string, systemPrefix : string, account ?: Account) : Promise { - // If you're paying attention, you'll realize we also return 'undefined' (and hence false for existence) if there's - // a duplicate. This is fine, it means the DB is somehow screwed, so all bets are off, better to err on the side of - // saying this doesn't exist, rather than allowing for it to be referenced. - const supp = await get(id, type, systemPrefix, account).catch(() => undefined); - - // We only need a boolean. - return !!supp; + return suppRA.exists(id, type, systemPrefix, account); } export async function add( @@ -186,39 +39,7 @@ export async function add( account ?: Account ) : Promise { - const db = await getDB(); - const tableName = `${ systemPrefix }_${ type }`; - const supplement = Supplement.fromJSON(systemPrefix, type, newSupplement); - - // Ensure the supplement's ownership is valid. - await $ensureCorrectOwner(supplement, systemPrefix, account); - - // Ensure that official is allowed to be set. - await $ensureOfficialAllowed(supplement, systemPrefix, account); - - // Make sure we have permission to modify - await $checkModAccess(supplement, systemPrefix, type, account); - - // First, we check to see if we already have one that matches the unique constraint. We do this manually, because - // it's very hard to catch specific sqlite errors reliably, so we do the check explicitly. - const suppExists = (await db(tableName) - .select() - .where({ scope: supplement.scope, owner: supplement.owner ?? null, name: supplement.name })).length > 0; - - if(suppExists) - { - logger.warn( - 'Attempted to add supplement with the same name, scope and owner as an existing one:', - inspect(supplement.toJSON(), { depth: null }) - ); - throw new DuplicateSupplementError(`${ systemPrefix }/${ type }/${ supplement.name }`); - } - - // Now, we insert the supplement - const [ id ] = await db(tableName).insert(supplement.toDB()); - - // Return the inserted supplement - return get(id, type, systemPrefix, account); + return suppRA.add(newSupplement, type, systemPrefix, account); } export async function update( @@ -228,38 +49,7 @@ export async function update( systemPrefix : string, account ?: Account ) : Promise { - const db = await getDB(); - const supplement = await get(id, type, systemPrefix, account); - const tableName = `${ systemPrefix }_${ type }`; - - // Mix the current character with the allowed updates. Note: because we don't know what properties to allow to be - // updatable, we assume everything but ID is. Instead of trying to destructure just id out, we apply everything, - // and re-apply id. It's less efficient, but more explicit. - const allowedUpdate = { - ...supplement.toJSON(), - ...updateSup, - id - }; - - // Make a new character object - const newSupplement = Supplement.fromJSON(systemPrefix, type, allowedUpdate); - - // Ensure the supplement's ownership is valid. - await $ensureCorrectOwner(supplement, systemPrefix, account); - - // Ensure that official is allowed to be set. - await $ensureOfficialAllowed(supplement, systemPrefix, account); - - // Make sure we have permission to modify - await $checkModAccess(newSupplement, systemPrefix, type, account); - - // Now, we update the supplement - await db(tableName) - .update(newSupplement.toDB()) - .where({ id }); - - // Return the updated supplement - return get(id, type, systemPrefix, account); + return suppRA.update(id, updateSup, type, systemPrefix, account); } export async function remove( @@ -269,22 +59,7 @@ export async function remove( account ?: Account ) : Promise<{ status : 'ok' }> { - const db = await getDB(); - const supplement = await get(id, type, systemPrefix, account).catch(() => undefined); - const tableName = `${ systemPrefix }_${ type }`; - - if(supplement) - { - // Make sure we have permission to modify - await $checkModAccess(supplement, systemPrefix, type, account); - - // Delete the supplement - await db(tableName) - .delete() - .where({ id }); - } - - return { status: 'ok' }; + return suppRA.remove(id, type, systemPrefix, account); } //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/resource-access/supplement.ts b/src/server/resource-access/supplement.ts new file mode 100644 index 0000000..04fc22e --- /dev/null +++ b/src/server/resource-access/supplement.ts @@ -0,0 +1,290 @@ +//---------------------------------------------------------------------------------------------------------------------- +// SupplementManager +//---------------------------------------------------------------------------------------------------------------------- + +import { inspect } from 'node:util'; +import { Knex } from 'knex'; +import logging from '@strata-js/util-logging'; + +// Managers + +// Models +import { Account } from '../../common/interfaces/models/account'; +import { Supplement } from '../models/supplement'; + +// Utilities +import { getDB } from '../utils/database'; +import * as permMan from '../utils/permissions'; +import { applyFilters } from '../knex/utils'; +import { FilterToken } from '../routes/utils'; +import { camelCaseKeys } from '../utils/misc'; + +// Errors +import { MultipleResultsError, DuplicateSupplementError, NotFoundError, NotAuthorizedError } from '../errors'; + +//---------------------------------------------------------------------------------------------------------------------- + +const logger = logging.getLogger(module.filename); + +//---------------------------------------------------------------------------------------------------------------------- + +async function $checkViewAccess( + query : Knex.QueryBuilder, + systemPrefix ?: string, + account ?: Account +) : Promise +{ + if(account && systemPrefix) + { + // Generally, this is just going to be admins; but hey, why not let admins see everything? + if(!permMan.hasPerm(account, `${ systemPrefix }/canViewContent`)) + { + // Add scoping in + query = query.where(function() + { + this.where({ scope: 'public' }).orWhere({ scope: 'user', owner: account.id }); + }); + } + } + + return query; +} + +async function $checkModAccess( + supplement : Supplement, + systemPrefix : string, + type : string, + account ?: Account +) : Promise +{ + if(account) + { + // Check if we have permission to remove + const hasRight = permMan.hasPerm(account, `${ systemPrefix }/canModifyContent`); + const isOwner = supplement.scope === 'user' && account.id === supplement.owner; + if(!hasRight && !isOwner) + { + throw new NotAuthorizedError( + 'modify', + `${ systemPrefix }/${ type }/${ supplement.name }/${ supplement.id }` + ); + } + } +} + +async function $ensureOfficialAllowed( + supplement : Supplement, + systemPrefix : string, + account ?: Account +) : Promise +{ + if(account) + { + // Check if we have permission to set official + const hasRight = permMan.hasPerm(account, `${ systemPrefix }/canSetOfficial`); + if(!hasRight) + { + supplement.official = false; + } + } +} + +async function $ensureCorrectOwner( + supplement : Supplement, + systemPrefix ?: string, + account ?: Account +) : Promise +{ + if(account && systemPrefix) + { + const hasRight = permMan.hasPerm(account, `${ systemPrefix }/canModifyContent`); + const isOwner = account.id === supplement.owner; + + // If we're not an admin user, and therefore allowed to add/edit content for other people, we have to make sure + // that the owner is set to the account making this call. (Assuming one was passed in, of course.) + if(supplement.scope === 'user' && (isOwner || hasRight)) + { + supplement.owner = account.id; + } + + if(supplement.scope === 'public') + { + supplement.owner = undefined; + } + } + + return supplement; +} + +//---------------------------------------------------------------------------------------------------------------------- + +export async function get(id : number, type : string, systemPrefix : string, account ?: Account) : Promise +{ + const tableName = `${ systemPrefix }_${ type }`; + const db = await getDB(); + const query = db(`${ tableName } as t`) + .select('t.*', 'a.account_id as ownerHash') + .leftJoin('account as a', 'a.account_id', '=', 't.owner') + .where({ id }); + + // Handle retrieval + const supplements = await $checkViewAccess(query, systemPrefix, account); + if(supplements.length > 1) + { + throw new MultipleResultsError(type); + } + else if(supplements.length === 0) + { + throw new NotFoundError(`No ${ type } with id '${ id }' found.`); + } + else + { + const { ownerHash, ...restSupp } = supplements[0]; + return Supplement.fromDB(systemPrefix, type, { ...camelCaseKeys(restSupp), owner: ownerHash }); + } +} + +export async function list( + filters : Record, + type : string, systemPrefix : string, + account ?: Account +) : Promise +{ + const tableName = `${ systemPrefix }_${ type }`; + const db = await getDB(); + let query = db(`${ tableName } as t`) + .select('t.*', 'a.account_id as ownerHash') + .leftJoin('account as a', 'a.account_id', '=', 't.owner'); + + // Add filters for only what we have access to + query = await $checkViewAccess(query, systemPrefix, account); + + // Apply any filters + query = applyFilters(query, filters); + + return (await query).map((supp) => + { + const { ownerHash, ...restSupp } = supp; + return Supplement.fromDB(systemPrefix, type, { ...camelCaseKeys(restSupp), owner: ownerHash }); + }); +} + +export async function exists(id : number, type : string, systemPrefix : string, account ?: Account) : Promise +{ + // If you're paying attention, you'll realize we also return 'undefined' (and hence false for existence) if there's + // a duplicate. This is fine, it means the DB is somehow screwed, so all bets are off, better to err on the side of + // saying this doesn't exist, rather than allowing for it to be referenced. + const supp = await get(id, type, systemPrefix, account).catch(() => undefined); + + // We only need a boolean. + return !!supp; +} + +export async function add( + newSupplement : Record, + type : string, systemPrefix : string, + account ?: Account +) : Promise +{ + const db = await getDB(); + const tableName = `${ systemPrefix }_${ type }`; + const supplement = Supplement.fromJSON(systemPrefix, type, newSupplement); + + // Ensure the supplement's ownership is valid. + await $ensureCorrectOwner(supplement, systemPrefix, account); + + // Ensure that official is allowed to be set. + await $ensureOfficialAllowed(supplement, systemPrefix, account); + + // Make sure we have permission to modify + await $checkModAccess(supplement, systemPrefix, type, account); + + // First, we check to see if we already have one that matches the unique constraint. We do this manually, because + // it's very hard to catch specific sqlite errors reliably, so we do the check explicitly. + const suppExists = (await db(tableName) + .select() + .where({ scope: supplement.scope, owner: supplement.owner ?? null, name: supplement.name })).length > 0; + + if(suppExists) + { + logger.warn( + 'Attempted to add supplement with the same name, scope and owner as an existing one:', + inspect(supplement.toJSON(), { depth: null }) + ); + throw new DuplicateSupplementError(`${ systemPrefix }/${ type }/${ supplement.name }`); + } + + // Now, we insert the supplement + const [ id ] = await db(tableName).insert(supplement.toDB()); + + // Return the inserted supplement + return get(id, type, systemPrefix, account); +} + +export async function update( + id : number, + updateSup : Record, + type : string, + systemPrefix : string, account ?: Account +) : Promise +{ + const db = await getDB(); + const supplement = await get(id, type, systemPrefix, account); + const tableName = `${ systemPrefix }_${ type }`; + + // Mix the current character with the allowed updates. Note: because we don't know what properties to allow to be + // updatable, we assume everything but ID is. Instead of trying to destructure just id out, we apply everything, + // and re-apply id. It's less efficient, but more explicit. + const allowedUpdate = { + ...supplement.toJSON(), + ...updateSup, + id + }; + + // Make a new character object + const newSupplement = Supplement.fromJSON(systemPrefix, type, allowedUpdate); + + // Ensure the supplement's ownership is valid. + await $ensureCorrectOwner(supplement, systemPrefix, account); + + // Ensure that official is allowed to be set. + await $ensureOfficialAllowed(supplement, systemPrefix, account); + + // Make sure we have permission to modify + await $checkModAccess(newSupplement, systemPrefix, type, account); + + // Now, we update the supplement + await db(tableName) + .update(newSupplement.toDB()) + .where({ id }); + + // Return the updated supplement + return get(id, type, systemPrefix, account); +} + +export async function remove( + id : number, + type : string, + systemPrefix : string, + account ?: Account +) : Promise<{ status : 'ok' }> +{ + const db = await getDB(); + const supplement = await get(id, type, systemPrefix, account).catch(() => undefined); + const tableName = `${ systemPrefix }_${ type }`; + + if(supplement) + { + // Make sure we have permission to modify + await $checkModAccess(supplement, systemPrefix, type, account); + + // Delete the supplement + await db(tableName) + .delete() + .where({ id }); + } + + return { status: 'ok' }; +} + +//---------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/routes/accounts.ts b/src/server/routes/accounts.ts index b73a493..6b1dbd4 100644 --- a/src/server/routes/accounts.ts +++ b/src/server/routes/accounts.ts @@ -7,7 +7,7 @@ import { processRequest } from 'zod-express'; // Managers import * as accountMan from '../managers/account'; -import * as permsMan from '../managers/permissions'; +import * as permsMan from '../utils/permissions'; // Validation import * as AccountValidators from '../engines/validation/models/account'; diff --git a/src/server/routes/characters.ts b/src/server/routes/characters.ts index 4b62918..79ac2c9 100644 --- a/src/server/routes/characters.ts +++ b/src/server/routes/characters.ts @@ -7,7 +7,7 @@ import express from 'express'; // Managers import * as accountMan from '../managers/account'; import * as charMan from '../managers/character'; -import * as permsMan from '../managers/permissions'; +import * as permsMan from '../utils/permissions'; import sysMan from '../managers/system'; // Utils diff --git a/src/server/routes/notebook.ts b/src/server/routes/notebook.ts index f822713..78990a9 100644 --- a/src/server/routes/notebook.ts +++ b/src/server/routes/notebook.ts @@ -8,7 +8,7 @@ import { convertQueryToRecord, ensureAuthenticated, errorHandler } from './utils // Managers import * as noteMan from '../managers/notebook'; -import { hasPerm } from '../managers/permissions'; +import { hasPerm } from '../utils/permissions'; // Logger import logging from '@strata-js/util-logging'; diff --git a/src/server/routes/systems/index.ts b/src/server/routes/systems/index.ts index 77cf082..05dc6d4 100644 --- a/src/server/routes/systems/index.ts +++ b/src/server/routes/systems/index.ts @@ -5,7 +5,7 @@ import express from 'express'; // Managers -import * as permMan from '../../managers/permissions'; +import * as permMan from '../../utils/permissions'; import systemMan from '../../managers/system'; // Utils diff --git a/src/server/server.ts b/src/server/server.ts index af47136..6c400f6 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -18,7 +18,8 @@ import logging from '@strata-js/util-logging'; import { Server as SIOServer } from 'socket.io'; // Managers -import * as permsMan from './managers/permissions'; +import * as rolesMan from './managers/role'; +import * as permsMan from './utils/permissions'; // Session Store import connectSessionKnex from 'connect-session-knex'; @@ -84,7 +85,8 @@ async function main() : Promise // Initialize managers //------------------------------------------------------------------------------------------------------------------ - await permsMan.init(); + const roles = await rolesMan.list(); + await permsMan.loadRoles(roles); //------------------------------------------------------------------------------------------------------------------ // Database diff --git a/src/server/managers/permissions.ts b/src/server/utils/permissions.ts similarity index 81% rename from src/server/managers/permissions.ts rename to src/server/utils/permissions.ts index 04f3c4e..6f37f62 100644 --- a/src/server/managers/permissions.ts +++ b/src/server/utils/permissions.ts @@ -1,20 +1,20 @@ // --------------------------------------------------------------------------------------------------------------------- -// PermissionsManager +// Permissions Util //---------------------------------------------------------------------------------------------------------------------- import tp from 'trivialperms'; -// Managers -import * as rolesMan from './role'; +// Interfaces +import { Role } from '../../common/interfaces/models/role'; // Models import { Account } from '../../common/interfaces/models/account'; //---------------------------------------------------------------------------------------------------------------------- -export async function init() : Promise +export async function loadRoles(roles : Role[]) : Promise { - tp.loadGroups(await rolesMan.list()); + tp.loadGroups(roles); } export function hasPerm(user : Account, perm : string) : boolean