From 8a6a27c4da23fb8a402818e6edbb5f9b63bb69c2 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 15 Jan 2025 10:54:42 +0200 Subject: [PATCH 01/11] feat: add resources & remove container creation --- modules/storage/src/Storage.ts | 22 +++++++++---- modules/storage/src/admin/adminFile.ts | 8 ----- modules/storage/src/authz/index.ts | 44 ++++++++++++++++++++++++++ modules/storage/src/config/config.ts | 4 --- modules/storage/src/handlers/file.ts | 13 ++------ 5 files changed, 63 insertions(+), 28 deletions(-) diff --git a/modules/storage/src/Storage.ts b/modules/storage/src/Storage.ts index 433e5ecf5..8851abd18 100644 --- a/modules/storage/src/Storage.ts +++ b/modules/storage/src/Storage.ts @@ -39,8 +39,8 @@ import { ManagedModule, } from '@conduitplatform/module-tools'; import { StorageParamAdapter } from './adapter/StorageParamAdapter.js'; -import { FileResource } from './authz/index.js'; import { AdminFileHandlers } from './admin/adminFile.js'; +import * as resources from './authz/index.js'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); @@ -95,11 +95,20 @@ export default class Storage extends ManagedModule { config.aws.accountId = await getAwsAccountId(config); } } + if ( + !isNil( + (config as Config & { allowContainerCreation?: boolean }).allowContainerCreation, + ) + ) { + delete (config as Config & { allowContainerCreation?: boolean }) + .allowContainerCreation; + } return config; } async onConfig() { - if (!ConfigController.getInstance().config.active) { + const config = ConfigController.getInstance().config; + if (!config.active) { this.updateHealth(HealthCheckStatus.NOT_SERVING); } else { await this.grpcSdk.monitorModule( @@ -110,11 +119,12 @@ export default class Storage extends ManagedModule { }, false, ); - const { provider, local, google, azure, aws, aliyun, authorization } = - ConfigController.getInstance().config; + const { provider, local, google, azure, aws, aliyun, authorization } = config; if (authorization.enabled) { - this.grpcSdk.onceModuleUp('authorization', () => { - this.grpcSdk.authorization!.defineResource(FileResource); + this.grpcSdk.onceModuleUp('authorization', async () => { + for (const resource of Object.values(resources)) { + this.grpcSdk.authorization!.defineResource(resource); + } }); } this.storageProvider = createStorageProvider(provider, { diff --git a/modules/storage/src/admin/adminFile.ts b/modules/storage/src/admin/adminFile.ts index 628b72869..4345c894c 100644 --- a/modules/storage/src/admin/adminFile.ts +++ b/modules/storage/src/admin/adminFile.ts @@ -257,18 +257,10 @@ export class AdminFileHandlers { container: string, isPublic?: boolean, ): Promise { - const config = ConfigController.getInstance().config; - // the container is sent from the client const found = await _StorageContainer.getInstance().findOne({ name: container, }); if (!found) { - if (!config.allowContainerCreation) { - throw new GrpcError( - status.PERMISSION_DENIED, - 'Container creation is not allowed!', - ); - } const exists = await this.storageProvider.containerExists(container); if (!exists) { await this.storageProvider.createContainer(container); diff --git a/modules/storage/src/authz/index.ts b/modules/storage/src/authz/index.ts index ab3a35dc8..129ecf9eb 100644 --- a/modules/storage/src/authz/index.ts +++ b/modules/storage/src/authz/index.ts @@ -1,5 +1,49 @@ import { ConduitAuthorizedResource } from '@conduitplatform/grpc-sdk'; +export const ContainerResource = new ConduitAuthorizedResource( + 'Container', + { + owner: ['*'], + reader: ['*'], + editor: ['*'], + }, + { + read: [ + 'owner', + 'reader', + 'editor', + 'reader->read', + 'editor->edit', + 'owner->read', + 'owner->edit', + ], + edit: ['owner', 'editor', 'editor->edit', 'owner->edit'], + delete: ['owner', 'owner->edit'], + }, +); + +export const FolderResource = new ConduitAuthorizedResource( + 'Folder', + { + owner: ['*'], + reader: ['*'], + editor: ['*'], + }, + { + read: [ + 'owner', + 'reader', + 'editor', + 'reader->read', + 'editor->edit', + 'owner->read', + 'owner->edit', + ], + edit: ['owner', 'editor', 'editor->edit', 'owner->edit'], + delete: ['owner', 'owner->edit'], + }, +); + export const FileResource = new ConduitAuthorizedResource( 'File', { diff --git a/modules/storage/src/config/config.ts b/modules/storage/src/config/config.ts index 222a806e3..ec3f58546 100644 --- a/modules/storage/src/config/config.ts +++ b/modules/storage/src/config/config.ts @@ -17,10 +17,6 @@ export default { format: 'String', default: 'conduit', }, - allowContainerCreation: { - format: 'Boolean', - default: true, - }, google: { serviceAccountKeyPath: { format: 'String', diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index 62ed02ab6..1b102aa24 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -56,7 +56,7 @@ export class FileHandlers { if (action === 'create' && request.queryParams.scope) { const allowed = await this.grpcSdk.authorization?.can({ subject: `User:${request.context.user._id}`, - actions: ['read'], + actions: ['edit'], resource: request.params.scope, }); if (!allowed || !allowed.allow) { @@ -325,23 +325,16 @@ export class FileHandlers { container: string, isPublic?: boolean, ): Promise { - const config = ConfigController.getInstance().config; - // the container is sent from the client const found = await _StorageContainer.getInstance().findOne({ name: container, }); if (!found) { - if (!config.allowContainerCreation) { - throw new GrpcError( - status.PERMISSION_DENIED, - 'Container creation is not allowed!', - ); - } const exists = await this.storageProvider.containerExists(container); if (!exists) { - await this.storageProvider.createContainer(container); + throw new GrpcError(status.NOT_FOUND, 'Container does not exist'); } await _StorageContainer.getInstance().create({ + // TODO: think about this name: container, isPublic, }); From fdd1ae1a20b55ff6dda9f6267bfd3fbd983abdc4 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 15 Jan 2025 16:37:16 +0200 Subject: [PATCH 02/11] refactor: full on authz in client file creation routes --- modules/storage/src/Storage.ts | 8 + modules/storage/src/admin/adminFile.ts | 2 +- modules/storage/src/handlers/file.ts | 250 +++++++++++++++++-------- modules/storage/src/utils/index.ts | 34 ++-- 4 files changed, 205 insertions(+), 89 deletions(-) diff --git a/modules/storage/src/Storage.ts b/modules/storage/src/Storage.ts index 8851abd18..521e52722 100644 --- a/modules/storage/src/Storage.ts +++ b/modules/storage/src/Storage.ts @@ -125,6 +125,14 @@ export default class Storage extends ManagedModule { for (const resource of Object.values(resources)) { this.grpcSdk.authorization!.defineResource(resource); } + const defaultContainer = await models._StorageContainer.getInstance().findOne({ + name: config.defaultContainer, + }); + if (!defaultContainer) { + await models._StorageContainer.getInstance().create({ + name: config.defaultContainer, + }); + } }); } this.storageProvider = createStorageProvider(provider, { diff --git a/modules/storage/src/admin/adminFile.ts b/modules/storage/src/admin/adminFile.ts index 4345c894c..35e077fa5 100644 --- a/modules/storage/src/admin/adminFile.ts +++ b/modules/storage/src/admin/adminFile.ts @@ -281,7 +281,7 @@ export class AdminFileHandlers { ): Promise<_StorageFolder[]> { const createdFolders: _StorageFolder[] = []; let folder: _StorageFolder | null = null; - await deepPathHandler(folderPath, async (folderPath, isLast) => { + await deepPathHandler(folderPath, async (folderPath, isFirst, isLast) => { folder = await _StorageFolder .getInstance() .findOne({ name: folderPath, container }); diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index 1b102aa24..4d7789f7d 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -15,7 +15,7 @@ import { _createFileUploadUrl, _updateFile, _updateFileUploadUrl, - deepPathHandler, + getNestedPaths, normalizeFolderPath, storeNewFile, validateName, @@ -46,59 +46,110 @@ export class FileHandlers { async fileAccessCheck( action: 'read' | 'create' | 'edit' | 'delete', - request: Indexable, + userId: string, + scope?: string, file?: File, ) { - if (!request.context.user) { + if (!userId) { throw new GrpcError(status.PERMISSION_DENIED, 'File access is not public'); } - if (ConfigController.getInstance().config.authorization.enabled) { - if (action === 'create' && request.queryParams.scope) { - const allowed = await this.grpcSdk.authorization?.can({ - subject: `User:${request.context.user._id}`, - actions: ['edit'], - resource: request.params.scope, - }); - if (!allowed || !allowed.allow) { - throw new GrpcError( - status.PERMISSION_DENIED, - 'You are not allowed to create files in this scope', - ); - } + if (!ConfigController.getInstance().config.authorization.enabled) { + return; + } + if (action === 'create' && scope) { + const allowed = await this.grpcSdk.authorization?.can({ + subject: 'User:' + userId, + actions: ['edit'], + resource: scope, + }); + if (!allowed || !allowed.allow) { + throw new GrpcError( + status.PERMISSION_DENIED, + 'You are not allowed to create files in this scope', + ); } - if (['read', 'edit', 'delete'].includes(action)) { - const allowed = await this.grpcSdk.authorization?.can({ - subject: `User:${request.context.user._id}`, - actions: [action], - resource: `File:${file!._id}`, - }); - if (!allowed || !allowed.allow) { - throw new GrpcError(status.PERMISSION_DENIED, 'You do not have access to file'); - } + } + if (['read', 'edit', 'delete'].includes(action)) { + const allowed = await this.grpcSdk.authorization?.can({ + subject: 'User:' + userId, + actions: [action], + resource: `File:${file!._id}`, + }); + if (!allowed || !allowed.allow) { + throw new GrpcError(status.PERMISSION_DENIED, 'You do not have access to file'); } } } async fileAccessAdd(file: File, request: Indexable) { - if (ConfigController.getInstance().config.authorization.enabled) { - if (request.queryParams.scope) { - const allowed = await this.grpcSdk.authorization?.can({ - subject: `User:${request.context.user._id}`, - actions: ['read'], - resource: request.params.scope, + if (!ConfigController.getInstance().config.authorization.enabled) { + return; + } + if (request.queryParams.scope) { + const allowed = await this.grpcSdk.authorization?.can({ + subject: `User:${request.context.user._id}`, + actions: ['read'], // TODO: check this out + resource: request.params.scope, + }); + if (!allowed || !allowed.allow) { + throw new GrpcError( + status.PERMISSION_DENIED, + 'You are not allowed to create files in this scope', + ); + } + } + await this.grpcSdk.authorization?.createRelation({ + subject: request.params.scope ?? `User:${request.context.user._id}`, + relation: 'owner', + resource: `File:${file._id}`, + }); + } + + /* Checks if user can create-update file in provided folder */ + async fileAccessEdit(container: string, folder: string, scope: string) { + const folderDoc = await _StorageFolder.getInstance().findOne({ + name: folder, + container, + }); + if (folderDoc) { + const allowed = await this.grpcSdk.authorization?.can({ + subject: scope, + actions: ['edit'], + resource: 'Folder:' + folderDoc._id, + }); + if (!allowed || !allowed.allow) { + throw new GrpcError( + status.PERMISSION_DENIED, + 'You are not allowed to edit files in folder' + folderDoc.name, + ); + } + return; + } else { + // Check previous folders + const nestedPaths = getNestedPaths(folder); + for (let i = nestedPaths.length - 2; i >= 0; i--) { + const prevFolder = await _StorageFolder.getInstance().findOne({ + name: nestedPaths[i], + container, }); - if (!allowed || !allowed.allow) { - throw new GrpcError( - status.PERMISSION_DENIED, - 'You are not allowed to create files in this scope', - ); + if (!prevFolder) { + continue; + } + if (prevFolder) { + const allowed = await this.grpcSdk.authorization?.can({ + subject: scope, + actions: ['edit'], + resource: 'Folder:' + prevFolder._id, + }); + if (!allowed || !allowed.allow) { + throw new GrpcError( + status.PERMISSION_DENIED, + 'You are not allowed to edit files in folder' + prevFolder.name, + ); + } + return; } } - await this.grpcSdk.authorization?.createRelation({ - subject: request.params.scope ?? `User:${request.context.user._id}`, - relation: 'owner', - resource: `File:${file._id}`, - }); } } @@ -115,19 +166,26 @@ export class FileHandlers { } async createFile(call: ParsedRouterRequest): Promise { - const { name, alias, data, container, mimeType, isPublic } = call.request.params; - await this.fileAccessCheck('create', call.request); - const folder = normalizeFolderPath(call.request.params.folder); + const { name, alias, data, container, mimeType, isPublic, scope } = + call.request.params; + const { user } = call.request.context; + await this.fileAccessCheck('create', user, scope); + const config = ConfigController.getInstance().config; const usedContainer = isNil(container) ? config.defaultContainer - : await this.findOrCreateContainer(container, isPublic); + : await this.findContainer(container); + + const folder = normalizeFolderPath(call.request.params.folder); + await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); if (folder !== '/') { - await this.findOrCreateFolders(folder, usedContainer, isPublic); + await this.findOrCreateFolders(folder, usedContainer, isPublic, scope); } const validatedName = await validateName(name, folder, usedContainer); + try { - const file = await storeNewFile(this.storageProvider, { + // TODO: check this try catch + return await storeNewFile(this.storageProvider, { name: validatedName, alias, data, @@ -136,8 +194,6 @@ export class FileHandlers { isPublic, mimeType, }); - await this.fileAccessAdd(file, call.request); - return file; } catch (e) { throw new GrpcError( status.INTERNAL, @@ -147,17 +203,30 @@ export class FileHandlers { } async createFileUploadUrl(call: ParsedRouterRequest): Promise { - const { name, alias, container, size = 0, mimeType, isPublic } = call.request.params; - await this.fileAccessCheck('create', call.request); - const folder = normalizeFolderPath(call.request.params.folder); + const { + name, + alias, + container, + size = 0, + mimeType, + isPublic, + scope, + } = call.request.params; + const { user } = call.request.context; + await this.fileAccessCheck('create', user, scope); + const config = ConfigController.getInstance().config; const usedContainer = isNil(container) ? config.defaultContainer - : await this.findOrCreateContainer(container, isPublic); + : await this.findContainer(container); + + const folder = normalizeFolderPath(call.request.params.folder); + await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); if (folder !== '/') { - await this.findOrCreateFolders(folder, usedContainer, isPublic); + await this.findOrCreateFolders(folder, usedContainer, isPublic, scope); } const validatedName = await validateName(name, folder, usedContainer); + try { const { file, url } = await _createFileUploadUrl(this.storageProvider, { container: usedContainer, @@ -168,7 +237,6 @@ export class FileHandlers { size, mimeType, }); - await this.fileAccessAdd(file, call.request); return { file, url }; } catch (e) { throw new GrpcError( @@ -321,45 +389,67 @@ export class FileHandlers { } } - private async findOrCreateContainer( - container: string, - isPublic?: boolean, - ): Promise { + private async findContainer(container: string): Promise { const found = await _StorageContainer.getInstance().findOne({ name: container, }); if (!found) { - const exists = await this.storageProvider.containerExists(container); - if (!exists) { - throw new GrpcError(status.NOT_FOUND, 'Container does not exist'); - } - await _StorageContainer.getInstance().create({ - // TODO: think about this - name: container, - isPublic, - }); + throw new GrpcError(status.NOT_FOUND, 'Container does not exist'); } return container; } + /* First folder in path will be owned by container + First folder in path, if not pre-existing, will be owned by provided scope + The other subfolders will be owned by their previous folder */ async findOrCreateFolders( folderPath: string, container: string, isPublic?: boolean, + scope?: string, lastExistsHandler?: () => void, ): Promise<_StorageFolder[]> { + const authzOn = ConfigController.getInstance().config.authorization.enabled; + const containerDoc = await _StorageContainer + .getInstance() + .findOne({ name: container }); + if (!containerDoc) { + throw new GrpcError(status.NOT_FOUND, 'Container does not exist'); + } const createdFolders: _StorageFolder[] = []; let folder: _StorageFolder | null = null; - await deepPathHandler(folderPath, async (folderPath, isLast) => { + const nestedPaths = getNestedPaths(folderPath); + + for (let i = 0; i < nestedPaths.length; i++) { + const folderPath = nestedPaths[i]; + const isFirst = i === 0; + const isLast = i === nestedPaths.length - 1; + folder = await _StorageFolder .getInstance() .findOne({ name: folderPath, container }); - if (isNil(folder)) { - folder = await _StorageFolder.getInstance().create({ - name: folderPath, + + let folderScope: string | undefined; + if (authzOn && isFirst) { + folderScope = scope; + } else if (authzOn && !isFirst) { + const prevFolder = (await _StorageFolder.getInstance().findOne({ + name: nestedPaths[i - 1], container, - isPublic, - }); + })) as _StorageFolder; + folderScope = 'Folder:' + prevFolder._id; + } + + if (!folder) { + folder = await _StorageFolder.getInstance().create( + { + name: folderPath, + container, + isPublic, + }, + undefined, + folderScope, + ); createdFolders.push(folder); const exists = await this.storage.container(container).folderExists(folderPath); if (!exists) { @@ -368,7 +458,15 @@ export class FileHandlers { } else if (isLast) { lastExistsHandler?.(); } - }); + + if (authzOn && isFirst) { + await this.grpcSdk.authorization?.createRelation({ + subject: 'Container:' + containerDoc._id, + relation: 'owner', + resource: `Folder:${folder!._id}`, + }); + } + } return createdFolders; } @@ -377,7 +475,7 @@ export class FileHandlers { const newName = name ?? file.name; const newContainer = container ?? file.container; if (newContainer !== file.container) { - await this.findOrCreateContainer(newContainer); + await this.findContainer(newContainer); } const newFolder = isNil(folder) ? file.folder : normalizeFolderPath(folder); if (newFolder !== file.folder && newFolder !== '/') { diff --git a/modules/storage/src/utils/index.ts b/modules/storage/src/utils/index.ts index edae3ad7a..fad59dbd5 100644 --- a/modules/storage/src/utils/index.ts +++ b/modules/storage/src/utils/index.ts @@ -2,7 +2,7 @@ import { GetUserCommand, IAMClient } from '@aws-sdk/client-iam'; import { IFileParams, IStorageProvider, StorageConfig } from '../interfaces/index.js'; import { isNil } from 'lodash-es'; import path from 'path'; -import { File } from '../models/index.js'; +import { _StorageFolder, File } from '../models/index.js'; import { ConduitGrpcSdk, GrpcError } from '@conduitplatform/grpc-sdk'; import { randomUUID } from 'node:crypto'; import { ConfigController } from '@conduitplatform/module-tools'; @@ -43,7 +43,7 @@ export function normalizeFolderPath(folderPath?: string) { return `${path.normalize(folderPath.trim()).replace(/^\/|\/$/g, '')}/`; } -function getNestedPaths(inputPath: string): string[] { +export function getNestedPaths(inputPath: string): string[] { const paths: string[] = []; const strippedPath = !inputPath.trim() ? '' @@ -74,6 +74,8 @@ export async function storeNewFile( params: IFileParams, ): Promise { const { name, alias, data, container, folder, mimeType, isPublic } = params; + const authzOn = ConfigController.getInstance().config.authorization.enabled; + const buffer = Buffer.from(data as string, 'base64'); const size = buffer.byteLength; const fileName = (folder === '/' ? '' : folder) + name; @@ -81,18 +83,26 @@ export async function storeNewFile( const publicUrl = isPublic ? await storageProvider.container(container).getPublicUrl(fileName) : null; + const folderDoc = await _StorageFolder + .getInstance() + .findOne({ name: folder, container }); + ConduitGrpcSdk.Metrics?.increment('files_total'); ConduitGrpcSdk.Metrics?.increment('storage_size_bytes_total', size); - return await File.getInstance().create({ - name, - alias, - mimeType, - folder: folder, - container: container, - size, - isPublic, - url: publicUrl, - }); + return await File.getInstance().create( + { + name, + alias, + mimeType, + folder: folder, + container: container, + size, + isPublic, + url: publicUrl, + }, + undefined, + authzOn ? 'Folder:' + folderDoc!._id : undefined, + ); } export async function _createFileUploadUrl( From 963f5d25077a4b2efff378944587c14d434edb54 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 15 Jan 2025 18:01:38 +0200 Subject: [PATCH 03/11] refactor: completed client api & personal folders --- modules/storage/src/admin/adminFile.ts | 2 +- modules/storage/src/handlers/file.ts | 76 +++++++++++++------------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/modules/storage/src/admin/adminFile.ts b/modules/storage/src/admin/adminFile.ts index 35e077fa5..4345c894c 100644 --- a/modules/storage/src/admin/adminFile.ts +++ b/modules/storage/src/admin/adminFile.ts @@ -281,7 +281,7 @@ export class AdminFileHandlers { ): Promise<_StorageFolder[]> { const createdFolders: _StorageFolder[] = []; let folder: _StorageFolder | null = null; - await deepPathHandler(folderPath, async (folderPath, isFirst, isLast) => { + await deepPathHandler(folderPath, async (folderPath, isLast) => { folder = await _StorageFolder .getInstance() .findOne({ name: folderPath, container }); diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index 4d7789f7d..0aaf71f65 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -2,7 +2,6 @@ import { ConduitGrpcSdk, DatabaseProvider, GrpcError, - Indexable, ParsedRouterRequest, UnparsedRouterResponse, } from '@conduitplatform/grpc-sdk'; @@ -46,7 +45,7 @@ export class FileHandlers { async fileAccessCheck( action: 'read' | 'create' | 'edit' | 'delete', - userId: string, + userId?: string, scope?: string, file?: File, ) { @@ -81,30 +80,6 @@ export class FileHandlers { } } - async fileAccessAdd(file: File, request: Indexable) { - if (!ConfigController.getInstance().config.authorization.enabled) { - return; - } - if (request.queryParams.scope) { - const allowed = await this.grpcSdk.authorization?.can({ - subject: `User:${request.context.user._id}`, - actions: ['read'], // TODO: check this out - resource: request.params.scope, - }); - if (!allowed || !allowed.allow) { - throw new GrpcError( - status.PERMISSION_DENIED, - 'You are not allowed to create files in this scope', - ); - } - } - await this.grpcSdk.authorization?.createRelation({ - subject: request.params.scope ?? `User:${request.context.user._id}`, - relation: 'owner', - resource: `File:${file._id}`, - }); - } - /* Checks if user can create-update file in provided folder */ async fileAccessEdit(container: string, folder: string, scope: string) { const folderDoc = await _StorageFolder.getInstance().findOne({ @@ -160,7 +135,12 @@ export class FileHandlers { } if (!file.isPublic) { - await this.fileAccessCheck('read', call.request, file); + await this.fileAccessCheck( + 'read', + call.request.context.user._id, + call.request.context.scope, + file, + ); } return file; } @@ -176,7 +156,7 @@ export class FileHandlers { ? config.defaultContainer : await this.findContainer(container); - const folder = normalizeFolderPath(call.request.params.folder); + const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd-' + user._id); await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); if (folder !== '/') { await this.findOrCreateFolders(folder, usedContainer, isPublic, scope); @@ -220,7 +200,7 @@ export class FileHandlers { ? config.defaultContainer : await this.findContainer(container); - const folder = normalizeFolderPath(call.request.params.folder); + const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd-' + user._id); await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); if (folder !== '/') { await this.findOrCreateFolders(folder, usedContainer, isPublic, scope); @@ -247,12 +227,12 @@ export class FileHandlers { } async updateFileUploadUrl(call: ParsedRouterRequest): Promise { - const { id, alias, mimeType, size } = call.request.params; + const { id, alias, mimeType, size, scope } = call.request.params; const found = await File.getInstance().findOne({ _id: id }); if (isNil(found)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); } - await this.fileAccessCheck('edit', call.request, found); + await this.fileAccessCheck('edit', call.request.context.user._id, scope, found); const { name, folder, container } = await this.validateFilenameAndContainer( call, found, @@ -275,12 +255,12 @@ export class FileHandlers { } async updateFile(call: ParsedRouterRequest): Promise { - const { id, alias, data, mimeType } = call.request.params; + const { id, alias, data, mimeType, scope } = call.request.params; const found = await File.getInstance().findOne({ _id: id }); if (isNil(found)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); } - await this.fileAccessCheck('edit', call.request, found); + await this.fileAccessCheck('edit', call.request.context.user._id, scope, found); const { name, folder, container } = await this.validateFilenameAndContainer( call, found, @@ -303,22 +283,25 @@ export class FileHandlers { } async deleteFile(call: ParsedRouterRequest): Promise { - if (!isString(call.request.params.id)) { + const { id } = call.request.urlParams; + if (!isString(id)) { throw new GrpcError(status.INVALID_ARGUMENT, 'The provided id is invalid'); } try { - const found = await File.getInstance().findOne({ _id: call.request.params.id }); + const found = await File.getInstance().findOne({ _id: id }); if (isNil(found)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); } - await this.fileAccessCheck('delete', call.request, found); + await this.fileAccessCheck('delete', id, call.request.queryParams.scope, found); + const success = await this.storageProvider .container(found.container) .delete((found.folder === '/' ? '' : found.folder) + found.name); if (!success) { throw new GrpcError(status.INTERNAL, 'File could not be deleted'); } - await File.getInstance().deleteOne({ _id: call.request.params.id }); + await File.getInstance().deleteOne({ _id: id }); + ConduitGrpcSdk.Metrics?.decrement('files_total'); ConduitGrpcSdk.Metrics?.decrement('storage_size_bytes_total', found.size); return { success: true }; @@ -342,7 +325,12 @@ export class FileHandlers { } return { redirect: found.url }; } - await this.fileAccessCheck('read', call.request, found); + await this.fileAccessCheck( + 'read', + call.request.context.user._id, + call.request.queryParams.scope, + found, + ); const url = await this.storageProvider .container(found.container) .getSignedUrl((found.folder === '/' ? '' : found.folder) + found.name); @@ -368,7 +356,12 @@ export class FileHandlers { if (isNil(file)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); } - await this.fileAccessCheck('read', call.request, file); + await this.fileAccessCheck( + 'read', + call.request.context.user._id, + call.request.queryParams.scope, + file, + ); let data: Buffer; const result = await this.storageProvider .container(file.container) @@ -479,6 +472,11 @@ export class FileHandlers { } const newFolder = isNil(folder) ? file.folder : normalizeFolderPath(folder); if (newFolder !== file.folder && newFolder !== '/') { + await this.fileAccessEdit( + newContainer, + newFolder, + call.request.queryParams.scope ?? 'User:' + call.request.context.user._id, + ); await this.findOrCreateFolders(newFolder, newContainer); } const isDataUpdate = From 7cd2782b309c80217bd91719c1c0b8251a2eb394 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 16 Jan 2025 12:57:00 +0200 Subject: [PATCH 04/11] fix: folder & scope cases --- modules/storage/src/handlers/file.ts | 70 ++++++++++++++++++---------- modules/storage/src/utils/index.ts | 44 ++++++++++++----- 2 files changed, 78 insertions(+), 36 deletions(-) diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index 0aaf71f65..8abf36159 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -82,6 +82,9 @@ export class FileHandlers { /* Checks if user can create-update file in provided folder */ async fileAccessEdit(container: string, folder: string, scope: string) { + if (!ConfigController.getInstance().config.authorization.enabled) { + return; + } const folderDoc = await _StorageFolder.getInstance().findOne({ name: folder, container, @@ -156,24 +159,32 @@ export class FileHandlers { ? config.defaultContainer : await this.findContainer(container); - const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd-' + user._id); - await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); + const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd_' + user._id); if (folder !== '/') { - await this.findOrCreateFolders(folder, usedContainer, isPublic, scope); + await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); + await this.findOrCreateFolders( + folder, + usedContainer, + isPublic, + scope ?? 'User:' + user._id, + ); } const validatedName = await validateName(name, folder, usedContainer); try { - // TODO: check this try catch - return await storeNewFile(this.storageProvider, { - name: validatedName, - alias, - data, - container: usedContainer, - folder, - isPublic, - mimeType, - }); + return await storeNewFile( + this.storageProvider, + { + name: validatedName, + alias, + data, + container: usedContainer, + folder, + isPublic, + mimeType, + }, + scope, + ); } catch (e) { throw new GrpcError( status.INTERNAL, @@ -200,23 +211,32 @@ export class FileHandlers { ? config.defaultContainer : await this.findContainer(container); - const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd-' + user._id); - await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); + const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd_' + user._id); if (folder !== '/') { - await this.findOrCreateFolders(folder, usedContainer, isPublic, scope); + await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); + await this.findOrCreateFolders( + folder, + usedContainer, + isPublic, + scope ?? 'User:' + user._id, + ); } const validatedName = await validateName(name, folder, usedContainer); try { - const { file, url } = await _createFileUploadUrl(this.storageProvider, { - container: usedContainer, - folder, - isPublic, - name: validatedName, - alias, - size, - mimeType, - }); + const { file, url } = await _createFileUploadUrl( + this.storageProvider, + { + container: usedContainer, + folder, + isPublic, + name: validatedName, + alias, + size, + mimeType, + }, + scope, + ); return { file, url }; } catch (e) { throw new GrpcError( diff --git a/modules/storage/src/utils/index.ts b/modules/storage/src/utils/index.ts index fad59dbd5..07698e4d7 100644 --- a/modules/storage/src/utils/index.ts +++ b/modules/storage/src/utils/index.ts @@ -72,6 +72,7 @@ export async function deepPathHandler( export async function storeNewFile( storageProvider: IStorageProvider, params: IFileParams, + scope?: string, ): Promise { const { name, alias, data, container, folder, mimeType, isPublic } = params; const authzOn = ConfigController.getInstance().config.authorization.enabled; @@ -87,6 +88,11 @@ export async function storeNewFile( .getInstance() .findOne({ name: folder, container }); + let fileScope: string | undefined; + if (authzOn) { + fileScope = folder === '/' ? scope : 'Folder:' + folderDoc!._id; + } + ConduitGrpcSdk.Metrics?.increment('files_total'); ConduitGrpcSdk.Metrics?.increment('storage_size_bytes_total', size); return await File.getInstance().create( @@ -101,15 +107,18 @@ export async function storeNewFile( url: publicUrl, }, undefined, - authzOn ? 'Folder:' + folderDoc!._id : undefined, + fileScope, ); } export async function _createFileUploadUrl( storageProvider: IStorageProvider, params: IFileParams, + scope?: string, ): Promise<{ file: File; url: string }> { const { name, alias, container, folder, mimeType, isPublic, size } = params; + const authzOn = ConfigController.getInstance().config.authorization.enabled; + const fileName = (folder === '/' ? '' : folder) + name; await storageProvider .container(container) @@ -117,18 +126,31 @@ export async function _createFileUploadUrl( const publicUrl = isPublic ? await storageProvider.container(container).getPublicUrl(fileName) : null; + const folderDoc = await _StorageFolder + .getInstance() + .findOne({ name: folder, container }); + + let fileScope: string | undefined; + if (authzOn) { + fileScope = folder === '/' ? scope : 'Folder:' + folderDoc!._id; + } + ConduitGrpcSdk.Metrics?.increment('files_total'); ConduitGrpcSdk.Metrics?.increment('storage_size_bytes_total', size); - const file = await File.getInstance().create({ - name, - alias, - mimeType, - size, - folder: folder, - container: container, - isPublic, - url: publicUrl, - }); + const file = await File.getInstance().create( + { + name, + alias, + mimeType, + size, + folder: folder, + container: container, + isPublic, + url: publicUrl, + }, + undefined, + fileScope, + ); const url = (await storageProvider .container(container) .getUploadUrl(fileName)) as string; From 748171cff943cf0791f93d4bf875715f655ebf31 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 16 Jan 2025 17:19:17 +0200 Subject: [PATCH 05/11] refactor: missing cases in client routes --- modules/storage/src/admin/adminFile.ts | 33 +--- modules/storage/src/admin/index.ts | 5 + modules/storage/src/handlers/file.ts | 166 +++++++---------- modules/storage/src/utils/index.ts | 248 ++++++++++++++++++++----- 4 files changed, 274 insertions(+), 178 deletions(-) diff --git a/modules/storage/src/admin/adminFile.ts b/modules/storage/src/admin/adminFile.ts index 4345c894c..8abbbaa7c 100644 --- a/modules/storage/src/admin/adminFile.ts +++ b/modules/storage/src/admin/adminFile.ts @@ -53,7 +53,8 @@ export class AdminFileHandlers { } async createFile(call: ParsedRouterRequest): Promise { - const { name, alias, data, container, mimeType, isPublic } = call.request.params; + const { name, alias, data, container, mimeType, isPublic, scope } = + call.request.params; const folder = normalizeFolderPath(call.request.params.folder); const config = ConfigController.getInstance().config; const usedContainer = isNil(container) @@ -273,36 +274,6 @@ export class AdminFileHandlers { return container; } - async findOrCreateFolders( - folderPath: string, - container: string, - isPublic?: boolean, - lastExistsHandler?: () => void, - ): Promise<_StorageFolder[]> { - const createdFolders: _StorageFolder[] = []; - let folder: _StorageFolder | null = null; - await deepPathHandler(folderPath, async (folderPath, isLast) => { - folder = await _StorageFolder - .getInstance() - .findOne({ name: folderPath, container }); - if (isNil(folder)) { - folder = await _StorageFolder.getInstance().create({ - name: folderPath, - container, - isPublic, - }); - createdFolders.push(folder); - const exists = await this.storage.container(container).folderExists(folderPath); - if (!exists) { - await this.storage.container(container).createFolder(folderPath); - } - } else if (isLast) { - lastExistsHandler?.(); - } - }); - return createdFolders; - } - private async validateFilenameAndContainer(call: ParsedRouterRequest, file: File) { const { name, folder, container } = call.request.params; const newName = name ?? file.name; diff --git a/modules/storage/src/admin/index.ts b/modules/storage/src/admin/index.ts index 1f70ef419..36d190b54 100644 --- a/modules/storage/src/admin/index.ts +++ b/modules/storage/src/admin/index.ts @@ -12,6 +12,7 @@ import { ConduitBoolean, ConduitNumber, ConduitString, + ConfigController, GrpcServer, RoutingManager, } from '@conduitplatform/module-tools'; @@ -192,6 +193,7 @@ export class AdminRoutes { private registerAdminRoutes() { this.routingManager.clear(); + const authzEnabled = ConfigController.getInstance().config.authorization.enabled; this.routingManager.route( { path: '/files/:id', @@ -238,6 +240,9 @@ export class AdminRoutes { mimeType: ConduitString.Optional, isPublic: ConduitBoolean.Optional, }, + queryParams: { + ...(authzEnabled && { scope: { type: TYPE.String, required: false } }), + }, }, new ConduitRouteReturnDefinition('CreateFile', File.name), this.fileHandlers.createFile.bind(this.fileHandlers), diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index 8abf36159..1ac00ff3f 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -14,6 +14,7 @@ import { _createFileUploadUrl, _updateFile, _updateFileUploadUrl, + findOrCreateFolders, getNestedPaths, normalizeFolderPath, storeNewFile, @@ -75,7 +76,10 @@ export class FileHandlers { resource: `File:${file!._id}`, }); if (!allowed || !allowed.allow) { - throw new GrpcError(status.PERMISSION_DENIED, 'You do not have access to file'); + throw new GrpcError( + status.PERMISSION_DENIED, + `You are not allowed to ${action} this file`, + ); } } } @@ -162,7 +166,9 @@ export class FileHandlers { const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd_' + user._id); if (folder !== '/') { await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); - await this.findOrCreateFolders( + await findOrCreateFolders( + this.grpcSdk, + this.storageProvider, folder, usedContainer, isPublic, @@ -173,6 +179,7 @@ export class FileHandlers { try { return await storeNewFile( + this.grpcSdk, this.storageProvider, { name: validatedName, @@ -183,7 +190,7 @@ export class FileHandlers { isPublic, mimeType, }, - scope, + scope ?? 'User:' + user._id, ); } catch (e) { throw new GrpcError( @@ -214,7 +221,9 @@ export class FileHandlers { const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd_' + user._id); if (folder !== '/') { await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); - await this.findOrCreateFolders( + await findOrCreateFolders( + this.grpcSdk, + this.storageProvider, folder, usedContainer, isPublic, @@ -225,6 +234,7 @@ export class FileHandlers { try { const { file, url } = await _createFileUploadUrl( + this.grpcSdk, this.storageProvider, { container: usedContainer, @@ -235,7 +245,7 @@ export class FileHandlers { size, mimeType, }, - scope, + scope ?? 'User:' + user._id, ); return { file, url }; } catch (e) { @@ -248,24 +258,32 @@ export class FileHandlers { async updateFileUploadUrl(call: ParsedRouterRequest): Promise { const { id, alias, mimeType, size, scope } = call.request.params; + const { user } = call.request.context; + const found = await File.getInstance().findOne({ _id: id }); if (isNil(found)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); } - await this.fileAccessCheck('edit', call.request.context.user._id, scope, found); + await this.fileAccessCheck('edit', user._id, scope, found); const { name, folder, container } = await this.validateFilenameAndContainer( call, found, ); try { - return await _updateFileUploadUrl(this.storageProvider, found, { - name, - alias, - folder, - container, - mimeType: mimeType ?? found.mimeType, - size, - }); + return await _updateFileUploadUrl( + this.grpcSdk, + this.storageProvider, + found, + { + name, + alias, + folder, + container, + mimeType: mimeType ?? found.mimeType, + size, + }, + scope ?? 'User:' + user._id, + ); } catch (e) { throw new GrpcError( status.INTERNAL, @@ -276,24 +294,32 @@ export class FileHandlers { async updateFile(call: ParsedRouterRequest): Promise { const { id, alias, data, mimeType, scope } = call.request.params; + const { user } = call.request.context; + const found = await File.getInstance().findOne({ _id: id }); if (isNil(found)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); } - await this.fileAccessCheck('edit', call.request.context.user._id, scope, found); + await this.fileAccessCheck('edit', user._id, scope, found); const { name, folder, container } = await this.validateFilenameAndContainer( call, found, ); try { - return await _updateFile(this.storageProvider, found, { - name, - alias, - folder, - container, - data: Buffer.from(data, 'base64'), - mimeType: mimeType ?? found.mimeType, - }); + return await _updateFile( + this.grpcSdk, + this.storageProvider, + found, + { + name, + alias, + folder, + container, + data: Buffer.from(data, 'base64'), + mimeType: mimeType ?? found.mimeType, + }, + scope ?? 'User:' + user._id, + ); } catch (e) { throw new GrpcError( status.INTERNAL, @@ -304,6 +330,7 @@ export class FileHandlers { async deleteFile(call: ParsedRouterRequest): Promise { const { id } = call.request.urlParams; + const { scope } = call.request.queryParams; if (!isString(id)) { throw new GrpcError(status.INVALID_ARGUMENT, 'The provided id is invalid'); } @@ -312,7 +339,7 @@ export class FileHandlers { if (isNil(found)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); } - await this.fileAccessCheck('delete', id, call.request.queryParams.scope, found); + await this.fileAccessCheck('delete', id, scope, found); const success = await this.storageProvider .container(found.container) @@ -321,9 +348,14 @@ export class FileHandlers { throw new GrpcError(status.INTERNAL, 'File could not be deleted'); } await File.getInstance().deleteOne({ _id: id }); - ConduitGrpcSdk.Metrics?.decrement('files_total'); ConduitGrpcSdk.Metrics?.decrement('storage_size_bytes_total', found.size); + + if (ConfigController.getInstance().config.authorization.enabled) { + await this.grpcSdk.authorization?.deleteAllRelations({ + resource: 'File:' + id, + }); + } return { success: true }; } catch (e) { throw new GrpcError( @@ -412,79 +444,8 @@ export class FileHandlers { return container; } - /* First folder in path will be owned by container - First folder in path, if not pre-existing, will be owned by provided scope - The other subfolders will be owned by their previous folder */ - async findOrCreateFolders( - folderPath: string, - container: string, - isPublic?: boolean, - scope?: string, - lastExistsHandler?: () => void, - ): Promise<_StorageFolder[]> { - const authzOn = ConfigController.getInstance().config.authorization.enabled; - const containerDoc = await _StorageContainer - .getInstance() - .findOne({ name: container }); - if (!containerDoc) { - throw new GrpcError(status.NOT_FOUND, 'Container does not exist'); - } - const createdFolders: _StorageFolder[] = []; - let folder: _StorageFolder | null = null; - const nestedPaths = getNestedPaths(folderPath); - - for (let i = 0; i < nestedPaths.length; i++) { - const folderPath = nestedPaths[i]; - const isFirst = i === 0; - const isLast = i === nestedPaths.length - 1; - - folder = await _StorageFolder - .getInstance() - .findOne({ name: folderPath, container }); - - let folderScope: string | undefined; - if (authzOn && isFirst) { - folderScope = scope; - } else if (authzOn && !isFirst) { - const prevFolder = (await _StorageFolder.getInstance().findOne({ - name: nestedPaths[i - 1], - container, - })) as _StorageFolder; - folderScope = 'Folder:' + prevFolder._id; - } - - if (!folder) { - folder = await _StorageFolder.getInstance().create( - { - name: folderPath, - container, - isPublic, - }, - undefined, - folderScope, - ); - createdFolders.push(folder); - const exists = await this.storage.container(container).folderExists(folderPath); - if (!exists) { - await this.storage.container(container).createFolder(folderPath); - } - } else if (isLast) { - lastExistsHandler?.(); - } - - if (authzOn && isFirst) { - await this.grpcSdk.authorization?.createRelation({ - subject: 'Container:' + containerDoc._id, - relation: 'owner', - resource: `Folder:${folder!._id}`, - }); - } - } - return createdFolders; - } - private async validateFilenameAndContainer(call: ParsedRouterRequest, file: File) { - const { name, folder, container } = call.request.params; + const { name, folder, container, scope } = call.request.params; const newName = name ?? file.name; const newContainer = container ?? file.container; if (newContainer !== file.container) { @@ -495,9 +456,16 @@ export class FileHandlers { await this.fileAccessEdit( newContainer, newFolder, - call.request.queryParams.scope ?? 'User:' + call.request.context.user._id, + scope ?? 'User:' + call.request.context.user._id, + ); + await findOrCreateFolders( + this.grpcSdk, + this.storageProvider, + newFolder, + newContainer, + file.isPublic, + scope ?? 'User:' + call.request.context.user._id, ); - await this.findOrCreateFolders(newFolder, newContainer); } const isDataUpdate = newName === file.name && diff --git a/modules/storage/src/utils/index.ts b/modules/storage/src/utils/index.ts index 07698e4d7..60f8e462b 100644 --- a/modules/storage/src/utils/index.ts +++ b/modules/storage/src/utils/index.ts @@ -2,7 +2,7 @@ import { GetUserCommand, IAMClient } from '@aws-sdk/client-iam'; import { IFileParams, IStorageProvider, StorageConfig } from '../interfaces/index.js'; import { isNil } from 'lodash-es'; import path from 'path'; -import { _StorageFolder, File } from '../models/index.js'; +import { _StorageContainer, _StorageFolder, File } from '../models/index.js'; import { ConduitGrpcSdk, GrpcError } from '@conduitplatform/grpc-sdk'; import { randomUUID } from 'node:crypto'; import { ConfigController } from '@conduitplatform/module-tools'; @@ -69,13 +69,166 @@ export async function deepPathHandler( } } +export async function createFileRelations( + grpcSdk: ConduitGrpcSdk, + file: File, + scope: string, +) { + if (file.folder === '/') { + const containerDoc = await _StorageContainer + .getInstance() + .findOne({ name: file.container }); + await grpcSdk.authorization?.createRelation({ + subject: 'Container:' + containerDoc!._id, + relation: 'owner', + resource: `File:${file._id}`, + }); + await grpcSdk.authorization?.createRelation({ + subject: scope!, + relation: 'owner', + resource: `File:${file._id}`, + }); + } else { + const folderDoc = await _StorageFolder + .getInstance() + .findOne({ name: file.folder, container: file.container }); + await grpcSdk.authorization?.createRelation({ + subject: 'Folder:' + folderDoc!._id, + relation: 'owner', + resource: `File:${file._id}`, + }); + } +} + +export async function updateFileRelations( + grpcSdk: ConduitGrpcSdk, + file: File, + updatedFile: File, + scope: string, +) { + if (updatedFile.container !== file.container) { + const prevContainer = await _StorageContainer + .getInstance() + .findOne({ name: file.container }); + await grpcSdk.authorization?.deleteRelation({ + subject: 'Container:' + prevContainer!._id, + relation: 'owner', + resource: `File:${updatedFile._id}`, + }); // TODO: add catch + } + if (updatedFile.folder === file.folder) { + return; + } + + if (updatedFile.folder === '/') { + const newContainer = await _StorageContainer + .getInstance() + .findOne({ name: updatedFile.container }); + await grpcSdk.authorization?.createRelation({ + subject: 'Container:' + newContainer!._id, + relation: 'owner', + resource: `File:${updatedFile._id}`, + }); + await grpcSdk.authorization?.createRelation({ + subject: scope!, + relation: 'owner', + resource: `File:${updatedFile._id}`, + }); + } else { + const folderDoc = await _StorageFolder + .getInstance() + .findOne({ name: updatedFile.folder, container: updatedFile.container }); + await grpcSdk.authorization?.createRelation({ + subject: 'Folder:' + folderDoc!._id, + relation: 'owner', + resource: `File:${updatedFile._id}`, + }); + } + + if (file.folder !== '/') { + const prevFolder = await _StorageFolder + .getInstance() + .findOne({ name: file.folder, container: file.container }); + await grpcSdk.authorization?.deleteRelation({ + subject: 'Folder:' + prevFolder!._id, + relation: 'owner', + resource: `File:${updatedFile._id}`, + }); + } +} + +/* First folder in nested path will be owned by container + First folder in nested path, if not pre-existing, will be owned by provided scope + The other subfolders will be owned by their previous folder */ +export async function findOrCreateFolders( + grpcSdk: ConduitGrpcSdk, + storageProvider: IStorageProvider, + folderPath: string, + container: string, + isPublic?: boolean, + scope?: string, +): Promise<_StorageFolder[]> { + const authzEnabled = ConfigController.getInstance().config.authorization.enabled; + const containerDoc = await _StorageContainer.getInstance().findOne({ name: container }); + if (!containerDoc) { + throw new GrpcError(status.NOT_FOUND, 'Container does not exist'); + } + const createdFolders: _StorageFolder[] = []; + let folder: _StorageFolder | null = null; + const nestedPaths = getNestedPaths(folderPath); + + for (let i = 0; i < nestedPaths.length; i++) { + const folderPath = nestedPaths[i]; + const isFirst = i === 0; + folder = await _StorageFolder.getInstance().findOne({ name: folderPath, container }); + + let folderScope: string | undefined; + if (authzEnabled && isFirst) { + folderScope = scope; + } else if (authzEnabled && !isFirst) { + const prevFolder = (await _StorageFolder.getInstance().findOne({ + name: nestedPaths[i - 1], + container, + })) as _StorageFolder; + folderScope = 'Folder:' + prevFolder._id; + } + + if (!folder) { + folder = await _StorageFolder.getInstance().create( + { + name: folderPath, + container, + isPublic, + }, + undefined, + folderScope, + ); + createdFolders.push(folder); + const exists = await storageProvider.container(container).folderExists(folderPath); + if (!exists) { + await storageProvider.container(container).createFolder(folderPath); + } + } + + if (authzEnabled && isFirst) { + await grpcSdk.authorization?.createRelation({ + subject: 'Container:' + containerDoc._id, + relation: 'owner', + resource: `Folder:${folder!._id}`, + }); + } + } + return createdFolders; +} + export async function storeNewFile( + grpcSdk: ConduitGrpcSdk, storageProvider: IStorageProvider, params: IFileParams, scope?: string, ): Promise { const { name, alias, data, container, folder, mimeType, isPublic } = params; - const authzOn = ConfigController.getInstance().config.authorization.enabled; + const authzEnabled = ConfigController.getInstance().config.authorization.enabled; const buffer = Buffer.from(data as string, 'base64'); const size = buffer.byteLength; @@ -84,40 +237,33 @@ export async function storeNewFile( const publicUrl = isPublic ? await storageProvider.container(container).getPublicUrl(fileName) : null; - const folderDoc = await _StorageFolder - .getInstance() - .findOne({ name: folder, container }); - - let fileScope: string | undefined; - if (authzOn) { - fileScope = folder === '/' ? scope : 'Folder:' + folderDoc!._id; - } ConduitGrpcSdk.Metrics?.increment('files_total'); ConduitGrpcSdk.Metrics?.increment('storage_size_bytes_total', size); - return await File.getInstance().create( - { - name, - alias, - mimeType, - folder: folder, - container: container, - size, - isPublic, - url: publicUrl, - }, - undefined, - fileScope, - ); + const file = await File.getInstance().create({ + name, + alias, + mimeType, + folder: folder, + container: container, + size, + isPublic, + url: publicUrl, + }); + if (authzEnabled) { + await createFileRelations(grpcSdk, file, scope!); + } + return file; } export async function _createFileUploadUrl( + grpcSdk: ConduitGrpcSdk, storageProvider: IStorageProvider, params: IFileParams, scope?: string, ): Promise<{ file: File; url: string }> { const { name, alias, container, folder, mimeType, isPublic, size } = params; - const authzOn = ConfigController.getInstance().config.authorization.enabled; + const authzEnabled = ConfigController.getInstance().config.authorization.enabled; const fileName = (folder === '/' ? '' : folder) + name; await storageProvider @@ -126,31 +272,22 @@ export async function _createFileUploadUrl( const publicUrl = isPublic ? await storageProvider.container(container).getPublicUrl(fileName) : null; - const folderDoc = await _StorageFolder - .getInstance() - .findOne({ name: folder, container }); - - let fileScope: string | undefined; - if (authzOn) { - fileScope = folder === '/' ? scope : 'Folder:' + folderDoc!._id; - } ConduitGrpcSdk.Metrics?.increment('files_total'); ConduitGrpcSdk.Metrics?.increment('storage_size_bytes_total', size); - const file = await File.getInstance().create( - { - name, - alias, - mimeType, - size, - folder: folder, - container: container, - isPublic, - url: publicUrl, - }, - undefined, - fileScope, - ); + const file = await File.getInstance().create({ + name, + alias, + mimeType, + size, + folder: folder, + container: container, + isPublic, + url: publicUrl, + }); + if (authzEnabled) { + await createFileRelations(grpcSdk, file, scope!); + } const url = (await storageProvider .container(container) .getUploadUrl(fileName)) as string; @@ -161,13 +298,17 @@ export async function _createFileUploadUrl( } export async function _updateFile( + grpcSdk: ConduitGrpcSdk, storageProvider: IStorageProvider, file: File, params: IFileParams, + scope?: string, ): Promise { const { name, alias, data, folder, container, mimeType } = params; const onlyDataUpdate = name === file.name && folder === file.folder && container === file.container; + const authzEnabled = ConfigController.getInstance().config.authorization.enabled; + await storageProvider .container(container) .store((folder === '/' ? '' : folder) + name, data, file.isPublic); @@ -189,19 +330,26 @@ export async function _updateFile( url, mimeType, })) as File; + if (authzEnabled) { + await updateFileRelations(grpcSdk, file, updatedFile, scope!); + } updateFileMetrics(file.size, (data as Buffer).byteLength); return updatedFile; } export async function _updateFileUploadUrl( + grpcSdk: ConduitGrpcSdk, storageProvider: IStorageProvider, file: File, params: IFileParams, + scope?: string, ): Promise<{ file: File; url: string }> { const { name, alias, folder, container, mimeType, size } = params; - let updatedFile; const onlyDataUpdate = name === file.name && folder === file.folder && container === file.container; + const authzEnabled = ConfigController.getInstance().config.authorization.enabled; + let updatedFile; + if (onlyDataUpdate) { updatedFile = await File.getInstance().findByIdAndUpdate(file._id, { mimeType, @@ -224,6 +372,7 @@ export async function _updateFileUploadUrl( .container(container) .getPublicUrl((folder === '/' ? '' : folder) + name) : null; + updatedFile = await File.getInstance().findByIdAndUpdate(file._id, { name, alias, @@ -233,6 +382,9 @@ export async function _updateFileUploadUrl( mimeType, ...{ size: size ?? file.size }, }); + if (authzEnabled) { + await updateFileRelations(grpcSdk, file, updatedFile!, scope!); + } } if (!isNil(size)) updateFileMetrics(file.size, size!); const uploadUrl = (await storageProvider From 18b19242abe76bcc1cd1ba6f80cd216823f1a56e Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 16 Jan 2025 18:49:15 +0200 Subject: [PATCH 06/11] refactor: authz in admin routes --- modules/storage/src/admin/adminFile.ts | 149 +++++++++++++++++-------- modules/storage/src/admin/index.ts | 104 ++++++++++++----- modules/storage/src/handlers/file.ts | 2 +- modules/storage/src/utils/index.ts | 36 +++--- 4 files changed, 199 insertions(+), 92 deletions(-) diff --git a/modules/storage/src/admin/adminFile.ts b/modules/storage/src/admin/adminFile.ts index 8abbbaa7c..0a7fb4382 100644 --- a/modules/storage/src/admin/adminFile.ts +++ b/modules/storage/src/admin/adminFile.ts @@ -15,6 +15,7 @@ import { _updateFile, _updateFileUploadUrl, deepPathHandler, + findOrCreateFolders, normalizeFolderPath, storeNewFile, validateName, @@ -48,20 +49,27 @@ export class AdminFileHandlers { if (isNil(file)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); } - return file; } async createFile(call: ParsedRouterRequest): Promise { const { name, alias, data, container, mimeType, isPublic, scope } = call.request.params; - const folder = normalizeFolderPath(call.request.params.folder); const config = ConfigController.getInstance().config; + + const folder = normalizeFolderPath(call.request.params.folder); const usedContainer = isNil(container) ? config.defaultContainer : await this.findOrCreateContainer(container, isPublic); if (folder !== '/') { - await this.findOrCreateFolders(folder, usedContainer, isPublic); + await findOrCreateFolders( + this.grpcSdk, + this.storageProvider, + folder, + usedContainer, + isPublic, + scope, + ); } const validatedName = await validateName(name, folder, usedContainer); if (!isString(data)) { @@ -69,15 +77,20 @@ export class AdminFileHandlers { } try { - return await storeNewFile(this.storageProvider, { - name: validatedName, - alias, - data, - container: usedContainer, - folder, - isPublic, - mimeType, - }); + return await storeNewFile( + this.grpcSdk, + this.storageProvider, + { + name: validatedName, + alias, + data, + container: usedContainer, + folder, + isPublic, + mimeType, + }, + scope, + ); } catch (e) { throw new GrpcError( status.INTERNAL, @@ -87,27 +100,48 @@ export class AdminFileHandlers { } async createFileUploadUrl(call: ParsedRouterRequest): Promise { - const { name, alias, container, size = 0, mimeType, isPublic } = call.request.params; + const { + name, + alias, + container, + size = 0, + mimeType, + isPublic, + scope, + } = call.request.params; + const folder = normalizeFolderPath(call.request.params.folder); const config = ConfigController.getInstance().config; const usedContainer = isNil(container) ? config.defaultContainer : await this.findOrCreateContainer(container, isPublic); if (folder !== '/') { - await this.findOrCreateFolders(folder, usedContainer, isPublic); + await findOrCreateFolders( + this.grpcSdk, + this.storageProvider, + folder, + usedContainer, + isPublic, + scope, + ); } const validatedName = await validateName(name, folder, usedContainer); try { - return await _createFileUploadUrl(this.storageProvider, { - container: usedContainer, - folder, - isPublic, - name: validatedName, - alias, - size, - mimeType, - }); + return await _createFileUploadUrl( + this.grpcSdk, + this.storageProvider, + { + container: usedContainer, + folder, + isPublic, + name: validatedName, + alias, + size, + mimeType, + }, + scope, + ); } catch (e) { throw new GrpcError( status.INTERNAL, @@ -117,7 +151,7 @@ export class AdminFileHandlers { } async updateFileUploadUrl(call: ParsedRouterRequest): Promise { - const { id, alias, mimeType, size } = call.request.params; + const { id, alias, mimeType, size, scope } = call.request.params; const found = await File.getInstance().findOne({ _id: id }); if (isNil(found)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); @@ -127,14 +161,20 @@ export class AdminFileHandlers { found, ); try { - return await _updateFileUploadUrl(this.storageProvider, found, { - name, - alias, - folder, - container, - mimeType: mimeType ?? found.mimeType, - size, - }); + return await _updateFileUploadUrl( + this.grpcSdk, + this.storageProvider, + found, + { + name, + alias, + folder, + container, + mimeType: mimeType ?? found.mimeType, + size, + }, + scope, + ); } catch (e) { throw new GrpcError( status.INTERNAL, @@ -144,7 +184,7 @@ export class AdminFileHandlers { } async updateFile(call: ParsedRouterRequest): Promise { - const { id, alias, data, mimeType } = call.request.params; + const { id, alias, data, mimeType, scope } = call.request.params; const found = await File.getInstance().findOne({ _id: id }); if (isNil(found)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); @@ -154,14 +194,20 @@ export class AdminFileHandlers { found, ); try { - return await _updateFile(this.storageProvider, found, { - name, - alias, - folder, - container, - data: Buffer.from(data, 'base64'), - mimeType: mimeType ?? found.mimeType, - }); + return await _updateFile( + this.grpcSdk, + this.storageProvider, + found, + { + name, + alias, + folder, + container, + data: Buffer.from(data, 'base64'), + mimeType: mimeType ?? found.mimeType, + }, + scope, + ); } catch (e) { throw new GrpcError( status.INTERNAL, @@ -171,11 +217,12 @@ export class AdminFileHandlers { } async deleteFile(call: ParsedRouterRequest): Promise { - if (!isString(call.request.params.id)) { + const { id } = call.request.urlParams; + if (!isString(id)) { throw new GrpcError(status.INVALID_ARGUMENT, 'The provided id is invalid'); } try { - const found = await File.getInstance().findOne({ _id: call.request.params.id }); + const found = await File.getInstance().findOne({ _id: id }); if (isNil(found)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); } @@ -185,9 +232,15 @@ export class AdminFileHandlers { if (!success) { throw new GrpcError(status.INTERNAL, 'File could not be deleted'); } - await File.getInstance().deleteOne({ _id: call.request.params.id }); + await File.getInstance().deleteOne({ _id: id }); ConduitGrpcSdk.Metrics?.decrement('files_total'); ConduitGrpcSdk.Metrics?.decrement('storage_size_bytes_total', found.size); + + if (ConfigController.getInstance().config.authorization.enabled) { + await this.grpcSdk.authorization?.deleteAllRelations({ + resource: 'File:' + id, + }); // TODO: add catch + } return { success: true }; } catch (e) { throw new GrpcError( @@ -283,7 +336,13 @@ export class AdminFileHandlers { } const newFolder = isNil(folder) ? file.folder : normalizeFolderPath(folder); if (newFolder !== file.folder && newFolder !== '/') { - await this.findOrCreateFolders(newFolder, newContainer); + await findOrCreateFolders( + this.grpcSdk, + this.storageProvider, + newFolder, + newContainer, + file.isPublic, + ); } const isDataUpdate = newName === file.name && diff --git a/modules/storage/src/admin/index.ts b/modules/storage/src/admin/index.ts index 36d190b54..e668922d8 100644 --- a/modules/storage/src/admin/index.ts +++ b/modules/storage/src/admin/index.ts @@ -19,7 +19,7 @@ import { import { status } from '@grpc/grpc-js'; import { isEmpty, isNil } from 'lodash-es'; import { _StorageContainer, _StorageFolder, File } from '../models/index.js'; -import { normalizeFolderPath } from '../utils/index.js'; +import { findOrCreateFolders, normalizeFolderPath } from '../utils/index.js'; import { AdminFileHandlers } from './adminFile.js'; export class AdminRoutes { @@ -97,26 +97,33 @@ export class AdminRoutes { } async createFolder(call: ParsedRouterRequest): Promise { - const { container, isPublic } = call.request.params; + const { container, isPublic, scope } = call.request.params; const name = normalizeFolderPath(call.request.params.name); if (name === '/') { throw new GrpcError(status.INVALID_ARGUMENT, 'Folder name may not be empty'); } - const containerDocument = await _StorageContainer + const foundFolder = await _StorageFolder.getInstance().findOne({ + name, + container, + }); + if (!foundFolder) { + throw new GrpcError(status.ALREADY_EXISTS, 'Folder already exists'); + } + const containerDoc = await _StorageContainer .getInstance() .findOne({ name: container }); - if (isNil(containerDocument)) { + if (isNil(containerDoc)) { await this._createContainer(container, isPublic).catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); }); } - const createdFolders = await this.fileHandlers.findOrCreateFolders( + const createdFolders = await findOrCreateFolders( + this.grpcSdk, + this.fileHandlers.storage, name, container, isPublic, - () => { - throw new GrpcError(status.ALREADY_EXISTS, 'Folder already exists'); - }, + scope, ); return createdFolders[createdFolders.length - 1]; } @@ -133,14 +140,30 @@ export class AdminRoutes { await this.fileHandlers.storage .container(folder.container) .deleteFolder(folder.name); - await _StorageFolder.getInstance().deleteOne({ - name: folder.name, + await _StorageFolder.getInstance().deleteMany({ + name: { $regex: `^${folder.name}` }, container: folder.container, }); await File.getInstance().deleteMany({ folder: folder.name, container: folder.container, }); + if (ConfigController.getInstance().config.authorization.enabled) { + await this.grpcSdk.authorization + ?.deleteAllRelations({ subject: 'Folder:' + folder._id }) + .catch((e: Error) => { + if (e.message !== 'No relations found') throw e; + }); + await this.grpcSdk.authorization + ?.deleteAllRelations({ resource: 'Folder:' + folder._id }) + .catch((e: Error) => { + if (e.message !== 'No relations found') throw e; + }); + } + await _StorageFolder.getInstance().deleteOne({ + name: folder.name, + container: folder.container, + }); } return 'OK'; } @@ -158,29 +181,29 @@ export class AdminRoutes { } async createContainer(call: ParsedRouterRequest): Promise { - const { name, isPublic } = call.request.params; - return await this._createContainer(name, isPublic); + const { name, isPublic, scope } = call.request.params; + return await this._createContainer(name, isPublic, scope); } async deleteContainer(call: ParsedRouterRequest): Promise { - const { id } = call.request.params; + const { id, scope } = call.request.params; try { - const container = await _StorageContainer.getInstance().findOne({ - _id: id, - }); + const container = await _StorageContainer + .getInstance() + .findOne({ _id: id }, undefined, scope); if (isNil(container)) { throw new GrpcError(status.NOT_FOUND, 'Container does not exist'); } else { await this.fileHandlers.storage.deleteContainer(container.name); - await _StorageContainer.getInstance().deleteOne({ - _id: id, - }); - await File.getInstance().deleteMany({ - container: container.name, - }); - await _StorageFolder.getInstance().deleteMany({ - container: container.name, - }); + await _StorageContainer.getInstance().deleteOne({ _id: id }, undefined, scope); + await File.getInstance().deleteMany( + { container: container.name }, + undefined, + scope, + ); + await _StorageFolder + .getInstance() + .deleteMany({ container: container.name }, undefined, scope); } return container; } catch (e) { @@ -258,6 +281,9 @@ export class AdminRoutes { container: { type: TYPE.String, required: false }, isPublic: TYPE.Boolean, }, + queryParams: { + ...(authzEnabled && { scope: { type: TYPE.String, required: false } }), + }, action: ConduitRouteActions.POST, path: '/files/upload', description: `Creates a new file and provides a URL to upload it to.`, @@ -284,6 +310,9 @@ export class AdminRoutes { data: ConduitString.Required, mimeType: ConduitString.Optional, }, + queryParams: { + ...(authzEnabled && { scope: { type: TYPE.String, required: false } }), + }, }, new ConduitRouteReturnDefinition('PatchFile', File.name), this.fileHandlers.updateFile.bind(this.fileHandlers), @@ -301,6 +330,9 @@ export class AdminRoutes { mimeType: ConduitString.Optional, size: ConduitNumber.Optional, }, + queryParams: { + ...(authzEnabled && { scope: { type: TYPE.String, required: false } }), + }, action: ConduitRouteActions.PATCH, path: '/files/upload/:id', description: `Updates a file and provides a URL to upload its data to.`, @@ -387,6 +419,9 @@ export class AdminRoutes { container: ConduitString.Required, isPublic: ConduitBoolean.Optional, }, + queryParams: { + ...(authzEnabled && { scope: { type: TYPE.String, required: false } }), + }, }, new ConduitRouteReturnDefinition('CreateFolder', _StorageFolder.name), this.createFolder.bind(this), @@ -429,6 +464,9 @@ export class AdminRoutes { name: ConduitString.Required, isPublic: ConduitBoolean.Optional, }, + queryParams: { + ...(authzEnabled && { scope: { type: TYPE.String, required: false } }), + }, }, new ConduitRouteReturnDefinition(_StorageContainer.name), this.createContainer.bind(this), @@ -441,6 +479,9 @@ export class AdminRoutes { urlParams: { id: { type: TYPE.String, required: true }, }, + queryParams: { + ...(authzEnabled && { scope: { type: TYPE.String, required: false } }), + }, }, new ConduitRouteReturnDefinition('DeleteContainer', _StorageContainer.name), this.deleteContainer.bind(this), @@ -448,7 +489,11 @@ export class AdminRoutes { this.routingManager.registerRoutes(); } - private async _createContainer(name: string, isPublic: boolean | undefined) { + private async _createContainer( + name: string, + isPublic: boolean | undefined, + scope?: string, + ) { try { let container = await _StorageContainer.getInstance().findOne({ name, @@ -459,10 +504,9 @@ export class AdminRoutes { if (!exists) { await this.fileHandlers.storage.createContainer(name); } - container = await _StorageContainer.getInstance().create({ - name, - isPublic, - }); + container = await _StorageContainer + .getInstance() + .create({ name, isPublic }, undefined, scope); } else { throw new GrpcError(status.ALREADY_EXISTS, 'Container already exists'); } diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index 1ac00ff3f..d601ac9e4 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -354,7 +354,7 @@ export class FileHandlers { if (ConfigController.getInstance().config.authorization.enabled) { await this.grpcSdk.authorization?.deleteAllRelations({ resource: 'File:' + id, - }); + }); // TODO: add catch } return { success: true }; } catch (e) { diff --git a/modules/storage/src/utils/index.ts b/modules/storage/src/utils/index.ts index 60f8e462b..969e5a00f 100644 --- a/modules/storage/src/utils/index.ts +++ b/modules/storage/src/utils/index.ts @@ -72,7 +72,7 @@ export async function deepPathHandler( export async function createFileRelations( grpcSdk: ConduitGrpcSdk, file: File, - scope: string, + scope?: string, ) { if (file.folder === '/') { const containerDoc = await _StorageContainer @@ -83,11 +83,13 @@ export async function createFileRelations( relation: 'owner', resource: `File:${file._id}`, }); - await grpcSdk.authorization?.createRelation({ - subject: scope!, - relation: 'owner', - resource: `File:${file._id}`, - }); + if (scope) { + await grpcSdk.authorization?.createRelation({ + subject: scope, + relation: 'owner', + resource: `File:${file._id}`, + }); + } } else { const folderDoc = await _StorageFolder .getInstance() @@ -104,7 +106,7 @@ export async function updateFileRelations( grpcSdk: ConduitGrpcSdk, file: File, updatedFile: File, - scope: string, + scope?: string, ) { if (updatedFile.container !== file.container) { const prevContainer = await _StorageContainer @@ -129,11 +131,13 @@ export async function updateFileRelations( relation: 'owner', resource: `File:${updatedFile._id}`, }); - await grpcSdk.authorization?.createRelation({ - subject: scope!, - relation: 'owner', - resource: `File:${updatedFile._id}`, - }); + if (scope) { + await grpcSdk.authorization?.createRelation({ + subject: scope!, + relation: 'owner', + resource: `File:${updatedFile._id}`, + }); + } } else { const folderDoc = await _StorageFolder .getInstance() @@ -251,7 +255,7 @@ export async function storeNewFile( url: publicUrl, }); if (authzEnabled) { - await createFileRelations(grpcSdk, file, scope!); + await createFileRelations(grpcSdk, file, scope); } return file; } @@ -286,7 +290,7 @@ export async function _createFileUploadUrl( url: publicUrl, }); if (authzEnabled) { - await createFileRelations(grpcSdk, file, scope!); + await createFileRelations(grpcSdk, file, scope); } const url = (await storageProvider .container(container) @@ -331,7 +335,7 @@ export async function _updateFile( mimeType, })) as File; if (authzEnabled) { - await updateFileRelations(grpcSdk, file, updatedFile, scope!); + await updateFileRelations(grpcSdk, file, updatedFile, scope); } updateFileMetrics(file.size, (data as Buffer).byteLength); return updatedFile; @@ -383,7 +387,7 @@ export async function _updateFileUploadUrl( ...{ size: size ?? file.size }, }); if (authzEnabled) { - await updateFileRelations(grpcSdk, file, updatedFile!, scope!); + await updateFileRelations(grpcSdk, file, updatedFile!, scope); } } if (!isNil(size)) updateFileMetrics(file.size, size!); From 20752af787637adf8854595397e3e1ad33cf3d2c Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 17 Jan 2025 17:32:02 +0200 Subject: [PATCH 07/11] feat: client get files & authz in admin routes --- modules/storage/src/admin/index.ts | 40 ++++++++++++++++- modules/storage/src/handlers/file.ts | 67 +++++++++++++++++++++++++--- modules/storage/src/routes/index.ts | 24 ++++++++++ 3 files changed, 123 insertions(+), 8 deletions(-) diff --git a/modules/storage/src/admin/index.ts b/modules/storage/src/admin/index.ts index e668922d8..1c6b3ed28 100644 --- a/modules/storage/src/admin/index.ts +++ b/modules/storage/src/admin/index.ts @@ -113,7 +113,7 @@ export class AdminRoutes { .getInstance() .findOne({ name: container }); if (isNil(containerDoc)) { - await this._createContainer(container, isPublic).catch((e: Error) => { + await this._createContainer(container, isPublic, scope).catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); }); } @@ -195,7 +195,7 @@ export class AdminRoutes { throw new GrpcError(status.NOT_FOUND, 'Container does not exist'); } else { await this.fileHandlers.storage.deleteContainer(container.name); - await _StorageContainer.getInstance().deleteOne({ _id: id }, undefined, scope); + await File.getInstance().deleteMany( { container: container.name }, undefined, @@ -204,6 +204,42 @@ export class AdminRoutes { await _StorageFolder .getInstance() .deleteMany({ container: container.name }, undefined, scope); + await _StorageContainer.getInstance().deleteOne({ _id: id }, undefined, scope); + + // Delete all relations & indexes associated with container + if (ConfigController.getInstance().config.authorization.enabled) { + const filesResult = await this.grpcSdk.authorization?.getAllowedResources({ + subject: 'Container:' + id, + action: 'read', + resourceType: 'File', + skip: 0, + limit: 0, + }); + if (filesResult?.resources?.length) { + for (const fileId of filesResult.resources) { + await this.grpcSdk.authorization?.deleteAllRelations({ + resource: 'File:' + fileId, + }); + } + } + const foldersResult = await this.grpcSdk.authorization?.getAllowedResources({ + subject: 'Container:' + id, + action: 'read', + resourceType: 'Folder', + skip: 0, + limit: 0, + }); + if (foldersResult?.resources?.length) { + for (const folderId of foldersResult.resources) { + await this.grpcSdk.authorization?.deleteAllRelations({ + resource: 'Folder:' + folderId, + }); + } + } + await this.grpcSdk.authorization?.deleteAllRelations({ + subject: 'Container:' + id, + }); + } } return container; } catch (e) { diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index d601ac9e4..b499bafb8 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -3,6 +3,7 @@ import { DatabaseProvider, GrpcError, ParsedRouterRequest, + Query, UnparsedRouterResponse, } from '@conduitplatform/grpc-sdk'; import { ConfigController } from '@conduitplatform/module-tools'; @@ -49,6 +50,7 @@ export class FileHandlers { userId?: string, scope?: string, file?: File, + container?: string, ) { if (!userId) { throw new GrpcError(status.PERMISSION_DENIED, 'File access is not public'); @@ -68,6 +70,23 @@ export class FileHandlers { 'You are not allowed to create files in this scope', ); } + const defaultContainer = ConfigController.getInstance().config.defaultContainer; + if (container !== defaultContainer) { + const containerDoc = await _StorageContainer + .getInstance() + .findOne({ name: container }); + const allowed = await this.grpcSdk.authorization?.can({ + subject: scope ?? 'User:' + userId, + actions: ['edit'], + resource: 'Container:' + containerDoc?._id, + }); + if (!allowed || !allowed.allow) { + throw new GrpcError( + status.PERMISSION_DENIED, + 'You are not allowed to create files in this container', + ); + } + } } if (['read', 'edit', 'delete'].includes(action)) { const allowed = await this.grpcSdk.authorization?.can({ @@ -85,7 +104,7 @@ export class FileHandlers { } /* Checks if user can create-update file in provided folder */ - async fileAccessEdit(container: string, folder: string, scope: string) { + async folderAccessEdit(container: string, folder: string, scope: string) { if (!ConfigController.getInstance().config.authorization.enabled) { return; } @@ -152,11 +171,47 @@ export class FileHandlers { return file; } + async getFiles(call: ParsedRouterRequest): Promise { + const { search, folder, container, skip, limit, sort, scope } = + call.request.queryParams; + const { user } = call.request.context; + const authzEnabled = ConfigController.getInstance().config.authorization.enabled; + const query: Query = { + $and: [{ container }], + }; + if (!isNil(folder)) { + query.$and?.push({ + folder: folder.trim().slice(-1) !== '/' ? folder.trim() + '/' : folder.trim(), + }); + } + if (!isNil(search)) { + query.$and?.push({ + $or: [ + { name: { $regex: `.*${search}.*`, $options: 'i' } }, + { alias: { $regex: `.*${search}.*`, $options: 'i' } }, + ], + }); + } + if (!user) { + return await File.getInstance().findMany(query, undefined, skip, limit, sort); + } + return await File.getInstance().findMany( + query, + undefined, + skip, + limit, + sort, + undefined, + authzEnabled ? user._id : undefined, + authzEnabled ? scope : undefined, + ); + } + async createFile(call: ParsedRouterRequest): Promise { const { name, alias, data, container, mimeType, isPublic, scope } = call.request.params; const { user } = call.request.context; - await this.fileAccessCheck('create', user, scope); + await this.fileAccessCheck('create', user, scope, undefined, container); const config = ConfigController.getInstance().config; const usedContainer = isNil(container) @@ -165,7 +220,7 @@ export class FileHandlers { const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd_' + user._id); if (folder !== '/') { - await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); + await this.folderAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); await findOrCreateFolders( this.grpcSdk, this.storageProvider, @@ -211,7 +266,7 @@ export class FileHandlers { scope, } = call.request.params; const { user } = call.request.context; - await this.fileAccessCheck('create', user, scope); + await this.fileAccessCheck('create', user, scope, undefined, container); const config = ConfigController.getInstance().config; const usedContainer = isNil(container) @@ -220,7 +275,7 @@ export class FileHandlers { const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd_' + user._id); if (folder !== '/') { - await this.fileAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); + await this.folderAccessEdit(usedContainer, folder, scope ?? 'User:' + user._id); await findOrCreateFolders( this.grpcSdk, this.storageProvider, @@ -453,7 +508,7 @@ export class FileHandlers { } const newFolder = isNil(folder) ? file.folder : normalizeFolderPath(folder); if (newFolder !== file.folder && newFolder !== '/') { - await this.fileAccessEdit( + await this.folderAccessEdit( newContainer, newFolder, scope ?? 'User:' + call.request.context.user._id, diff --git a/modules/storage/src/routes/index.ts b/modules/storage/src/routes/index.ts index d48958b96..8acd57ffd 100644 --- a/modules/storage/src/routes/index.ts +++ b/modules/storage/src/routes/index.ts @@ -6,6 +6,7 @@ import { TYPE, } from '@conduitplatform/grpc-sdk'; import { + ConduitBoolean, ConduitNumber, ConduitString, ConfigController, @@ -46,6 +47,29 @@ export class StorageRoutes { this.fileHandlers.getFile.bind(this.fileHandlers), ); + this._routingManager.route( + { + queryParams: { + container: ConduitString.Required, + folder: ConduitString.Optional, + search: ConduitString.Optional, + skip: ConduitNumber.Required, + limit: ConduitNumber.Required, + sort: ConduitString.Optional, + ...(authzEnabled && { scope: { type: TYPE.String, required: false } }), + }, + action: ConduitRouteActions.GET, + path: '/storage/file', + middlewares: ['authMiddleware?'], + description: `Returns queried files. If unauthenticated, only public files are returned.`, + }, + new ConduitRouteReturnDefinition('GetFiles', { + files: [File.name], + count: ConduitNumber.Required, + }), + this.fileHandlers.getFiles.bind(this.fileHandlers), + ); + this._routingManager.route( { urlParams: { From 7018b78a2731fe1aed904e42a5d6fac4a7cca4a6 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 19 Jan 2025 20:03:38 +0200 Subject: [PATCH 08/11] fix: add catch in deleteRelations --- modules/storage/src/admin/adminFile.ts | 10 +++++++--- modules/storage/src/handlers/file.ts | 10 +++++++--- modules/storage/src/utils/index.ts | 16 +++++++++++----- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/modules/storage/src/admin/adminFile.ts b/modules/storage/src/admin/adminFile.ts index 0a7fb4382..e87f03085 100644 --- a/modules/storage/src/admin/adminFile.ts +++ b/modules/storage/src/admin/adminFile.ts @@ -237,9 +237,13 @@ export class AdminFileHandlers { ConduitGrpcSdk.Metrics?.decrement('storage_size_bytes_total', found.size); if (ConfigController.getInstance().config.authorization.enabled) { - await this.grpcSdk.authorization?.deleteAllRelations({ - resource: 'File:' + id, - }); // TODO: add catch + await this.grpcSdk.authorization + ?.deleteAllRelations({ resource: 'File:' + id }) + .catch((e: Error) => { + if (!e.message.includes('No relations found')) { + throw e; + } + }); } return { success: true }; } catch (e) { diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index b499bafb8..42a28c60e 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -407,9 +407,13 @@ export class FileHandlers { ConduitGrpcSdk.Metrics?.decrement('storage_size_bytes_total', found.size); if (ConfigController.getInstance().config.authorization.enabled) { - await this.grpcSdk.authorization?.deleteAllRelations({ - resource: 'File:' + id, - }); // TODO: add catch + await this.grpcSdk.authorization + ?.deleteAllRelations({ resource: 'File:' + id }) + .catch((e: Error) => { + if (!e.message.includes('No relations found')) { + throw e; + } + }); } return { success: true }; } catch (e) { diff --git a/modules/storage/src/utils/index.ts b/modules/storage/src/utils/index.ts index 969e5a00f..5d4cbb3c5 100644 --- a/modules/storage/src/utils/index.ts +++ b/modules/storage/src/utils/index.ts @@ -112,11 +112,17 @@ export async function updateFileRelations( const prevContainer = await _StorageContainer .getInstance() .findOne({ name: file.container }); - await grpcSdk.authorization?.deleteRelation({ - subject: 'Container:' + prevContainer!._id, - relation: 'owner', - resource: `File:${updatedFile._id}`, - }); // TODO: add catch + await grpcSdk.authorization + ?.deleteRelation({ + subject: 'Container:' + prevContainer!._id, + relation: 'owner', + resource: `File:${updatedFile._id}`, + }) + .catch((e: Error) => { + if (!e.message.includes('Relation does not exist')) { + throw e; + } + }); } if (updatedFile.folder === file.folder) { return; From 37986f5cf93df7b2e8d08b36c1ae6c9c3a8567c8 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 20 Jan 2025 16:52:40 +0200 Subject: [PATCH 09/11] fix: bugs found in testing --- modules/storage/src/Storage.ts | 18 +++++----- modules/storage/src/admin/index.ts | 13 ++++--- modules/storage/src/handlers/file.ts | 34 +++++++++--------- modules/storage/src/utils/index.ts | 52 +++++++++++++--------------- 4 files changed, 60 insertions(+), 57 deletions(-) diff --git a/modules/storage/src/Storage.ts b/modules/storage/src/Storage.ts index 521e52722..947ca4612 100644 --- a/modules/storage/src/Storage.ts +++ b/modules/storage/src/Storage.ts @@ -123,18 +123,18 @@ export default class Storage extends ManagedModule { if (authorization.enabled) { this.grpcSdk.onceModuleUp('authorization', async () => { for (const resource of Object.values(resources)) { - this.grpcSdk.authorization!.defineResource(resource); - } - const defaultContainer = await models._StorageContainer.getInstance().findOne({ - name: config.defaultContainer, - }); - if (!defaultContainer) { - await models._StorageContainer.getInstance().create({ - name: config.defaultContainer, - }); + await this.grpcSdk.authorization!.defineResource(resource); } }); } + const defaultContainer = await models._StorageContainer.getInstance().findOne({ + name: config.defaultContainer, + }); + if (!defaultContainer) { + await models._StorageContainer.getInstance().create({ + name: config.defaultContainer, + }); + } this.storageProvider = createStorageProvider(provider, { local, google, diff --git a/modules/storage/src/admin/index.ts b/modules/storage/src/admin/index.ts index 1c6b3ed28..c3ad64997 100644 --- a/modules/storage/src/admin/index.ts +++ b/modules/storage/src/admin/index.ts @@ -536,13 +536,18 @@ export class AdminRoutes { }); if (isNil(container)) { const exists = await this.fileHandlers.storage.containerExists(name); - if (!exists) { await this.fileHandlers.storage.createContainer(name); } - container = await _StorageContainer - .getInstance() - .create({ name, isPublic }, undefined, scope); + container = await _StorageContainer.getInstance().create({ name, isPublic }); + const authzEnabled = ConfigController.getInstance().config.authorization.enabled; + if (authzEnabled && scope) { + await this.grpcSdk.authorization?.createRelation({ + subject: scope, + relation: 'owner', + resource: 'Container:' + container._id, + }); + } } else { throw new GrpcError(status.ALREADY_EXISTS, 'Container already exists'); } diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index 42a28c60e..58a2a8a5d 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -58,17 +58,20 @@ export class FileHandlers { if (!ConfigController.getInstance().config.authorization.enabled) { return; } - if (action === 'create' && scope) { - const allowed = await this.grpcSdk.authorization?.can({ - subject: 'User:' + userId, - actions: ['edit'], - resource: scope, - }); - if (!allowed || !allowed.allow) { - throw new GrpcError( - status.PERMISSION_DENIED, - 'You are not allowed to create files in this scope', - ); + + if (action === 'create') { + if (scope) { + const allowed = await this.grpcSdk.authorization?.can({ + subject: 'User:' + userId, + actions: ['edit'], + resource: scope, + }); + if (!allowed || !allowed.allow) { + throw new GrpcError( + status.PERMISSION_DENIED, + 'You are not allowed to create files in this scope', + ); + } } const defaultContainer = ConfigController.getInstance().config.defaultContainer; if (container !== defaultContainer) { @@ -87,8 +90,7 @@ export class FileHandlers { ); } } - } - if (['read', 'edit', 'delete'].includes(action)) { + } else { const allowed = await this.grpcSdk.authorization?.can({ subject: 'User:' + userId, actions: [action], @@ -121,7 +123,7 @@ export class FileHandlers { if (!allowed || !allowed.allow) { throw new GrpcError( status.PERMISSION_DENIED, - 'You are not allowed to edit files in folder' + folderDoc.name, + 'You are not allowed to edit files in folder ' + folderDoc.name, ); } return; @@ -211,12 +213,12 @@ export class FileHandlers { const { name, alias, data, container, mimeType, isPublic, scope } = call.request.params; const { user } = call.request.context; - await this.fileAccessCheck('create', user, scope, undefined, container); const config = ConfigController.getInstance().config; const usedContainer = isNil(container) ? config.defaultContainer : await this.findContainer(container); + await this.fileAccessCheck('create', user, scope, undefined, usedContainer); const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd_' + user._id); if (folder !== '/') { @@ -266,12 +268,12 @@ export class FileHandlers { scope, } = call.request.params; const { user } = call.request.context; - await this.fileAccessCheck('create', user, scope, undefined, container); const config = ConfigController.getInstance().config; const usedContainer = isNil(container) ? config.defaultContainer : await this.findContainer(container); + await this.fileAccessCheck('create', user, scope, undefined, usedContainer); const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd_' + user._id); if (folder !== '/') { diff --git a/modules/storage/src/utils/index.ts b/modules/storage/src/utils/index.ts index 5d4cbb3c5..41d154dd5 100644 --- a/modules/storage/src/utils/index.ts +++ b/modules/storage/src/utils/index.ts @@ -192,40 +192,36 @@ export async function findOrCreateFolders( const isFirst = i === 0; folder = await _StorageFolder.getInstance().findOne({ name: folderPath, container }); - let folderScope: string | undefined; - if (authzEnabled && isFirst) { - folderScope = scope; - } else if (authzEnabled && !isFirst) { - const prevFolder = (await _StorageFolder.getInstance().findOne({ - name: nestedPaths[i - 1], - container, - })) as _StorageFolder; - folderScope = 'Folder:' + prevFolder._id; - } - if (!folder) { - folder = await _StorageFolder.getInstance().create( - { - name: folderPath, - container, - isPublic, - }, - undefined, - folderScope, - ); + folder = await _StorageFolder + .getInstance() + .create({ name: folderPath, container, isPublic }); createdFolders.push(folder); const exists = await storageProvider.container(container).folderExists(folderPath); if (!exists) { await storageProvider.container(container).createFolder(folderPath); } - } - - if (authzEnabled && isFirst) { - await grpcSdk.authorization?.createRelation({ - subject: 'Container:' + containerDoc._id, - relation: 'owner', - resource: `Folder:${folder!._id}`, - }); + if (!authzEnabled) { + continue; + } + // Create folder owner relations + const folderOwners: string[] = []; + if (isFirst) { + folderOwners.push('Container:' + containerDoc._id, scope!); + } else { + const prevFolder = await _StorageFolder.getInstance().findOne({ + name: nestedPaths[i - 1], + container, + }); + folderOwners.push('Folder:' + prevFolder!._id); + } + for (const owner of folderOwners) { + await grpcSdk.authorization?.createRelation({ + subject: owner, + relation: 'owner', + resource: `Folder:${folder._id}`, + }); + } } } return createdFolders; From 4119d8c4ab4289597296030e2120c3d45cb1fa35 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 20 Jan 2025 18:30:40 +0200 Subject: [PATCH 10/11] fix: bugs found in testing --- modules/storage/src/admin/index.ts | 2 +- modules/storage/src/handlers/file.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/storage/src/admin/index.ts b/modules/storage/src/admin/index.ts index c3ad64997..71081f1b5 100644 --- a/modules/storage/src/admin/index.ts +++ b/modules/storage/src/admin/index.ts @@ -106,7 +106,7 @@ export class AdminRoutes { name, container, }); - if (!foundFolder) { + if (foundFolder) { throw new GrpcError(status.ALREADY_EXISTS, 'Folder already exists'); } const containerDoc = await _StorageContainer diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index 58a2a8a5d..dd84d220d 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -135,9 +135,6 @@ export class FileHandlers { name: nestedPaths[i], container, }); - if (!prevFolder) { - continue; - } if (prevFolder) { const allowed = await this.grpcSdk.authorization?.can({ subject: scope, From b7a5505eb9d524d75acbfd9fd5280689b8b717ae Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 21 Jan 2025 16:32:36 +0200 Subject: [PATCH 11/11] fix: bugs found in testing --- modules/storage/src/admin/index.ts | 89 +++++++++----------- modules/storage/src/handlers/file.ts | 63 +++++--------- modules/storage/src/providers/local/index.ts | 2 +- modules/storage/src/routes/index.ts | 23 ----- 4 files changed, 66 insertions(+), 111 deletions(-) diff --git a/modules/storage/src/admin/index.ts b/modules/storage/src/admin/index.ts index 71081f1b5..cf5c2ffdd 100644 --- a/modules/storage/src/admin/index.ts +++ b/modules/storage/src/admin/index.ts @@ -145,7 +145,7 @@ export class AdminRoutes { container: folder.container, }); await File.getInstance().deleteMany({ - folder: folder.name, + folder: { $regex: `^${folder.name}` }, container: folder.container, }); if (ConfigController.getInstance().config.authorization.enabled) { @@ -186,59 +186,57 @@ export class AdminRoutes { } async deleteContainer(call: ParsedRouterRequest): Promise { - const { id, scope } = call.request.params; + const { id } = call.request.params; try { - const container = await _StorageContainer - .getInstance() - .findOne({ _id: id }, undefined, scope); + const container = await _StorageContainer.getInstance().findOne({ _id: id }); if (isNil(container)) { throw new GrpcError(status.NOT_FOUND, 'Container does not exist'); } else { await this.fileHandlers.storage.deleteContainer(container.name); + await _StorageContainer.getInstance().deleteOne({ _id: id }); - await File.getInstance().deleteMany( - { container: container.name }, - undefined, - scope, - ); - await _StorageFolder - .getInstance() - .deleteMany({ container: container.name }, undefined, scope); - await _StorageContainer.getInstance().deleteOne({ _id: id }, undefined, scope); - - // Delete all relations & indexes associated with container - if (ConfigController.getInstance().config.authorization.enabled) { - const filesResult = await this.grpcSdk.authorization?.getAllowedResources({ - subject: 'Container:' + id, - action: 'read', - resourceType: 'File', - skip: 0, - limit: 0, - }); - if (filesResult?.resources?.length) { - for (const fileId of filesResult.resources) { - await this.grpcSdk.authorization?.deleteAllRelations({ - resource: 'File:' + fileId, + if (!ConfigController.getInstance().config.authorization.enabled) { + await File.getInstance().deleteMany({ container: container.name }); + await _StorageFolder.getInstance().deleteMany({ container: container.name }); + } else { + // Delete all relations & indexes associated with container + const files = await File.getInstance().findMany( + { container: container.name }, + '_id', + ); + for (const file of files) { + await this.grpcSdk.authorization + ?.deleteAllRelations({ + resource: 'File:' + file._id, + }) + .catch((e: Error) => { + if (!e.message.includes('No relations found')) throw e; }); - } } - const foldersResult = await this.grpcSdk.authorization?.getAllowedResources({ - subject: 'Container:' + id, - action: 'read', - resourceType: 'Folder', - skip: 0, - limit: 0, - }); - if (foldersResult?.resources?.length) { - for (const folderId of foldersResult.resources) { - await this.grpcSdk.authorization?.deleteAllRelations({ - resource: 'Folder:' + folderId, + await File.getInstance().deleteMany({ container: container.name }); + + const folders = await _StorageFolder + .getInstance() + .findMany({ container: container.name }, '_id'); + for (const folder of folders) { + await this.grpcSdk.authorization + ?.deleteAllRelations({ resource: 'Folder:' + folder._id }) + .catch((e: Error) => { + if (!e.message.includes('No relations found')) throw e; + }); + await this.grpcSdk.authorization + ?.deleteAllRelations({ subject: 'Folder:' + folder._id }) + .catch((e: Error) => { + if (!e.message.includes('No relations found')) throw e; }); - } } - await this.grpcSdk.authorization?.deleteAllRelations({ - subject: 'Container:' + id, - }); + await _StorageFolder.getInstance().deleteMany({ container: container.name }); + + await this.grpcSdk.authorization + ?.deleteAllRelations({ subject: 'Container:' + id }) + .catch((e: Error) => { + if (!e.message.includes('No relations found')) throw e; + }); } } return container; @@ -515,9 +513,6 @@ export class AdminRoutes { urlParams: { id: { type: TYPE.String, required: true }, }, - queryParams: { - ...(authzEnabled && { scope: { type: TYPE.String, required: false } }), - }, }, new ConduitRouteReturnDefinition('DeleteContainer', _StorageContainer.name), this.deleteContainer.bind(this), diff --git a/modules/storage/src/handlers/file.ts b/modules/storage/src/handlers/file.ts index dd84d220d..afc4fdea5 100644 --- a/modules/storage/src/handlers/file.ts +++ b/modules/storage/src/handlers/file.ts @@ -170,42 +170,6 @@ export class FileHandlers { return file; } - async getFiles(call: ParsedRouterRequest): Promise { - const { search, folder, container, skip, limit, sort, scope } = - call.request.queryParams; - const { user } = call.request.context; - const authzEnabled = ConfigController.getInstance().config.authorization.enabled; - const query: Query = { - $and: [{ container }], - }; - if (!isNil(folder)) { - query.$and?.push({ - folder: folder.trim().slice(-1) !== '/' ? folder.trim() + '/' : folder.trim(), - }); - } - if (!isNil(search)) { - query.$and?.push({ - $or: [ - { name: { $regex: `.*${search}.*`, $options: 'i' } }, - { alias: { $regex: `.*${search}.*`, $options: 'i' } }, - ], - }); - } - if (!user) { - return await File.getInstance().findMany(query, undefined, skip, limit, sort); - } - return await File.getInstance().findMany( - query, - undefined, - skip, - limit, - sort, - undefined, - authzEnabled ? user._id : undefined, - authzEnabled ? scope : undefined, - ); - } - async createFile(call: ParsedRouterRequest): Promise { const { name, alias, data, container, mimeType, isPublic, scope } = call.request.params; @@ -215,7 +179,7 @@ export class FileHandlers { const usedContainer = isNil(container) ? config.defaultContainer : await this.findContainer(container); - await this.fileAccessCheck('create', user, scope, undefined, usedContainer); + await this.fileAccessCheck('create', user._id, scope, undefined, usedContainer); const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd_' + user._id); if (folder !== '/') { @@ -270,7 +234,7 @@ export class FileHandlers { const usedContainer = isNil(container) ? config.defaultContainer : await this.findContainer(container); - await this.fileAccessCheck('create', user, scope, undefined, usedContainer); + await this.fileAccessCheck('create', user._id, scope, undefined, usedContainer); const folder = normalizeFolderPath(call.request.params.folder ?? 'cnd_' + user._id); if (folder !== '/') { @@ -385,6 +349,7 @@ export class FileHandlers { async deleteFile(call: ParsedRouterRequest): Promise { const { id } = call.request.urlParams; const { scope } = call.request.queryParams; + const { user } = call.request.context; if (!isString(id)) { throw new GrpcError(status.INVALID_ARGUMENT, 'The provided id is invalid'); } @@ -393,7 +358,7 @@ export class FileHandlers { if (isNil(found)) { throw new GrpcError(status.NOT_FOUND, 'File does not exist'); } - await this.fileAccessCheck('delete', id, scope, found); + await this.fileAccessCheck('delete', user._id, scope, found); const success = await this.storageProvider .container(found.container) @@ -507,7 +472,25 @@ export class FileHandlers { const newName = name ?? file.name; const newContainer = container ?? file.container; if (newContainer !== file.container) { - await this.findContainer(newContainer); + const foundContainer = await _StorageContainer.getInstance().findOne({ + name: newContainer, + }); + if (!foundContainer) { + throw new GrpcError(status.NOT_FOUND, 'Container does not exist'); + } + if (ConfigController.getInstance().config.authorization.enabled) { + const allowed = await this.grpcSdk.authorization?.can({ + subject: scope ?? 'User:' + call.request.context.user._id, + actions: ['edit'], + resource: 'Container:' + foundContainer._id, + }); + if (!allowed || !allowed.allow) { + throw new GrpcError( + status.PERMISSION_DENIED, + 'You are not allowed to edit files in this container', + ); + } + } } const newFolder = isNil(folder) ? file.folder : normalizeFolderPath(folder); if (newFolder !== file.folder && newFolder !== '/') { diff --git a/modules/storage/src/providers/local/index.ts b/modules/storage/src/providers/local/index.ts index ecc5b1da9..0c3b24e8d 100644 --- a/modules/storage/src/providers/local/index.ts +++ b/modules/storage/src/providers/local/index.ts @@ -42,7 +42,7 @@ export class LocalStorage implements IStorageProvider { } deleteContainer(name: string): Promise { - return this.deleteFolder(name); + return this.container(name).deleteFolder(name); } deleteFolder(name: string): Promise { diff --git a/modules/storage/src/routes/index.ts b/modules/storage/src/routes/index.ts index 8acd57ffd..eda1ab67f 100644 --- a/modules/storage/src/routes/index.ts +++ b/modules/storage/src/routes/index.ts @@ -47,29 +47,6 @@ export class StorageRoutes { this.fileHandlers.getFile.bind(this.fileHandlers), ); - this._routingManager.route( - { - queryParams: { - container: ConduitString.Required, - folder: ConduitString.Optional, - search: ConduitString.Optional, - skip: ConduitNumber.Required, - limit: ConduitNumber.Required, - sort: ConduitString.Optional, - ...(authzEnabled && { scope: { type: TYPE.String, required: false } }), - }, - action: ConduitRouteActions.GET, - path: '/storage/file', - middlewares: ['authMiddleware?'], - description: `Returns queried files. If unauthenticated, only public files are returned.`, - }, - new ConduitRouteReturnDefinition('GetFiles', { - files: [File.name], - count: ConduitNumber.Required, - }), - this.fileHandlers.getFiles.bind(this.fileHandlers), - ); - this._routingManager.route( { urlParams: {