feat: files visiblity with file configuration (#10438)
Ref: #10404 - Added `FileFolderConfig` with `isPublic` key. - Updated `file-path-guard.ts` to `ignoreExpiration` to validate the token if `isPublic` is `true`. - Token verification ignores expiration, assuming it's used to fetch file metadata with a required workspaceId as we cannot remove the token as we will loose the `workspaceId`. --------- Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
committed by
GitHub
parent
f4fcf39eb5
commit
3cd52b052e
@ -1,8 +1,11 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import {
|
||||
checkFilename,
|
||||
checkFilePath,
|
||||
checkFileFolder,
|
||||
} from 'src/engine/core-modules/file/file.utils';
|
||||
|
||||
describe('FileUtils', () => {
|
||||
@ -58,4 +61,32 @@ describe('FileUtils', () => {
|
||||
expect(() => checkFilename(filename)).toThrow(`Filename is not allowed`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkFileFolder', () => {
|
||||
it('should return the root folder when it is allowed', () => {
|
||||
expect(checkFileFolder(`${FileFolder.Attachment}/file.txt`)).toBe(
|
||||
FileFolder.Attachment,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for disallowed folders', () => {
|
||||
expect(() => checkFileFolder('invalid-folder/file.txt')).toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should sanitize null characters in file path', () => {
|
||||
expect(() => checkFileFolder('\0invalid-folder/file.txt')).toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle edge cases like empty file path', () => {
|
||||
expect(() => checkFileFolder('')).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should handle cases where filePath has no folder', () => {
|
||||
expect(() => checkFileFolder('file.txt')).toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -48,3 +48,18 @@ export const checkFilename = (filename: string) => {
|
||||
|
||||
return basename(sanitizedFilename);
|
||||
};
|
||||
|
||||
export const checkFileFolder = (filePath: string): FileFolder => {
|
||||
const allowedFolders = Object.values(FileFolder).map((value) =>
|
||||
kebabCase(value),
|
||||
);
|
||||
|
||||
const sanitizedFilePath = filePath.replace(/\0/g, '');
|
||||
const [rootFolder] = sanitizedFilePath.split('/');
|
||||
|
||||
if (!allowedFolders.includes(rootFolder as AllowedFolders)) {
|
||||
throw new BadRequestException(`Folder ${rootFolder} is not allowed`);
|
||||
}
|
||||
|
||||
return rootFolder as FileFolder;
|
||||
};
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
|
||||
import { fileFolderConfigs } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { checkFileFolder } from 'src/engine/core-modules/file/file.utils';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
|
||||
@Injectable()
|
||||
@ -8,6 +11,10 @@ export class FilePathGuard implements CanActivate {
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const fileFolder = checkFileFolder(request.params[0]);
|
||||
const ignoreExpirationToken =
|
||||
fileFolderConfigs[fileFolder].ignoreExpirationToken;
|
||||
|
||||
const query = request.query;
|
||||
|
||||
if (!query || !query['token']) {
|
||||
@ -18,6 +25,7 @@ export class FilePathGuard implements CanActivate {
|
||||
const payload = await this.jwtWrapperService.verifyWorkspaceToken(
|
||||
query['token'],
|
||||
'FILE',
|
||||
ignoreExpirationToken ? { ignoreExpiration: true } : {},
|
||||
);
|
||||
|
||||
if (!payload.workspaceId) {
|
||||
|
||||
@ -11,3 +11,25 @@ export enum FileFolder {
|
||||
registerEnumType(FileFolder, {
|
||||
name: 'FileFolder',
|
||||
});
|
||||
|
||||
export type FileFolderConfig = {
|
||||
ignoreExpirationToken: boolean;
|
||||
};
|
||||
|
||||
export const fileFolderConfigs: Record<FileFolder, FileFolderConfig> = {
|
||||
[FileFolder.ProfilePicture]: {
|
||||
ignoreExpirationToken: true,
|
||||
},
|
||||
[FileFolder.WorkspaceLogo]: {
|
||||
ignoreExpirationToken: true,
|
||||
},
|
||||
[FileFolder.Attachment]: {
|
||||
ignoreExpirationToken: false,
|
||||
},
|
||||
[FileFolder.PersonPicture]: {
|
||||
ignoreExpirationToken: false,
|
||||
},
|
||||
[FileFolder.ServerlessFunction]: {
|
||||
ignoreExpirationToken: false,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user