From dc89ff95d87501770e5d216f37113d3e985f1f16 Mon Sep 17 00:00:00 2001 From: Huskydog9988 <39809509+Huskydog9988@users.noreply.github.com> Date: Thu, 10 Apr 2025 19:57:08 -0400 Subject: [PATCH] feat: make internal objectbackend methods private --- .../api/v1/admin/game/image/index.delete.ts | 8 +-- server/api/v1/object/[id]/index.get.ts | 5 +- server/api/v1/object/[id]/index.head.ts | 5 +- server/internal/news/index.ts | 2 +- server/internal/objects/fsBackend.ts | 1 - server/internal/objects/index.ts | 6 +- server/internal/objects/objectHandler.ts | 68 +++++++++++++------ server/internal/saves/index.ts | 4 +- 8 files changed, 67 insertions(+), 32 deletions(-) diff --git a/server/api/v1/admin/game/image/index.delete.ts b/server/api/v1/admin/game/image/index.delete.ts index feb338e..a0149f2 100644 --- a/server/api/v1/admin/game/image/index.delete.ts +++ b/server/api/v1/admin/game/image/index.delete.ts @@ -3,9 +3,7 @@ import prisma from "~/server/internal/db/database"; import objectHandler from "~/server/internal/objects"; export default defineEventHandler(async (h3) => { - const allowed = await aclManager.allowSystemACL(h3, [ - "game:image:delete", - ]); + const allowed = await aclManager.allowSystemACL(h3, ["game:image:delete"]); if (!allowed) throw createError({ statusCode: 403 }); const body = await readBody(h3); @@ -37,8 +35,8 @@ export default defineEventHandler(async (h3) => { throw createError({ statusCode: 400, statusMessage: "Image not found" }); game.mImageLibrary.splice(imageIndex, 1); - await objectHandler.delete(imageId); - + await objectHandler.deleteWithPermission(imageId); + if (game.mBannerId === imageId) { game.mBannerId = game.mImageLibrary[0]; } diff --git a/server/api/v1/object/[id]/index.get.ts b/server/api/v1/object/[id]/index.get.ts index 1db25ea..cfd6979 100644 --- a/server/api/v1/object/[id]/index.get.ts +++ b/server/api/v1/object/[id]/index.get.ts @@ -13,7 +13,10 @@ export default defineEventHandler(async (h3) => { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag const etagRequestValue = h3.headers.get("If-None-Match"); - const etagActualValue = await objectHandler.fetchHash(id); + const etagActualValue = await objectHandler.fetchHashWithWithPermissions( + id, + userId + ); if (etagRequestValue !== null && etagActualValue === etagRequestValue) { // would compare if etag is valid, but objects should never change setResponseStatus(h3, 304); diff --git a/server/api/v1/object/[id]/index.head.ts b/server/api/v1/object/[id]/index.head.ts index b3f836e..a1e9b51 100644 --- a/server/api/v1/object/[id]/index.head.ts +++ b/server/api/v1/object/[id]/index.head.ts @@ -14,7 +14,10 @@ export default defineEventHandler(async (h3) => { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag const etagRequestValue = h3.headers.get("If-None-Match"); - const etagActualValue = await objectHandler.fetchHash(id); + const etagActualValue = await objectHandler.fetchHashWithWithPermissions( + id, + userId + ); if (etagRequestValue !== null && etagActualValue === etagRequestValue) { // would compare if etag is valid, but objects should never change setResponseStatus(h3, 304); diff --git a/server/internal/news/index.ts b/server/internal/news/index.ts index f47cb9a..96e2c31 100644 --- a/server/internal/news/index.ts +++ b/server/internal/news/index.ts @@ -129,7 +129,7 @@ class NewsManager { where: { id }, }); if (article.image) { - return await objectHandler.delete(article.image); + return await objectHandler.deleteWithPermission(article.image); } return true; } diff --git a/server/internal/objects/fsBackend.ts b/server/internal/objects/fsBackend.ts index 391e14b..a854621 100644 --- a/server/internal/objects/fsBackend.ts +++ b/server/internal/objects/fsBackend.ts @@ -1,5 +1,4 @@ import { - Object, ObjectBackend, ObjectMetadata, ObjectReference, diff --git a/server/internal/objects/index.ts b/server/internal/objects/index.ts index e18afa2..5b27638 100644 --- a/server/internal/objects/index.ts +++ b/server/internal/objects/index.ts @@ -1,3 +1,5 @@ import { FsObjectBackend } from "./fsBackend"; -export const objectHandler = new FsObjectBackend(); -export default objectHandler \ No newline at end of file +import { ObjectHandler } from "./objectHandler"; + +export const objectHandler = new ObjectHandler(new FsObjectBackend()); +export default objectHandler; diff --git a/server/internal/objects/objectHandler.ts b/server/internal/objects/objectHandler.ts index e51cc7b..c5d5fdb 100644 --- a/server/internal/objects/objectHandler.ts +++ b/server/internal/objects/objectHandler.ts @@ -49,21 +49,29 @@ export abstract class ObjectBackend { abstract create( id: string, source: Source, - metadata: ObjectMetadata, + metadata: ObjectMetadata ): Promise; abstract createWithWriteStream( id: string, - metadata: ObjectMetadata, + metadata: ObjectMetadata ): Promise; abstract delete(id: ObjectReference): Promise; abstract fetchMetadata( - id: ObjectReference, + id: ObjectReference ): Promise; abstract writeMetadata( id: ObjectReference, - metadata: ObjectMetadata, + metadata: ObjectMetadata ): Promise; abstract fetchHash(id: ObjectReference): Promise; +} + +export class ObjectHandler { + private backend: ObjectBackend; + + constructor(backend: ObjectBackend) { + this.backend = backend; + } private async fetchMimeType(source: Source) { if (source instanceof ReadableStream) { @@ -87,13 +95,13 @@ export abstract class ObjectBackend { id: string, sourceFetcher: () => Promise, metadata: { [key: string]: string }, - permissions: Array, + permissions: Array ) { const { source, mime } = await this.fetchMimeType(await sourceFetcher()); if (!mime) throw new Error("Unable to calculate MIME type - is the source empty?"); - await this.create(id, source, { + await this.backend.create(id, source, { permissions, userMetadata: metadata, mime, @@ -103,9 +111,9 @@ export abstract class ObjectBackend { async createWithStream( id: string, metadata: { [key: string]: string }, - permissions: Array, + permissions: Array ) { - return this.createWithWriteStream(id, { + return this.backend.createWithWriteStream(id, { permissions, userMetadata: metadata, mime: "application/octet-stream", @@ -113,13 +121,13 @@ export abstract class ObjectBackend { } /** - * Fetches object, but also checks if user has perms to access it - * @param id - * @param userId - * @returns + * Fetches object, but also checks if user has perms to access it + * @param id + * @param userId + * @returns */ async fetchWithPermissions(id: ObjectReference, userId?: string) { - const metadata = await this.fetchMetadata(id); + const metadata = await this.backend.fetchMetadata(id); if (!metadata) return; // We only need one permission, so find instead of filter is faster @@ -137,7 +145,7 @@ export abstract class ObjectBackend { // Because any permission can be read or up, we automatically know we can read this object // So just straight return the object - const source = await this.fetch(id); + const source = await this.backend.fetch(id); if (!source) return undefined; const object: Object = { data: source, @@ -146,6 +154,28 @@ export abstract class ObjectBackend { return object; } + async fetchHashWithWithPermissions(id: ObjectReference, userId?: string) { + const metadata = await this.backend.fetchMetadata(id); + if (!metadata) return; + + // We only need one permission, so find instead of filter is faster + const myPermissions = metadata.permissions.find((e) => { + if (userId !== undefined && e.startsWith(userId)) return true; + if (userId !== undefined && e.startsWith("internal")) return true; + if (e.startsWith("anonymous")) return true; + return false; + }); + + if (!myPermissions) { + // We do not have access to this object + return; + } + + // Because any permission can be read or up, we automatically know we can read this object + // So just straight return the object + return await this.backend.fetchHash(id); + } + // If we need to fetch a remote resource, it doesn't make sense // to immediately fetch the object, *then* check permissions. // Instead the caller can pass a simple anonymous funciton, like @@ -154,9 +184,9 @@ export abstract class ObjectBackend { async writeWithPermissions( id: ObjectReference, sourceFetcher: () => Promise, - userId?: string, + userId?: string ) { - const metadata = await this.fetchMetadata(id); + const metadata = await this.backend.fetchMetadata(id); if (!metadata) return false; const myPermissions = metadata.permissions @@ -178,13 +208,13 @@ export abstract class ObjectBackend { if (!hasPermission) return false; const source = await sourceFetcher(); - const result = await this.write(id, source); + const result = await this.backend.write(id, source); return result; } async deleteWithPermission(id: ObjectReference, userId?: string) { - const metadata = await this.fetchMetadata(id); + const metadata = await this.backend.fetchMetadata(id); if (!metadata) return false; const myPermissions = metadata.permissions @@ -205,7 +235,7 @@ export abstract class ObjectBackend { if (!hasPermission) return false; - const result = await this.delete(id); + const result = await this.backend.delete(id); return result; } } diff --git a/server/internal/saves/index.ts b/server/internal/saves/index.ts index 562e982..c76d52b 100644 --- a/server/internal/saves/index.ts +++ b/server/internal/saves/index.ts @@ -12,7 +12,7 @@ class SaveManager { index: number, objectId: string ) { - await objectHandler.delete(objectId); + await objectHandler.deleteWithPermission(objectId, userId); } async pushSave( @@ -62,7 +62,7 @@ class SaveManager { await Promise.all([hashPromise, uploadStream]); if (!hash) { - await objectHandler.delete(newSaveObjectId); + await objectHandler.deleteWithPermission(newSaveObjectId, userId); throw createError({ statusCode: 500, statusMessage: "Hash failed to generate",