1895 extensibility v1 application tokens 3 (#16504)
- moves applicationRoleId to application entity - add new `APPLICATION` FieldActorSource and `APPLICATION` JwtTokenTypeEnum value - create a new token with applicationId when executing a function - when applicationId is in token, check for application.defaultRole permissions -use twenty-shared types in `twenty-sdk/application` - create a new import from generate called "Twenty" that you can use directly without having to set TWENTY_API_KEY AND TWENTY_API_URL (keep metadata or core parameter only) - provide to serverless unique one time BEARER TOKEN to run it Result <img width="977" height="566" alt="image" src="https://github.com/user-attachments/assets/e78428a0-5b13-4975-aa13-58ee3b32450c" /> <img width="910" height="596" alt="image" src="https://github.com/user-attachments/assets/6ec72bf5-7655-4093-a45e-ad269595a324" /> <img width="741" height="568" alt="image" src="https://github.com/user-attachments/assets/7683944c-fd79-4417-8fb2-8e4815cc112f" />
This commit is contained in:
-1
@@ -73,7 +73,6 @@ export const fromCreateAgentInputToFlatAgent = ({
|
||||
userWorkspaceId: null,
|
||||
agentId,
|
||||
apiKeyId: null,
|
||||
targetApplicationId: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
universalIdentifier: v4(),
|
||||
|
||||
-1
@@ -67,7 +67,6 @@ const computeAgentFlatRoleTargetToUpdate = ({
|
||||
userWorkspaceId: null,
|
||||
agentId: flatAgent.id,
|
||||
apiKeyId: null,
|
||||
targetApplicationId: null,
|
||||
createdAt: updatedAt,
|
||||
updatedAt,
|
||||
universalIdentifier: v4(),
|
||||
|
||||
-1
@@ -4,5 +4,4 @@ export const ROLE_TARGET_FOREIGN_KEY_PROPERTIES = [
|
||||
'userWorkspaceId',
|
||||
'apiKeyId',
|
||||
'agentId',
|
||||
'targetApplicationId',
|
||||
] as const satisfies (keyof FlatRoleTarget)[];
|
||||
|
||||
-1
@@ -11,7 +11,6 @@ export const fromRoleTargetsEntityToFlatRoleTarget = (
|
||||
userWorkspaceId: roleTarget.userWorkspaceId,
|
||||
agentId: roleTarget.agentId,
|
||||
apiKeyId: roleTarget.apiKeyId,
|
||||
targetApplicationId: roleTarget.targetApplicationId,
|
||||
applicationId: roleTarget.applicationId,
|
||||
universalIdentifier: roleTarget.universalIdentifier ?? roleTarget.id,
|
||||
createdAt: roleTarget.createdAt.toISOString(),
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
|
||||
workspaceId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
universalIdentifier: id,
|
||||
universalIdentifier: createRoleInput.universalIdentifier ?? id,
|
||||
applicationId,
|
||||
roleTargetIds: [],
|
||||
objectPermissionIds: [],
|
||||
|
||||
@@ -56,9 +56,6 @@ export class RoleTargetEntity extends SyncableEntity {
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
apiKeyId: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
targetApplicationId: string | null;
|
||||
|
||||
@ManyToOne(() => ApiKeyEntity, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'apiKeyId' })
|
||||
apiKey: Relation<ApiKeyEntity>;
|
||||
|
||||
-1
@@ -27,7 +27,6 @@ export const fromCreateRoleTargetInputToFlatRoleTargetToCreate = ({
|
||||
userWorkspaceId: null,
|
||||
agentId: null,
|
||||
apiKeyId: null,
|
||||
targetApplicationId: null,
|
||||
createdAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
universalIdentifier: universalIdentifier ?? v4(),
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { Field, HideField, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
@@ -9,6 +9,9 @@ export class CreateRoleInput {
|
||||
@Field({ nullable: true })
|
||||
id?: string;
|
||||
|
||||
@HideField()
|
||||
universalIdentifier?: string;
|
||||
|
||||
@IsString()
|
||||
@Field({ nullable: false })
|
||||
label: string;
|
||||
|
||||
+11
-9
@@ -88,22 +88,24 @@ export class RouteTriggerService {
|
||||
request: Request;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const { workspace } =
|
||||
const authContext =
|
||||
await this.accessTokenService.validateTokenByRequest(request);
|
||||
|
||||
if (!isDefined(workspace)) {
|
||||
if (!isDefined(authContext.workspace)) {
|
||||
throw new RouteTriggerException(
|
||||
'Workspace not found',
|
||||
RouteTriggerExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (workspace.id !== workspaceId) {
|
||||
if (authContext.workspace.id !== workspaceId) {
|
||||
throw new RouteTriggerException(
|
||||
'You are not authorized',
|
||||
RouteTriggerExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
return authContext;
|
||||
}
|
||||
|
||||
async handle({
|
||||
@@ -137,12 +139,12 @@ export class RouteTriggerService {
|
||||
};
|
||||
|
||||
const result =
|
||||
await this.serverlessFunctionService.executeOneServerlessFunction(
|
||||
routeTriggerWithPathParams.routeTrigger.serverlessFunction.id,
|
||||
routeTriggerWithPathParams.routeTrigger.workspaceId,
|
||||
executionParams,
|
||||
'draft',
|
||||
);
|
||||
await this.serverlessFunctionService.executeOneServerlessFunction({
|
||||
id: routeTriggerWithPathParams.routeTrigger.serverlessFunction.id,
|
||||
workspaceId: routeTriggerWithPathParams.routeTrigger.workspaceId,
|
||||
payload: executionParams,
|
||||
version: 'draft',
|
||||
});
|
||||
|
||||
if (!isDefined(result)) {
|
||||
return result;
|
||||
|
||||
+6
-6
@@ -22,11 +22,11 @@ export class ServerlessFunctionTriggerJob {
|
||||
|
||||
@Process(ServerlessFunctionTriggerJob.name)
|
||||
async handle(data: ServerlessFunctionTriggerJobData) {
|
||||
await this.serverlessFunctionService.executeOneServerlessFunction(
|
||||
data.serverlessFunctionId,
|
||||
data.workspaceId,
|
||||
data.payload || {},
|
||||
'draft',
|
||||
);
|
||||
await this.serverlessFunctionService.executeOneServerlessFunction({
|
||||
id: data.serverlessFunctionId,
|
||||
workspaceId: data.workspaceId,
|
||||
payload: data.payload || {},
|
||||
version: 'draft',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverl
|
||||
import { WorkspaceFlatServerlessFunctionMapCacheService } from 'src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service';
|
||||
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
|
||||
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -45,6 +46,7 @@ import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.modu
|
||||
WorkspaceMigrationV2Module,
|
||||
ServerlessFunctionLayerModule,
|
||||
SubscriptionsModule,
|
||||
TokenModule,
|
||||
],
|
||||
providers: [
|
||||
ServerlessFunctionService,
|
||||
|
||||
+2
-2
@@ -163,12 +163,12 @@ export class ServerlessFunctionResolver {
|
||||
try {
|
||||
const { id, payload, version } = input;
|
||||
|
||||
return await this.serverlessFunctionService.executeOneServerlessFunction(
|
||||
return await this.serverlessFunctionService.executeOneServerlessFunction({
|
||||
id,
|
||||
workspaceId,
|
||||
payload,
|
||||
version,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
|
||||
+55
-7
@@ -8,6 +8,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { RedisPubSub } from 'graphql-redis-subscriptions';
|
||||
import { Sources } from 'twenty-shared/types';
|
||||
import {
|
||||
DEFAULT_API_URL_NAME,
|
||||
DEFAULT_API_KEY_NAME,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { type ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
||||
@@ -33,6 +37,12 @@ import {
|
||||
WorkflowVersionStepExceptionCode,
|
||||
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
|
||||
import { SERVERLESS_FUNCTION_LOGS_TRIGGER } from 'src/engine/metadata-modules/serverless-function/constants/serverless-function-logs-trigger';
|
||||
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
|
||||
import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/build-env-var';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { cleanServerUrl } from 'src/utils/clean-server-url';
|
||||
|
||||
const MIN_TOKEN_EXPIRATION_IN_SECONDS = 5;
|
||||
|
||||
@Injectable()
|
||||
export class ServerlessFunctionService {
|
||||
@@ -45,6 +55,8 @@ export class ServerlessFunctionService {
|
||||
private readonly throttlerService: ThrottlerService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly auditService: AuditService,
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly applicationTokenService: ApplicationTokenService,
|
||||
@Inject('PUB_SUB')
|
||||
private readonly pubSub: RedisPubSub,
|
||||
) {}
|
||||
@@ -86,12 +98,17 @@ export class ServerlessFunctionService {
|
||||
}
|
||||
}
|
||||
|
||||
async executeOneServerlessFunction(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
payload: object,
|
||||
async executeOneServerlessFunction({
|
||||
id,
|
||||
workspaceId,
|
||||
payload,
|
||||
version = 'latest',
|
||||
): Promise<ServerlessExecuteResult> {
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
payload: object;
|
||||
version?: string;
|
||||
}): Promise<ServerlessExecuteResult> {
|
||||
await this.throttleExecution(workspaceId);
|
||||
|
||||
const functionToExecute =
|
||||
@@ -102,14 +119,45 @@ export class ServerlessFunctionService {
|
||||
},
|
||||
relations: [
|
||||
'serverlessFunctionLayer',
|
||||
'application',
|
||||
'application.applicationVariables',
|
||||
],
|
||||
});
|
||||
|
||||
const applicationAccessToken = isDefined(functionToExecute.applicationId)
|
||||
? await this.applicationTokenService.generateApplicationToken({
|
||||
workspaceId,
|
||||
applicationId: functionToExecute.applicationId,
|
||||
expiresInSeconds: Math.max(
|
||||
functionToExecute.timeoutSeconds,
|
||||
MIN_TOKEN_EXPIRATION_IN_SECONDS,
|
||||
),
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const baseUrl = cleanServerUrl(this.twentyConfigService.get('SERVER_URL'));
|
||||
|
||||
const envVariables = {
|
||||
...(isDefined(baseUrl)
|
||||
? {
|
||||
[DEFAULT_API_URL_NAME]: baseUrl,
|
||||
}
|
||||
: {}),
|
||||
...(isDefined(applicationAccessToken)
|
||||
? {
|
||||
[DEFAULT_API_KEY_NAME]: applicationAccessToken.token,
|
||||
}
|
||||
: {}),
|
||||
...buildEnvVar(functionToExecute),
|
||||
};
|
||||
|
||||
const resultServerlessFunction = await this.callWithTimeout({
|
||||
callback: () =>
|
||||
this.serverlessService.execute(functionToExecute, payload, version),
|
||||
this.serverlessService.execute({
|
||||
serverlessFunction: functionToExecute,
|
||||
payload,
|
||||
version,
|
||||
env: envVariables,
|
||||
}),
|
||||
timeoutMs: functionToExecute.timeoutSeconds * 1000,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user