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:
@ -32,7 +32,9 @@ export class FileController {
|
|||||||
const workspaceId = (req as any)?.workspaceId;
|
const workspaceId = (req as any)?.workspaceId;
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
return res.status(401).send({ error: 'Unauthorized' });
|
return res
|
||||||
|
.status(401)
|
||||||
|
.send({ error: 'Unauthorized, missing workspaceId' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -2,17 +2,17 @@ import { join } from 'path';
|
|||||||
|
|
||||||
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 { 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 { 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 {
|
export class BaseServerlessDriver {
|
||||||
getFolderPath(serverlessFunction: ServerlessFunctionEntity) {
|
getFolderPath(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
return join(
|
return join(
|
||||||
|
'workspace-' + serverlessFunction.workspaceId,
|
||||||
FileFolder.ServerlessFunction,
|
FileFolder.ServerlessFunction,
|
||||||
serverlessFunction.workspaceId,
|
|
||||||
serverlessFunction.id,
|
serverlessFunction.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,21 +1,23 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { SortDirection } from '@ptc-org/nestjs-query-core';
|
||||||
import {
|
import {
|
||||||
NestjsQueryGraphQLModule,
|
NestjsQueryGraphQLModule,
|
||||||
PagingStrategies,
|
PagingStrategies,
|
||||||
} from '@ptc-org/nestjs-query-graphql';
|
} from '@ptc-org/nestjs-query-graphql';
|
||||||
import { SortDirection } from '@ptc-org/nestjs-query-core';
|
|
||||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
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 { 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -27,6 +29,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
|||||||
'metadata',
|
'metadata',
|
||||||
),
|
),
|
||||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||||
|
FileModule,
|
||||||
],
|
],
|
||||||
services: [ServerlessFunctionService],
|
services: [ServerlessFunctionService],
|
||||||
resolvers: [
|
resolvers: [
|
||||||
@ -42,6 +45,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
|||||||
update: { disabled: true },
|
update: { disabled: true },
|
||||||
delete: { disabled: true },
|
delete: { disabled: true },
|
||||||
guards: [JwtAuthGuard],
|
guards: [JwtAuthGuard],
|
||||||
|
interceptors: [ServerlessFunctionInterceptor],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
@ -21,6 +21,7 @@ import {
|
|||||||
ServerlessFunctionException,
|
ServerlessFunctionException,
|
||||||
ServerlessFunctionExceptionCode,
|
ServerlessFunctionExceptionCode,
|
||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
} 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 { 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';
|
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)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async updateOneServerlessFunction(
|
async updateOneServerlessFunction(
|
||||||
@Args('input')
|
@Args('input')
|
||||||
@ -84,6 +86,7 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseInterceptors(ServerlessFunctionInterceptor)
|
||||||
@Mutation(() => ServerlessFunctionDTO)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async createOneServerlessFunction(
|
async createOneServerlessFunction(
|
||||||
@Args('input')
|
@Args('input')
|
||||||
@ -106,6 +109,7 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseInterceptors(ServerlessFunctionInterceptor)
|
||||||
@Mutation(() => ServerlessFunctionDTO)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async createOneServerlessFunctionFromFile(
|
async createOneServerlessFunctionFromFile(
|
||||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||||
|
|||||||
@ -3,15 +3,20 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||||
import { FileUpload } from 'graphql-upload';
|
import { FileUpload } from 'graphql-upload';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
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 { ServerlessExecuteResult } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.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 { 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 {
|
import {
|
||||||
ServerlessFunctionEntity,
|
ServerlessFunctionEntity,
|
||||||
ServerlessFunctionSyncStatus,
|
ServerlessFunctionSyncStatus,
|
||||||
@ -20,12 +25,7 @@ import {
|
|||||||
ServerlessFunctionException,
|
ServerlessFunctionException,
|
||||||
ServerlessFunctionExceptionCode,
|
ServerlessFunctionExceptionCode,
|
||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
} 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 { 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()
|
@Injectable()
|
||||||
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {
|
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {
|
||||||
@ -119,8 +119,8 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
|||||||
|
|
||||||
if (codeHasChanged) {
|
if (codeHasChanged) {
|
||||||
const fileFolder = join(
|
const fileFolder = join(
|
||||||
|
'workspace-' + workspaceId,
|
||||||
FileFolder.ServerlessFunction,
|
FileFolder.ServerlessFunction,
|
||||||
workspaceId,
|
|
||||||
existingServerlessFunction.id,
|
existingServerlessFunction.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -164,13 +164,18 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
|||||||
|
|
||||||
const serverlessFunctionId = v4();
|
const serverlessFunctionId = v4();
|
||||||
|
|
||||||
const fileFolder = join(
|
const fileFolderWithoutWorkspace = join(
|
||||||
FileFolder.ServerlessFunction,
|
FileFolder.ServerlessFunction,
|
||||||
workspaceId,
|
|
||||||
serverlessFunctionId,
|
serverlessFunctionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const sourceCodeFullPath = fileFolder + '/' + SOURCE_FILE_NAME;
|
const fileFolder = join(
|
||||||
|
'workspace-' + workspaceId,
|
||||||
|
fileFolderWithoutWorkspace,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourceCodeFullPath =
|
||||||
|
fileFolderWithoutWorkspace + '/' + SOURCE_FILE_NAME;
|
||||||
|
|
||||||
const serverlessFunction = await super.createOne({
|
const serverlessFunction = await super.createOne({
|
||||||
...serverlessFunctionInput,
|
...serverlessFunctionInput,
|
||||||
|
|||||||
@ -180,7 +180,6 @@ yarn command:prod cron:calendar:calendar-event-list-fetch
|
|||||||
### Data enrichment and AI
|
### Data enrichment and AI
|
||||||
|
|
||||||
<ArticleTable options={[
|
<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"],
|
['OPENAI_API_KEY', 'sk-proj-abcdabcd...', "OpenAI API key"],
|
||||||
['LLM_CHAT_MODEL_DRIVER', 'openai', "LLM provider"],
|
['LLM_CHAT_MODEL_DRIVER', 'openai', "LLM provider"],
|
||||||
['LLM_TRACING_DRIVER', 'langfuse', "Where to output LangChain logs. 'langfuse' or 'console'."],
|
['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"],
|
['LANGFUSE_PUBLIC_KEY', 'pk-lf-abcdabcd-abcd...', "Langfuse public key"],
|
||||||
]}></ArticleTable>
|
]}></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
|
### Support Chat
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user