Enable roles on api keys (#13334)
This commit is contained in:
-212
@@ -1,212 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApiKeyService } from 'src/engine/core-modules/api-key/api-key.service';
|
||||
import { Webhook } from 'src/engine/core-modules/webhook/webhook.entity';
|
||||
import { WebhookService } from 'src/engine/core-modules/webhook/webhook.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||
import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-1:migrate-api-keys-webhooks-to-core',
|
||||
description:
|
||||
'Migrate API keys and webhooks from workspace schemas to core schema',
|
||||
})
|
||||
export class MigrateApiKeysWebhooksToCoreCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(ApiKey, 'core')
|
||||
private readonly coreApiKeyRepository: Repository<ApiKey>,
|
||||
@InjectRepository(Webhook, 'core')
|
||||
private readonly coreWebhookRepository: Repository<Webhook>,
|
||||
private readonly apiKeyService: ApiKeyService,
|
||||
private readonly webhookService: WebhookService,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Migrating API keys and webhooks for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.migrateApiKeys(workspaceId, options.dryRun);
|
||||
|
||||
await this.migrateWebhooks(workspaceId, options.dryRun);
|
||||
|
||||
this.logger.log(
|
||||
`Successfully migrated API keys and webhooks for workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to migrate API keys and webhooks for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateApiKeys(
|
||||
workspaceId: string,
|
||||
dryRun?: boolean,
|
||||
): Promise<void> {
|
||||
const workspaceApiKeyRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ApiKeyWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'apiKey',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workspaceApiKeys = await workspaceApiKeyRepository.find({
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (workspaceApiKeys.length === 0) {
|
||||
this.logger.log(`No API keys to migrate for workspace ${workspaceId}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'DRY RUN: ' : ''}Found ${workspaceApiKeys.length} API keys to migrate for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
workspaceApiKeys.forEach((apiKey) => {
|
||||
const deletedStatus = apiKey.deletedAt ? ' (DELETED)' : '';
|
||||
|
||||
this.logger.log(
|
||||
`DRY RUN: Would migrate API key ${apiKey.id} (${apiKey.name})${deletedStatus} from workspace ${workspaceId}`,
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const existingCoreApiKeys = await this.coreApiKeyRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id'],
|
||||
withDeleted: true,
|
||||
});
|
||||
const existingApiKeyIds = new Set(existingCoreApiKeys.map((ak) => ak.id));
|
||||
|
||||
for (const workspaceApiKey of workspaceApiKeys) {
|
||||
if (existingApiKeyIds.has(workspaceApiKey.id)) {
|
||||
this.logger.warn(
|
||||
`API key ${workspaceApiKey.id} already exists in core schema for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.apiKeyService.create({
|
||||
id: workspaceApiKey.id,
|
||||
name: workspaceApiKey.name,
|
||||
expiresAt: workspaceApiKey.expiresAt,
|
||||
revokedAt: workspaceApiKey.revokedAt
|
||||
? new Date(workspaceApiKey.revokedAt)
|
||||
: workspaceApiKey.deletedAt
|
||||
? new Date(workspaceApiKey.deletedAt)
|
||||
: undefined,
|
||||
workspaceId,
|
||||
createdAt: new Date(workspaceApiKey.createdAt),
|
||||
updatedAt: new Date(workspaceApiKey.updatedAt),
|
||||
});
|
||||
|
||||
const deletedStatus = workspaceApiKey.deletedAt ? ' (DELETED)' : '';
|
||||
|
||||
this.logger.log(
|
||||
`Migrated API key ${workspaceApiKey.id} (${workspaceApiKey.name})${deletedStatus} to core schema`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateWebhooks(
|
||||
workspaceId: string,
|
||||
dryRun?: boolean,
|
||||
): Promise<void> {
|
||||
const workspaceWebhookRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WebhookWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'webhook',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workspaceWebhooks = await workspaceWebhookRepository.find({
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (workspaceWebhooks.length === 0) {
|
||||
this.logger.log(`No webhooks to migrate for workspace ${workspaceId}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'DRY RUN: ' : ''}Found ${workspaceWebhooks.length} webhooks to migrate for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
workspaceWebhooks.forEach((webhook) => {
|
||||
const deletedStatus = webhook.deletedAt ? ' (DELETED)' : '';
|
||||
|
||||
this.logger.log(
|
||||
`DRY RUN: Would migrate webhook ${webhook.id} (${webhook.targetUrl})${deletedStatus} from workspace ${workspaceId}`,
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const existingCoreWebhooks = await this.coreWebhookRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id'],
|
||||
withDeleted: true,
|
||||
});
|
||||
const existingWebhookIds = new Set(existingCoreWebhooks.map((wh) => wh.id));
|
||||
|
||||
for (const workspaceWebhook of workspaceWebhooks) {
|
||||
if (existingWebhookIds.has(workspaceWebhook.id)) {
|
||||
this.logger.warn(
|
||||
`Webhook ${workspaceWebhook.id} already exists in core schema for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.webhookService.create({
|
||||
id: workspaceWebhook.id,
|
||||
targetUrl: workspaceWebhook.targetUrl,
|
||||
operations: workspaceWebhook.operations,
|
||||
description: workspaceWebhook.description,
|
||||
secret: workspaceWebhook.secret,
|
||||
workspaceId,
|
||||
createdAt: new Date(workspaceWebhook.createdAt),
|
||||
updatedAt: new Date(workspaceWebhook.updatedAt),
|
||||
deletedAt: workspaceWebhook.deletedAt
|
||||
? new Date(workspaceWebhook.deletedAt)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const deletedStatus = workspaceWebhook.deletedAt ? ' (DELETED)' : '';
|
||||
|
||||
this.logger.log(
|
||||
`Migrated webhook ${workspaceWebhook.id} (${workspaceWebhook.targetUrl})${deletedStatus} to core schema`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AddEnqueuedStatusToWorkflowRunCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-add-enqueued-status-to-workflow-run.command';
|
||||
import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command';
|
||||
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
|
||||
import { MigrateApiKeysWebhooksToCoreCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-api-keys-webhooks-to-core.command';
|
||||
import { MigrateWorkflowRunStatesCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-workflow-run-state.command';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
@@ -49,14 +48,12 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
|
||||
FixUpdateStandardFieldsIsLabelSyncedWithName,
|
||||
FixSchemaArrayTypeCommand,
|
||||
MigrateWorkflowRunStatesCommand,
|
||||
MigrateApiKeysWebhooksToCoreCommand,
|
||||
AddEnqueuedStatusToWorkflowRunCommand,
|
||||
],
|
||||
exports: [
|
||||
FixUpdateStandardFieldsIsLabelSyncedWithName,
|
||||
FixSchemaArrayTypeCommand,
|
||||
MigrateWorkflowRunStatesCommand,
|
||||
MigrateApiKeysWebhooksToCoreCommand,
|
||||
AddEnqueuedStatusToWorkflowRunCommand,
|
||||
],
|
||||
})
|
||||
|
||||
+2
-2
@@ -19,13 +19,13 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
],
|
||||
providers: [
|
||||
RemoveWorkflowRunsWithoutState,
|
||||
AddNextStepIdsToWorkflowVersionTriggers,
|
||||
AddEnqueuedStatusToWorkflowRunV2Command,
|
||||
AddNextStepIdsToWorkflowVersionTriggers,
|
||||
],
|
||||
exports: [
|
||||
RemoveWorkflowRunsWithoutState,
|
||||
AddNextStepIdsToWorkflowVersionTriggers,
|
||||
AddEnqueuedStatusToWorkflowRunV2Command,
|
||||
AddNextStepIdsToWorkflowVersionTriggers,
|
||||
],
|
||||
})
|
||||
export class V1_2_UpgradeVersionCommandModule {}
|
||||
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, In, QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-3:assign-roles-to-existing-api-keys',
|
||||
description:
|
||||
'Assign Admin roles to existing API keys that lack role assignments. ' +
|
||||
'This ensures existing integrations continue to work after enabling role-based permissions.',
|
||||
})
|
||||
export class AssignRolesToExistingApiKeysCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(ApiKey, 'core')
|
||||
private readonly apiKeyRepository: Repository<ApiKey>,
|
||||
@InjectRepository(RoleEntity, 'core')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(RoleTargetsEntity, 'core')
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectDataSource('core')
|
||||
private readonly dataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Assigning roles to existing API keys for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
|
||||
try {
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const result = await this.assignRolesToWorkspaceApiKeys(
|
||||
workspaceId,
|
||||
options.dryRun ?? false,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (result.failed.length > 0) {
|
||||
this.logger.warn(
|
||||
`Workspace ${workspaceId}: Processed ${result.processed}, Assigned roles to ${result.assigned} API keys, Failed: ${result.failed.length}`,
|
||||
);
|
||||
this.logger.warn(
|
||||
`Failed API keys: ${result.failed.map((f) => `${f.name} (${f.id}): ${f.error}`).join(', ')}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to assign roles to ${result.failed.length} API keys`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Workspace ${workspaceId}: Processed ${result.processed}, Assigned roles to ${result.assigned} API keys`,
|
||||
);
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would enable IS_API_KEY_ROLES_ENABLED feature flag for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
const shouldEnableFeatureFlagAndRecomputeCache =
|
||||
result.assigned > 0 ||
|
||||
(await this.shouldEnableForZeroApiKeys(workspaceId));
|
||||
|
||||
if (shouldEnableFeatureFlagAndRecomputeCache) {
|
||||
await this.enableApiKeyRolesFeatureFlagWithTransaction(
|
||||
workspaceId,
|
||||
result,
|
||||
queryRunner,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`All API keys already have roles and feature flag IS_API_KEY_ROLES_ENABLED already enabled for workspace ${workspaceId}, no action needed`,
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
if (shouldEnableFeatureFlagAndRecomputeCache) {
|
||||
try {
|
||||
await this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
this.logger.log(
|
||||
`Recomputed API key role cache and feature flag cache for workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to recompute API key role cache and feature flag cache for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (queryRunner.isTransactionActive) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Transaction rolled back for workspace ${workspaceId} due to error: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to assign roles to existing API keys for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
private async assignRolesToWorkspaceApiKeys(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<{
|
||||
processed: number;
|
||||
assigned: number;
|
||||
failed: Array<{ id: string; name: string; error: string }>;
|
||||
}> {
|
||||
const apiKeys = await this.apiKeyRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'name', 'workspaceId'],
|
||||
});
|
||||
|
||||
if (apiKeys.length === 0) {
|
||||
this.logger.log(`No API keys found in workspace ${workspaceId}`);
|
||||
|
||||
return { processed: 0, assigned: 0, failed: [] };
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${apiKeys.length} API keys in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const apiKeyIds = apiKeys.map((key) => key.id);
|
||||
const existingRoleTargets = await this.roleTargetsRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
apiKeyId: In(apiKeyIds),
|
||||
},
|
||||
});
|
||||
|
||||
const apiKeysWithRoles = new Set(
|
||||
existingRoleTargets.map((rt) => rt.apiKeyId),
|
||||
);
|
||||
const apiKeysWithoutRoles = apiKeys.filter(
|
||||
(key) => !apiKeysWithRoles.has(key.id),
|
||||
);
|
||||
|
||||
if (apiKeysWithoutRoles.length === 0) {
|
||||
this.logger.log(
|
||||
`All API keys already have role assignments for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return { processed: apiKeys.length, assigned: 0, failed: [] };
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'DRY RUN: ' : ''}Found ${apiKeysWithoutRoles.length} API keys without role assignments for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would assign Admin roles to ${apiKeysWithoutRoles.length} API keys for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
processed: apiKeys.length,
|
||||
assigned: apiKeysWithoutRoles.length,
|
||||
failed: [],
|
||||
};
|
||||
}
|
||||
|
||||
const adminRole = await this.roleRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
label: ADMIN_ROLE_LABEL,
|
||||
},
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
throw new Error(
|
||||
`No Admin role found in workspace ${workspaceId}. Should not happen.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Using Admin role ${adminRole.id} for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
try {
|
||||
let assignedCount = 0;
|
||||
|
||||
for (const apiKey of apiKeysWithoutRoles) {
|
||||
await queryRunner.manager.delete(RoleTargetsEntity, {
|
||||
apiKeyId: apiKey.id,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const roleTarget = queryRunner.manager.create(RoleTargetsEntity, {
|
||||
apiKeyId: apiKey.id,
|
||||
roleId: adminRole.id,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await queryRunner.manager.save(roleTarget);
|
||||
|
||||
this.logger.log(
|
||||
`Assigned Admin role to API key "${apiKey.name}" (${apiKey.id})`,
|
||||
);
|
||||
assignedCount++;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully assigned roles to ${assignedCount} API keys for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
processed: apiKeys.length,
|
||||
assigned: assignedCount,
|
||||
failed: [],
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to assign roles to API keys for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
const failedApiKeys = apiKeysWithoutRoles.map((apiKey) => ({
|
||||
id: apiKey.id,
|
||||
name: apiKey.name,
|
||||
error: errorMessage,
|
||||
}));
|
||||
|
||||
return {
|
||||
processed: apiKeys.length,
|
||||
assigned: 0,
|
||||
failed: failedApiKeys,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async enableApiKeyRolesFeatureFlagWithTransaction(
|
||||
workspaceId: string,
|
||||
result: { processed: number; assigned: number },
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
const shouldEnableFeatureFlag =
|
||||
result.processed > 0 ||
|
||||
(await this.shouldEnableForZeroApiKeys(workspaceId));
|
||||
|
||||
if (shouldEnableFeatureFlag) {
|
||||
try {
|
||||
const existingFeatureFlag = await queryRunner.manager.findOne(
|
||||
FeatureFlag,
|
||||
{
|
||||
where: {
|
||||
key: FeatureFlagKey.IS_API_KEY_ROLES_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const featureFlagToSave = existingFeatureFlag
|
||||
? {
|
||||
...existingFeatureFlag,
|
||||
value: true,
|
||||
}
|
||||
: {
|
||||
key: FeatureFlagKey.IS_API_KEY_ROLES_ENABLED,
|
||||
value: true,
|
||||
workspaceId: workspaceId,
|
||||
};
|
||||
|
||||
await queryRunner.manager.save(FeatureFlag, featureFlagToSave);
|
||||
|
||||
this.logger.log(
|
||||
`Enabled IS_API_KEY_ROLES_ENABLED feature flag for workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to enable feature flag for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to enable API key roles feature flag for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Feature flag already enabled for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async shouldEnableForZeroApiKeys(
|
||||
workspaceId: string,
|
||||
): Promise<boolean> {
|
||||
const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_API_KEY_ROLES_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return !isAlreadyEnabled;
|
||||
}
|
||||
}
|
||||
+36
-3
@@ -1,8 +1,41 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AssignRolesToExistingApiKeysCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-assign-roles-to-existing-api-keys.command';
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
||||
import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [],
|
||||
exports: [],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature(
|
||||
[
|
||||
Workspace,
|
||||
ApiKey,
|
||||
FieldMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
RoleEntity,
|
||||
RoleTargetsEntity,
|
||||
],
|
||||
'core',
|
||||
),
|
||||
ApiKeyModule,
|
||||
FeatureFlagModule,
|
||||
TwentyORMModule,
|
||||
RoleModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
WorkspaceFeatureFlagsMapCacheModule,
|
||||
],
|
||||
providers: [AssignRolesToExistingApiKeysCommand],
|
||||
exports: [AssignRolesToExistingApiKeysCommand],
|
||||
})
|
||||
export class V1_3_UpgradeVersionCommandModule {}
|
||||
|
||||
+3
-4
@@ -24,11 +24,11 @@ import { DeduplicateIndexedFieldsCommand } from 'src/database/commands/upgrade-v
|
||||
import { AddEnqueuedStatusToWorkflowRunCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-add-enqueued-status-to-workflow-run.command';
|
||||
import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command';
|
||||
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
|
||||
import { MigrateApiKeysWebhooksToCoreCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-api-keys-webhooks-to-core.command';
|
||||
import { MigrateWorkflowRunStatesCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-workflow-run-state.command';
|
||||
import { AddEnqueuedStatusToWorkflowRunV2Command } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-enqueued-status-to-workflow-run-v2.command';
|
||||
import { AddNextStepIdsToWorkflowVersionTriggers } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-next-step-ids-to-workflow-version-triggers.command';
|
||||
import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-version-command/1-2/1-2-remove-workflow-runs-without-state.command';
|
||||
import { AssignRolesToExistingApiKeysCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-assign-roles-to-existing-api-keys.command';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
@@ -147,7 +147,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
// 1.1 Commands
|
||||
protected readonly fixSchemaArrayTypeCommand: FixSchemaArrayTypeCommand,
|
||||
protected readonly fixUpdateStandardFieldsIsLabelSyncedWithNameCommand: FixUpdateStandardFieldsIsLabelSyncedWithName,
|
||||
protected readonly migrateApiKeysWebhooksToCoreCommand: MigrateApiKeysWebhooksToCoreCommand,
|
||||
protected readonly migrateWorkflowRunStatesCommand: MigrateWorkflowRunStatesCommand,
|
||||
protected readonly addEnqueuedStatusToWorkflowRunCommand: AddEnqueuedStatusToWorkflowRunCommand,
|
||||
|
||||
@@ -157,6 +156,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly addEnqueuedStatusToWorkflowRunV2Command: AddEnqueuedStatusToWorkflowRunV2Command,
|
||||
|
||||
// 1.3 Commands
|
||||
protected readonly assignRolesToExistingApiKeysCommand: AssignRolesToExistingApiKeysCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -201,7 +201,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
beforeSyncMetadata: [
|
||||
this.fixUpdateStandardFieldsIsLabelSyncedWithNameCommand,
|
||||
this.fixSchemaArrayTypeCommand,
|
||||
this.migrateApiKeysWebhooksToCoreCommand,
|
||||
this.addEnqueuedStatusToWorkflowRunCommand,
|
||||
],
|
||||
afterSyncMetadata: [this.migrateWorkflowRunStatesCommand],
|
||||
@@ -213,7 +212,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
this.addNextStepIdsToWorkflowVersionTriggers,
|
||||
this.addEnqueuedStatusToWorkflowRunV2Command,
|
||||
],
|
||||
afterSyncMetadata: [],
|
||||
afterSyncMetadata: [this.assignRolesToExistingApiKeysCommand],
|
||||
};
|
||||
|
||||
const commands_130: VersionCommands = {
|
||||
|
||||
Reference in New Issue
Block a user