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 { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
checkFilename,
|
checkFilename,
|
||||||
checkFilePath,
|
checkFilePath,
|
||||||
|
checkFileFolder,
|
||||||
} from 'src/engine/core-modules/file/file.utils';
|
} from 'src/engine/core-modules/file/file.utils';
|
||||||
|
|
||||||
describe('FileUtils', () => {
|
describe('FileUtils', () => {
|
||||||
@ -58,4 +61,32 @@ describe('FileUtils', () => {
|
|||||||
expect(() => checkFilename(filename)).toThrow(`Filename is not allowed`);
|
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);
|
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 { 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';
|
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -8,6 +11,10 @@ export class FilePathGuard implements CanActivate {
|
|||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const fileFolder = checkFileFolder(request.params[0]);
|
||||||
|
const ignoreExpirationToken =
|
||||||
|
fileFolderConfigs[fileFolder].ignoreExpirationToken;
|
||||||
|
|
||||||
const query = request.query;
|
const query = request.query;
|
||||||
|
|
||||||
if (!query || !query['token']) {
|
if (!query || !query['token']) {
|
||||||
@ -18,6 +25,7 @@ export class FilePathGuard implements CanActivate {
|
|||||||
const payload = await this.jwtWrapperService.verifyWorkspaceToken(
|
const payload = await this.jwtWrapperService.verifyWorkspaceToken(
|
||||||
query['token'],
|
query['token'],
|
||||||
'FILE',
|
'FILE',
|
||||||
|
ignoreExpirationToken ? { ignoreExpiration: true } : {},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!payload.workspaceId) {
|
if (!payload.workspaceId) {
|
||||||
|
|||||||
@ -11,3 +11,25 @@ export enum FileFolder {
|
|||||||
registerEnumType(FileFolder, {
|
registerEnumType(FileFolder, {
|
||||||
name: '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