Fix permissions for serverless functions (#6555)

Fixes #6525

(@martmull fyi it was not related to AWS but linked to the fact that we
recently enforced passing a token to access files)
This commit is contained in:
Félix Malfait 2024-08-06 15:31:22 +02:00 committed by GitHub
parent 67c41251fb
commit 4157a67bf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 107 additions and 26 deletions

View File

@ -32,7 +32,9 @@ export class FileController {
const workspaceId = (req as any)?.workspaceId;
if (!workspaceId) {
return res.status(401).send({ error: 'Unauthorized' });
return res
.status(401)
.send({ error: 'Unauthorized, missing workspaceId' });
}
try {

View File

@ -2,17 +2,17 @@ import { join } from 'path';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
import { compileTypescript } from 'src/engine/integrations/serverless/drivers/utils/compile-typescript';
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
import { compileTypescript } from 'src/engine/integrations/serverless/drivers/utils/compile-typescript';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
export class BaseServerlessDriver {
getFolderPath(serverlessFunction: ServerlessFunctionEntity) {
return join(
'workspace-' + serverlessFunction.workspaceId,
FileFolder.ServerlessFunction,
serverlessFunction.workspaceId,
serverlessFunction.id,
);
}

View File

@ -0,0 +1,57 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
@Injectable()
export class ServerlessFunctionInterceptor implements NestInterceptor {
constructor(private readonly fileService: FileService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(async (data) => {
if (data.edges && Array.isArray(data.edges)) {
return {
...data,
edges: Promise.all(
data.edges.map((item) => ({
...item,
node: this.processItem(item.node),
})),
),
};
} else {
return this.processItem(data);
}
}),
);
}
private async processItem(item: any): Promise<any> {
if (item && item.sourceCodeFullPath) {
const workspaceId = item.workspace?.id || item.workspaceId;
if (!workspaceId) {
return item;
}
const signedPayload = await this.fileService.encodeFileToken({
serverlessFunctionId: item.id,
workspace_id: workspaceId,
});
return {
...item,
sourceCodeFullPath: `${item.sourceCodeFullPath}?token=${signedPayload}`,
};
}
return item;
}
}

View File

@ -1,21 +1,23 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SortDirection } from '@ptc-org/nestjs-query-core';
import {
NestjsQueryGraphQLModule,
PagingStrategies,
} from '@ptc-org/nestjs-query-graphql';
import { SortDirection } from '@ptc-org/nestjs-query-core';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.module';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.module';
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionInterceptor } from 'src/engine/metadata-modules/serverless-function/serverless-function.interceptor';
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
@Module({
imports: [
@ -27,6 +29,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
'metadata',
),
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
FileModule,
],
services: [ServerlessFunctionService],
resolvers: [
@ -42,6 +45,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
update: { disabled: true },
delete: { disabled: true },
guards: [JwtAuthGuard],
interceptors: [ServerlessFunctionInterceptor],
},
],
}),

View File

@ -1,4 +1,4 @@
import { UseGuards } from '@nestjs/common';
import { UseGuards, UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
@ -21,6 +21,7 @@ import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { ServerlessFunctionInterceptor } from 'src/engine/metadata-modules/serverless-function/serverless-function.interceptor';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
@ -66,6 +67,7 @@ export class ServerlessFunctionResolver {
}
}
@UseInterceptors(ServerlessFunctionInterceptor)
@Mutation(() => ServerlessFunctionDTO)
async updateOneServerlessFunction(
@Args('input')
@ -84,6 +86,7 @@ export class ServerlessFunctionResolver {
}
}
@UseInterceptors(ServerlessFunctionInterceptor)
@Mutation(() => ServerlessFunctionDTO)
async createOneServerlessFunction(
@Args('input')
@ -106,6 +109,7 @@ export class ServerlessFunctionResolver {
}
}
@UseInterceptors(ServerlessFunctionInterceptor)
@Mutation(() => ServerlessFunctionDTO)
async createOneServerlessFunctionFromFile(
@Args({ name: 'file', type: () => GraphQLUpload })

View File

@ -3,15 +3,20 @@ import { InjectRepository } from '@nestjs/typeorm';
import { join } from 'path';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { FileUpload } from 'graphql-upload';
import { Repository } from 'typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { v4 } from 'uuid';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { ServerlessExecuteResult } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
import { ServerlessService } from 'src/engine/integrations/serverless/serverless.service';
import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input';
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
import {
ServerlessFunctionEntity,
ServerlessFunctionSyncStatus,
@ -20,12 +25,7 @@ import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input';
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
@Injectable()
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {
@ -119,8 +119,8 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
if (codeHasChanged) {
const fileFolder = join(
'workspace-' + workspaceId,
FileFolder.ServerlessFunction,
workspaceId,
existingServerlessFunction.id,
);
@ -164,13 +164,18 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
const serverlessFunctionId = v4();
const fileFolder = join(
const fileFolderWithoutWorkspace = join(
FileFolder.ServerlessFunction,
workspaceId,
serverlessFunctionId,
);
const sourceCodeFullPath = fileFolder + '/' + SOURCE_FILE_NAME;
const fileFolder = join(
'workspace-' + workspaceId,
fileFolderWithoutWorkspace,
);
const sourceCodeFullPath =
fileFolderWithoutWorkspace + '/' + SOURCE_FILE_NAME;
const serverlessFunction = await super.createOne({
...serverlessFunctionInput,

View File

@ -180,7 +180,6 @@ yarn command:prod cron:calendar:calendar-event-list-fetch
### Data enrichment and AI
<ArticleTable options={[
['OPENROUTER_API_KEY', '', "The API key for openrouter.ai, an abstraction layer over models from Mistral, OpenAI and more"],
['OPENAI_API_KEY', 'sk-proj-abcdabcd...', "OpenAI API key"],
['LLM_CHAT_MODEL_DRIVER', 'openai', "LLM provider"],
['LLM_TRACING_DRIVER', 'langfuse', "Where to output LangChain logs. 'langfuse' or 'console'."],
@ -188,6 +187,16 @@ yarn command:prod cron:calendar:calendar-event-list-fetch
['LANGFUSE_PUBLIC_KEY', 'pk-lf-abcdabcd-abcd...', "Langfuse public key"],
]}></ArticleTable>
### Serverless functions
This feature is WIP and is not yet useful for most users.
<ArticleTable options={[
['SERVERLESS_TYPE', 'local', "Functions can either be executed through Lambda or directly by the main node process"],
['SERVERLESS_LAMBDA_REGION', 'us-east-1', 'If you use the Lambda driver, region of the Lambda function'],
['SERVERLESS_LAMBDA_ROLE', 'arn:aws:iam::121334:role/lambda-execution-role', "If you use the Lambda drive, name of the IAM role with the right permissions"],
]}></ArticleTable>
### Support Chat