diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 1d2461acf..bcc50ee6c 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -126,6 +126,15 @@ declare module "@fastify/request-context" { namespace: string; name: string; }; + aws?: { + accountId: string; + arn: string; + userId: string; + partition: string; + service: string; + resourceType: string; + resourceName: string; + }; }; identityPermissionMetadata?: Record; // filled by permission service assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string }; diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 7b4f5d90c..4fe639510 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -162,6 +162,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { kubernetes: token?.identityAuth?.kubernetes }); } + if (token?.identityAuth?.aws) { + requestContext.set("identityAuthInfo", { + identityId: identity.identityId, + aws: token?.identityAuth?.aws + }); + } break; } case AuthMode.SERVICE_TOKEN: { diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 135effc4c..029d4a848 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -270,11 +270,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } }); - - remainingLimit -= imports.length; - adjustedOffset = 0; - } else { - adjustedOffset = Math.max(0, adjustedOffset - totalImportCount); } } @@ -317,7 +312,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } - if (!includeDynamicSecrets && !includeSecrets) + if (!includeDynamicSecrets && !includeSecrets && !includeSecretRotations) return { folders, totalFolderCount, @@ -547,7 +542,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + - (totalImportCount ?? 0) + (totalSecretRotationCount ?? 0) }; } diff --git a/backend/src/server/routes/v3/external-migration-router.ts b/backend/src/server/routes/v3/external-migration-router.ts index 865287157..89621811d 100644 --- a/backend/src/server/routes/v3/external-migration-router.ts +++ b/backend/src/server/routes/v3/external-migration-router.ts @@ -1,9 +1,11 @@ import fastifyMultipart from "@fastify/multipart"; +import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; -import { readLimit } from "@app/server/config/rateLimiter"; +import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { VaultMappingType } from "@app/services/external-migration/external-migration-types"; const MB25_IN_BYTES = 26214400; @@ -15,7 +17,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider bodyLimit: MB25_IN_BYTES, url: "/env-key", config: { - rateLimit: readLimit + rateLimit: writeLimit }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { @@ -52,4 +54,30 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider }); } }); + + server.route({ + method: "POST", + url: "/vault", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + vaultAccessToken: z.string(), + vaultNamespace: z.string().trim().optional(), + vaultUrl: z.string(), + mappingType: z.nativeEnum(VaultMappingType) + }) + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + await server.services.migration.importVaultData({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body + }); + } + }); }; diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts index ed8401560..1c31741a1 100644 --- a/backend/src/server/routes/v3/index.ts +++ b/backend/src/server/routes/v3/index.ts @@ -11,5 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => { await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerSecretRouter, { prefix: "/secrets" }); await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" }); - await server.register(registerExternalMigrationRouter, { prefix: "/migrate" }); + await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" }); }; diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts index d9adc91dd..4c671986f 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts @@ -164,7 +164,7 @@ export const validateSqlConnectionCredentials = async ( ) => { try { await executeWithPotentialGateway(config, gatewayService, async (client) => { - await client.raw(`Select 1`); + await client.raw(config.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`); }); return config.credentials; } catch (error) { diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns/envkey.ts similarity index 61% rename from backend/src/services/external-migration/external-migration-fns.ts rename to backend/src/services/external-migration/external-migration-fns/envkey.ts index 8af22d858..ffdf80e9c 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns/envkey.ts @@ -1,32 +1,26 @@ -import slugify from "@sindresorhus/slugify"; import sjcl from "sjcl"; import tweetnacl from "tweetnacl"; import tweetnaclUtil from "tweetnacl-util"; -import { SecretType, TSecretFolders } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { chunkArray } from "@app/lib/fn"; +import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; -import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; -import { TKmsServiceFactory } from "../kms/kms-service"; -import { KmsDataKey } from "../kms/kms-types"; -import { TProjectDALFactory } from "../project/project-dal"; -import { TProjectServiceFactory } from "../project/project-service"; -import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; -import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; -import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; -import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; -import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; -import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; -import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; -import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; -import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service"; -import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; -import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; -import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types"; +import { TFolderCommitServiceFactory } from "../../folder-commit/folder-commit-service"; +import { TKmsServiceFactory } from "../../kms/kms-service"; +import { TProjectDALFactory } from "../../project/project-dal"; +import { TProjectServiceFactory } from "../../project/project-service"; +import { TProjectEnvDALFactory } from "../../project-env/project-env-dal"; +import { TProjectEnvServiceFactory } from "../../project-env/project-env-service"; +import { TResourceMetadataDALFactory } from "../../resource-metadata/resource-metadata-dal"; +import { TSecretFolderDALFactory } from "../../secret-folder/secret-folder-dal"; +import { TSecretFolderVersionDALFactory } from "../../secret-folder/secret-folder-version-dal"; +import { TSecretTagDALFactory } from "../../secret-tag/secret-tag-dal"; +import { TSecretV2BridgeDALFactory } from "../../secret-v2-bridge/secret-v2-bridge-dal"; +import type { TSecretV2BridgeServiceFactory } from "../../secret-v2-bridge/secret-v2-bridge-service"; +import { TSecretVersionV2DALFactory } from "../../secret-v2-bridge/secret-version-dal"; +import { TSecretVersionV2TagDALFactory } from "../../secret-v2-bridge/secret-version-tag-dal"; +import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "../external-migration-types"; export type TImportDataIntoInfisicalDTO = { projectDAL: Pick; @@ -499,326 +493,3 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise { - // Import data to infisical - if (!data || !data.projects) { - throw new BadRequestError({ message: "No projects found in data" }); - } - - const originalToNewProjectId = new Map(); - const originalToNewEnvironmentId = new Map< - string, - { envId: string; envSlug: string; rootFolderId: string; projectId: string } - >(); - const originalToNewFolderId = new Map< - string, - { - folderId: string; - projectId: string; - } - >(); - const projectsNotImported: string[] = []; - - await projectDAL.transaction(async (tx) => { - for await (const project of data.projects) { - const newProject = await projectService - .createProject({ - actor, - actorId, - actorOrgId, - actorAuthMethod, - workspaceName: project.name, - createDefaultEnvs: false, - tx - }) - .catch((e) => { - logger.error(e, `Failed to import to project [name:${project.name}]`); - throw new BadRequestError({ message: `Failed to import to project [name:${project.name}]` }); - }); - originalToNewProjectId.set(project.id, newProject.id); - } - - // Import environments - if (data.environments) { - for await (const environment of data.environments) { - const projectId = originalToNewProjectId.get(environment.projectId); - const slug = slugify(`${environment.name}-${alphaNumericNanoId(4)}`); - - if (!projectId) { - projectsNotImported.push(environment.projectId); - // eslint-disable-next-line no-continue - continue; - } - - const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx); - - if (existingEnv) { - throw new BadRequestError({ - message: `Environment with slug '${slug}' already exist`, - name: "CreateEnvironment" - }); - } - - const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx); - const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx); - const folder = await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx); - - originalToNewEnvironmentId.set(environment.id, { - envSlug: doc.slug, - envId: doc.id, - rootFolderId: folder.id, - projectId - }); - } - } - - if (data.folders) { - for await (const folder of data.folders) { - const parentEnv = originalToNewEnvironmentId.get(folder.parentFolderId as string); - - if (!parentEnv) { - // eslint-disable-next-line no-continue - continue; - } - - const newFolder = await folderDAL.create( - { - name: folder.name, - envId: parentEnv.envId, - parentId: parentEnv.rootFolderId - }, - tx - ); - - const newFolderVersion = await folderVersionDAL.create( - { - name: newFolder.name, - envId: newFolder.envId, - version: newFolder.version, - folderId: newFolder.id - }, - tx - ); - - await folderCommitService.createCommit( - { - actor: { - type: actor, - metadata: { - id: actorId - } - }, - message: "Changed by external migration", - folderId: parentEnv.rootFolderId, - changes: [ - { - type: CommitType.ADD, - folderVersionId: newFolderVersion.id - } - ] - }, - tx - ); - - originalToNewFolderId.set(folder.id, { - folderId: newFolder.id, - projectId: parentEnv.projectId - }); - } - } - - // Useful for debugging: - // console.log("data.secrets", data.secrets); - // console.log("data.folders", data.folders); - // console.log("data.environment", data.environments); - - if (data.secrets && data.secrets.length > 0) { - const mappedToEnvironmentId = new Map< - string, - { - secretKey: string; - secretValue: string; - folderId?: string; - isFromBlock?: boolean; - }[] - >(); - - for (const secret of data.secrets) { - const targetId = secret.folderId || secret.environmentId; - - // Skip if we can't find either an environment or folder mapping for this secret - if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) { - logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret"); - - // eslint-disable-next-line no-continue - continue; - } - - if (!mappedToEnvironmentId.has(targetId)) { - mappedToEnvironmentId.set(targetId, []); - } - - const alreadyHasSecret = mappedToEnvironmentId - .get(targetId)! - .find((el) => el.secretKey === secret.name && el.folderId === secret.folderId); - - if (alreadyHasSecret && alreadyHasSecret.isFromBlock) { - // remove the existing secret if any - mappedToEnvironmentId - .get(targetId)! - .splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1); - } - mappedToEnvironmentId.get(targetId)!.push({ - secretKey: secret.name, - secretValue: secret.value || "", - folderId: secret.folderId, - isFromBlock: secret.appBlockOrderIndex !== undefined - }); - } - - // for each of the mappedEnvironmentId - for await (const [targetId, secrets] of mappedToEnvironmentId) { - logger.info("[importDataIntoInfisicalFn]: Processing secrets for targetId", targetId); - - let selectedFolder: TSecretFolders | undefined; - let selectedProjectId: string | undefined; - - // Case 1: Secret belongs to a folder / branch / branch of a block - const foundFolder = originalToNewFolderId.get(targetId); - if (foundFolder) { - logger.info("[importDataIntoInfisicalFn]: Processing secrets for folder"); - selectedFolder = await folderDAL.findById(foundFolder.folderId, tx); - selectedProjectId = foundFolder.projectId; - } else { - logger.info("[importDataIntoInfisicalFn]: Processing secrets for normal environment"); - const environment = data.environments.find((env) => env.id === targetId); - if (!environment) { - logger.info( - { - targetId - }, - "[importDataIntoInfisicalFn]: Could not find environment for secret" - ); - // eslint-disable-next-line no-continue - continue; - } - - const projectId = originalToNewProjectId.get(environment.projectId)!; - - if (!projectId) { - throw new BadRequestError({ message: `Failed to import secret, project not found` }); - } - - const env = originalToNewEnvironmentId.get(targetId); - if (!env) { - logger.info( - { - targetId - }, - "[importDataIntoInfisicalFn]: Could not find environment for secret" - ); - - // eslint-disable-next-line no-continue - continue; - } - - const folder = await folderDAL.findBySecretPath(projectId, env.envSlug, "/", tx); - - if (!folder) { - throw new NotFoundError({ - message: `Folder not found for the given environment slug (${env.envSlug}) & secret path (/)`, - name: "Create secret" - }); - } - - selectedFolder = folder; - selectedProjectId = projectId; - } - - if (!selectedFolder) { - throw new NotFoundError({ - message: `Folder not found for the given environment slug & secret path`, - name: "CreateSecret" - }); - } - - if (!selectedProjectId) { - throw new NotFoundError({ - message: `Project not found for the given environment slug & secret path`, - name: "CreateSecret" - }); - } - - const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey( - { - type: KmsDataKey.SecretManager, - projectId: selectedProjectId - }, - tx - ); - - const secretBatches = chunkArray(secrets, 2500); - for await (const secretBatch of secretBatches) { - const secretsByKeys = await secretDAL.findBySecretKeys( - selectedFolder.id, - secretBatch.map((el) => ({ - key: el.secretKey, - type: SecretType.Shared - })), - tx - ); - if (secretsByKeys.length) { - throw new BadRequestError({ - message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}` - }); - } - await fnSecretBulkInsert({ - inputSecrets: secretBatch.map((el) => { - const references = getAllSecretReferences(el.secretValue).nestedReferences; - - return { - version: 1, - encryptedValue: el.secretValue - ? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob - : undefined, - key: el.secretKey, - references, - type: SecretType.Shared - }; - }), - folderId: selectedFolder.id, - orgId: actorOrgId, - resourceMetadataDAL, - secretDAL, - secretVersionDAL, - secretTagDAL, - secretVersionTagDAL, - folderCommitService, - actor: { - type: actor, - actorId - }, - tx - }); - } - } - } - }); - - return { projectsNotImported }; -}; diff --git a/backend/src/services/external-migration/external-migration-fns/import.ts b/backend/src/services/external-migration/external-migration-fns/import.ts new file mode 100644 index 000000000..5728bf1c0 --- /dev/null +++ b/backend/src/services/external-migration/external-migration-fns/import.ts @@ -0,0 +1,352 @@ +import slugify from "@sindresorhus/slugify"; + +import { SecretType, TSecretFolders } from "@app/db/schemas"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { chunkArray } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { CommitType } from "@app/services/folder-commit/folder-commit-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { fnSecretBulkInsert, getAllSecretReferences } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; + +import { TImportDataIntoInfisicalDTO } from "./envkey"; + +export const importDataIntoInfisicalFn = async ({ + projectService, + projectEnvDAL, + projectDAL, + secretDAL, + kmsService, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + folderDAL, + resourceMetadataDAL, + folderVersionDAL, + folderCommitService, + input: { data, actor, actorId, actorOrgId, actorAuthMethod } +}: TImportDataIntoInfisicalDTO) => { + // Import data to infisical + if (!data || !data.projects) { + throw new BadRequestError({ message: "No projects found in data" }); + } + + const originalToNewProjectId = new Map(); + const originalToNewEnvironmentId = new Map< + string, + { envId: string; envSlug: string; rootFolderId?: string; projectId: string } + >(); + const originalToNewFolderId = new Map< + string, + { + envId: string; + envSlug: string; + folderId: string; + projectId: string; + } + >(); + const projectsNotImported: string[] = []; + + await projectDAL.transaction(async (tx) => { + for await (const project of data.projects) { + const newProject = await projectService + .createProject({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + workspaceName: project.name, + createDefaultEnvs: false, + tx + }) + .catch((e) => { + logger.error(e, `Failed to import to project [name:${project.name}]`); + throw new BadRequestError({ message: `Failed to import to project [name:${project.name}]` }); + }); + originalToNewProjectId.set(project.id, newProject.id); + } + + // Import environments + if (data.environments) { + for await (const environment of data.environments) { + const projectId = originalToNewProjectId.get(environment.projectId); + const slug = slugify(`${environment.name}-${alphaNumericNanoId(4)}`); + + if (!projectId) { + projectsNotImported.push(environment.projectId); + // eslint-disable-next-line no-continue + continue; + } + + const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx); + + if (existingEnv) { + throw new BadRequestError({ + message: `Environment with slug '${slug}' already exist`, + name: "CreateEnvironment" + }); + } + + const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx); + const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx); + const folder = await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx); + + originalToNewEnvironmentId.set(environment.id, { + envSlug: doc.slug, + envId: doc.id, + rootFolderId: folder.id, + projectId + }); + } + } + + if (data.folders) { + for await (const folder of data.folders) { + const parentEnv = originalToNewEnvironmentId.get(folder.parentFolderId as string); + const parentFolder = originalToNewFolderId.get(folder.parentFolderId as string); + + let newFolder: TSecretFolders; + + if (parentEnv?.rootFolderId) { + newFolder = await folderDAL.create( + { + name: folder.name, + envId: parentEnv.envId, + parentId: parentEnv.rootFolderId + }, + tx + ); + } else if (parentFolder) { + newFolder = await folderDAL.create( + { + name: folder.name, + envId: parentFolder.envId, + parentId: parentFolder.folderId + }, + tx + ); + } else { + logger.info({ folder }, "No parent environment found for folder"); + // eslint-disable-next-line no-continue + continue; + } + + const newFolderVersion = await folderVersionDAL.create( + { + name: newFolder.name, + envId: newFolder.envId, + version: newFolder.version, + folderId: newFolder.id + }, + tx + ); + + await folderCommitService.createCommit( + { + actor: { + type: actor, + metadata: { + id: actorId + } + }, + message: "Changed by external migration", + folderId: parentEnv?.rootFolderId || parentFolder?.folderId || "", + changes: [ + { + type: CommitType.ADD, + folderVersionId: newFolderVersion.id + } + ] + }, + tx + ); + + originalToNewFolderId.set(folder.id, { + folderId: newFolder.id, + envId: parentEnv?.envId || parentFolder?.envId || "", + envSlug: parentEnv?.envSlug || parentFolder?.envSlug || "", + projectId: parentEnv?.projectId || parentFolder?.projectId || "" + }); + } + } + + // Useful for debugging: + // console.log("data.secrets", data.secrets); + // console.log("data.folders", data.folders); + // console.log("data.environment", data.environments); + + if (data.secrets && data.secrets.length > 0) { + const mappedToEnvironmentId = new Map< + string, + { + secretKey: string; + secretValue: string; + folderId?: string; + isFromBlock?: boolean; + }[] + >(); + + for (const secret of data.secrets) { + const targetId = secret.folderId || secret.environmentId; + + // Skip if we can't find either an environment or folder mapping for this secret + if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) { + logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret"); + + // eslint-disable-next-line no-continue + continue; + } + + if (!mappedToEnvironmentId.has(targetId)) { + mappedToEnvironmentId.set(targetId, []); + } + + const alreadyHasSecret = mappedToEnvironmentId + .get(targetId)! + .find((el) => el.secretKey === secret.name && el.folderId === secret.folderId); + + if (alreadyHasSecret && alreadyHasSecret.isFromBlock) { + // remove the existing secret if any + mappedToEnvironmentId + .get(targetId)! + .splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1); + } + mappedToEnvironmentId.get(targetId)!.push({ + secretKey: secret.name, + secretValue: secret.value || "", + folderId: secret.folderId, + isFromBlock: secret.appBlockOrderIndex !== undefined + }); + } + + // for each of the mappedEnvironmentId + for await (const [targetId, secrets] of mappedToEnvironmentId) { + logger.info("[importDataIntoInfisicalFn]: Processing secrets for targetId", targetId); + + let selectedFolder: TSecretFolders | undefined; + let selectedProjectId: string | undefined; + + // Case 1: Secret belongs to a folder / branch / branch of a block + const foundFolder = originalToNewFolderId.get(targetId); + if (foundFolder) { + logger.info("[importDataIntoInfisicalFn]: Processing secrets for folder"); + selectedFolder = await folderDAL.findById(foundFolder.folderId, tx); + selectedProjectId = foundFolder.projectId; + } else { + logger.info("[importDataIntoInfisicalFn]: Processing secrets for normal environment"); + const environment = data.environments.find((env) => env.id === targetId); + if (!environment) { + logger.info( + { + targetId + }, + "[importDataIntoInfisicalFn]: Could not find environment for secret" + ); + // eslint-disable-next-line no-continue + continue; + } + + const projectId = originalToNewProjectId.get(environment.projectId)!; + + if (!projectId) { + throw new BadRequestError({ message: `Failed to import secret, project not found` }); + } + + const env = originalToNewEnvironmentId.get(targetId); + if (!env) { + logger.info( + { + targetId + }, + "[importDataIntoInfisicalFn]: Could not find environment for secret" + ); + + // eslint-disable-next-line no-continue + continue; + } + + const folder = await folderDAL.findBySecretPath(projectId, env.envSlug, "/", tx); + + if (!folder) { + throw new NotFoundError({ + message: `Folder not found for the given environment slug (${env.envSlug}) & secret path (/)`, + name: "Create secret" + }); + } + + selectedFolder = folder; + selectedProjectId = projectId; + } + + if (!selectedFolder) { + throw new NotFoundError({ + message: `Folder not found for the given environment slug & secret path`, + name: "CreateSecret" + }); + } + + if (!selectedProjectId) { + throw new NotFoundError({ + message: `Project not found for the given environment slug & secret path`, + name: "CreateSecret" + }); + } + + const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.SecretManager, + projectId: selectedProjectId + }, + tx + ); + + const secretBatches = chunkArray(secrets, 2500); + for await (const secretBatch of secretBatches) { + const secretsByKeys = await secretDAL.findBySecretKeys( + selectedFolder.id, + secretBatch.map((el) => ({ + key: el.secretKey, + type: SecretType.Shared + })), + tx + ); + if (secretsByKeys.length) { + throw new BadRequestError({ + message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}` + }); + } + await fnSecretBulkInsert({ + inputSecrets: secretBatch.map((el) => { + const references = getAllSecretReferences(el.secretValue).nestedReferences; + + return { + version: 1, + encryptedValue: el.secretValue + ? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob + : undefined, + key: el.secretKey, + references, + type: SecretType.Shared + }; + }), + folderId: selectedFolder.id, + orgId: actorOrgId, + resourceMetadataDAL, + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + folderCommitService, + actor: { + type: actor, + actorId + }, + tx + }); + } + } + } + }); + + return { projectsNotImported }; +}; diff --git a/backend/src/services/external-migration/external-migration-fns/index.ts b/backend/src/services/external-migration/external-migration-fns/index.ts new file mode 100644 index 000000000..4af82bf22 --- /dev/null +++ b/backend/src/services/external-migration/external-migration-fns/index.ts @@ -0,0 +1,3 @@ +export * from "./envkey"; +export * from "./import"; +export * from "./vault"; diff --git a/backend/src/services/external-migration/external-migration-fns/vault.ts b/backend/src/services/external-migration/external-migration-fns/vault.ts new file mode 100644 index 000000000..197102f82 --- /dev/null +++ b/backend/src/services/external-migration/external-migration-fns/vault.ts @@ -0,0 +1,341 @@ +import axios, { AxiosInstance } from "axios"; +import { v4 as uuidv4 } from "uuid"; + +import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +import { InfisicalImportData, VaultMappingType } from "../external-migration-types"; + +type VaultData = { + namespace: string; + mount: string; + path: string; + secretData: Record; +}; + +const vaultFactory = () => { + const getMounts = async (request: AxiosInstance) => { + const response = await request + .get< + Record< + string, + { + accessor: string; + options: { + version?: string; + } | null; + type: string; + } + > + >("/v1/sys/mounts") + .catch((err) => { + if (axios.isAxiosError(err)) { + logger.error(err.response?.data, "External migration: Failed to get Vault mounts"); + } + throw err; + }); + return response.data; + }; + + const getPaths = async ( + request: AxiosInstance, + { mountPath, secretPath = "" }: { mountPath: string; secretPath?: string } + ) => { + try { + // For KV v2: /v1/{mount}/metadata/{path}?list=true + const path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`; + const response = await request.get<{ + data: { + keys: string[]; + }; + }>(`/v1/${path}?list=true`); + + return response.data.data.keys; + } catch (err) { + if (axios.isAxiosError(err)) { + logger.error(err.response?.data, "External migration: Failed to get Vault paths"); + if (err.response?.status === 404) { + return null; + } + } + throw err; + } + }; + + const getSecrets = async ( + request: AxiosInstance, + { mountPath, secretPath }: { mountPath: string; secretPath: string } + ) => { + // For KV v2: /v1/{mount}/data/{path} + const response = await request + .get<{ + data: { + data: Record; // KV v2 has nested data structure + metadata: { + created_time: string; + deletion_time: string; + destroyed: boolean; + version: number; + }; + }; + }>(`/v1/${mountPath}/data/${secretPath}`) + .catch((err) => { + if (axios.isAxiosError(err)) { + logger.error(err.response?.data, "External migration: Failed to get Vault secret"); + } + throw err; + }); + + return response.data.data.data; + }; + + // helper function to check if a mount is KV v2 (will be useful if we add support for Vault KV v1) + // const isKvV2Mount = (mountInfo: { type: string; options?: { version?: string } | null }) => { + // return mountInfo.type === "kv" && mountInfo.options?.version === "2"; + // }; + + const recursivelyGetAllPaths = async ( + request: AxiosInstance, + mountPath: string, + currentPath: string = "" + ): Promise => { + const paths = await getPaths(request, { mountPath, secretPath: currentPath }); + + if (paths === null || paths.length === 0) { + return []; + } + + const allSecrets: string[] = []; + + for await (const path of paths) { + const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path; + const fullItemPath = currentPath ? `${currentPath}/${cleanPath}` : cleanPath; + + if (path.endsWith("/")) { + // it's a folder so we recurse into it + const subSecrets = await recursivelyGetAllPaths(request, mountPath, fullItemPath); + allSecrets.push(...subSecrets); + } else { + // it's a secret so we add it to our results + allSecrets.push(`${mountPath}/${fullItemPath}`); + } + } + + return allSecrets; + }; + + async function collectVaultData({ + baseUrl, + namespace, + accessToken + }: { + baseUrl: string; + namespace?: string; + accessToken: string; + }): Promise { + const request = axios.create({ + baseURL: baseUrl, + headers: { + "X-Vault-Token": accessToken, + ...(namespace ? { "X-Vault-Namespace": namespace } : {}) + } + }); + + const allData: VaultData[] = []; + + // Get all mounts in this namespace + const mounts = await getMounts(request); + + for (const mount of Object.keys(mounts)) { + if (!mount.endsWith("/")) { + delete mounts[mount]; + } + } + + for await (const [mountPath, mountInfo] of Object.entries(mounts)) { + // skip non-KV mounts + if (!mountInfo.type.startsWith("kv")) { + // eslint-disable-next-line no-continue + continue; + } + + // get all paths in this mount + const paths = await recursivelyGetAllPaths(request, `${mountPath.replace(/\/$/, "")}`); + + const cleanMountPath = mountPath.replace(/\/$/, ""); + + for await (const secretPath of paths) { + // get the actual secret data + const secretData = await getSecrets(request, { + mountPath: cleanMountPath, + secretPath: secretPath.replace(`${cleanMountPath}/`, "") + }); + + allData.push({ + namespace: namespace || "", + mount: mountPath.replace(/\/$/, ""), + path: secretPath.replace(`${cleanMountPath}/`, ""), + secretData + }); + } + } + + return allData; + } + + return { + collectVaultData, + getMounts, + getPaths, + getSecrets, + recursivelyGetAllPaths + }; +}; + +export const transformToInfisicalFormatNamespaceToProjects = ( + vaultData: VaultData[], + mappingType: VaultMappingType +): InfisicalImportData => { + const projects: Array<{ name: string; id: string }> = []; + const environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }> = []; + const folders: Array<{ id: string; name: string; environmentId: string; parentFolderId?: string }> = []; + const secrets: Array<{ id: string; name: string; environmentId: string; value: string; folderId?: string }> = []; + + // track created entities to avoid duplicates + const projectMap = new Map(); // namespace -> projectId + const environmentMap = new Map(); // namespace:mount -> environmentId + const folderMap = new Map(); // namespace:mount:folderPath -> folderId + + let environmentId: string = ""; + for (const data of vaultData) { + const { namespace, mount, path, secretData } = data; + + if (mappingType === VaultMappingType.Namespace) { + // create project (namespace) + if (!projectMap.has(namespace)) { + const projectId = uuidv4(); + projectMap.set(namespace, projectId); + projects.push({ + name: namespace, + id: projectId + }); + } + const projectId = projectMap.get(namespace)!; + + // create environment (mount) + const envKey = `${namespace}:${mount}`; + if (!environmentMap.has(envKey)) { + environmentId = uuidv4(); + environmentMap.set(envKey, environmentId); + environments.push({ + name: mount, + id: environmentId, + projectId + }); + } + environmentId = environmentMap.get(envKey)!; + } else if (mappingType === VaultMappingType.KeyVault) { + if (!projectMap.has(mount)) { + const projectId = uuidv4(); + projectMap.set(mount, projectId); + projects.push({ + name: mount, + id: projectId + }); + } + const projectId = projectMap.get(mount)!; + + // create single "Production" environment per project, because we have no good way of determining environments from vault + if (!environmentMap.has(mount)) { + environmentId = uuidv4(); + environmentMap.set(mount, environmentId); + environments.push({ + name: "Production", + id: environmentId, + projectId + }); + } + environmentId = environmentMap.get(mount)!; + } + + // create folder structure + let currentFolderId: string | undefined; + let currentPath = ""; + + if (path.includes("/")) { + const pathParts = path.split("/").filter(Boolean); + + const folderParts = pathParts; + + // create nested folder structure for the entire path + for (const folderName of folderParts) { + currentPath = currentPath ? `${currentPath}/${folderName}` : folderName; + const folderKey = `${namespace}:${mount}:${currentPath}`; + + if (!folderMap.has(folderKey)) { + const folderId = uuidv4(); + folderMap.set(folderKey, folderId); + folders.push({ + id: folderId, + name: folderName, + environmentId, + parentFolderId: currentFolderId || environmentId + }); + currentFolderId = folderId; + } else { + currentFolderId = folderMap.get(folderKey)!; + } + } + } + + for (const [key, value] of Object.entries(secretData)) { + secrets.push({ + id: uuidv4(), + name: key, + environmentId, + value: String(value), + folderId: currentFolderId + }); + } + } + + return { + projects, + environments, + folders, + secrets + }; +}; + +export const importVaultDataFn = async ({ + vaultAccessToken, + vaultNamespace, + vaultUrl, + mappingType +}: { + vaultAccessToken: string; + vaultNamespace?: string; + vaultUrl: string; + mappingType: VaultMappingType; +}) => { + await blockLocalAndPrivateIpAddresses(vaultUrl); + + if (mappingType === VaultMappingType.Namespace && !vaultNamespace) { + throw new BadRequestError({ + message: "Vault namespace is required when project mapping type is set to namespace." + }); + } + + const vaultApi = vaultFactory(); + + const vaultData = await vaultApi.collectVaultData({ + accessToken: vaultAccessToken, + baseUrl: vaultUrl, + namespace: vaultNamespace + }); + + const infisicalData = transformToInfisicalFormatNamespaceToProjects(vaultData, mappingType); + + return infisicalData; +}; diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index c4e6b43c5..8bde91aa5 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -19,7 +19,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { importDataIntoInfisicalFn } from "./external-migration-fns"; -import { ExternalPlatforms, TImportInfisicalDataCreate } from "./external-migration-types"; +import { ExternalPlatforms, ImportType, TImportInfisicalDataCreate } from "./external-migration-types"; export type TExternalMigrationQueueFactoryDep = { smtpService: TSmtpService; @@ -67,6 +67,7 @@ export const externalMigrationQueueFactory = ({ const startImport = async (dto: { actorEmail: string; data: { + importType: ImportType; iv: string; tag: string; ciphertext: string; diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index c310fd273..766b6cef4 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -4,9 +4,9 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { TUserDALFactory } from "../user/user-dal"; -import { decryptEnvKeyDataFn, parseEnvKeyDataFn } from "./external-migration-fns"; +import { decryptEnvKeyDataFn, importVaultDataFn, parseEnvKeyDataFn } from "./external-migration-fns"; import { TExternalMigrationQueueFactory } from "./external-migration-queue"; -import { TImportEnvKeyDataCreate } from "./external-migration-types"; +import { ImportType, TImportEnvKeyDataDTO, TImportVaultDataDTO } from "./external-migration-types"; type TExternalMigrationServiceFactoryDep = { permissionService: TPermissionServiceFactory; @@ -28,7 +28,7 @@ export const externalMigrationServiceFactory = ({ actorId, actorOrgId, actorAuthMethod - }: TImportEnvKeyDataCreate) => { + }: TImportEnvKeyDataDTO) => { if (crypto.isFipsModeEnabled()) { throw new BadRequestError({ message: "EnvKey migration is not supported when running in FIPS mode." }); } @@ -60,11 +60,65 @@ export const externalMigrationServiceFactory = ({ await externalMigrationQueue.startImport({ actorEmail: user.email!, - data: encrypted + data: { + importType: ImportType.EnvKey, + ...encrypted + } + }); + }; + + const importVaultData = async ({ + vaultAccessToken, + vaultNamespace, + mappingType, + vaultUrl, + actor, + actorId, + actorOrgId, + actorAuthMethod + }: TImportVaultDataDTO) => { + const { membership } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + if (membership.role !== OrgMembershipRole.Admin) { + throw new ForbiddenRequestError({ message: "Only admins can import data" }); + } + + const user = await userDAL.findById(actorId); + + const vaultData = await importVaultDataFn({ + vaultAccessToken, + vaultNamespace, + vaultUrl, + mappingType + }); + + const stringifiedJson = JSON.stringify({ + data: vaultData, + actor, + actorId, + actorOrgId, + actorAuthMethod + }); + + const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson); + + await externalMigrationQueue.startImport({ + actorEmail: user.email!, + data: { + importType: ImportType.Vault, + ...encrypted + } }); }; return { - importEnvKeyData + importEnvKeyData, + importVaultData }; }; diff --git a/backend/src/services/external-migration/external-migration-types.ts b/backend/src/services/external-migration/external-migration-types.ts index 32c70a688..89589d674 100644 --- a/backend/src/services/external-migration/external-migration-types.ts +++ b/backend/src/services/external-migration/external-migration-types.ts @@ -1,5 +1,17 @@ +import { TOrgPermission } from "@app/lib/types"; + import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +export enum ImportType { + EnvKey = "envkey", + Vault = "vault" +} + +export enum VaultMappingType { + Namespace = "namespace", + KeyVault = "key-vault" +} + export type InfisicalImportData = { projects: Array<{ name: string; id: string }>; environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }>; @@ -14,14 +26,17 @@ export type InfisicalImportData = { }>; }; -export type TImportEnvKeyDataCreate = { +export type TImportEnvKeyDataDTO = { decryptionKey: string; encryptedJson: { nonce: string; data: string }; - actor: ActorType; - actorId: string; - actorOrgId: string; - actorAuthMethod: ActorAuthMethod; -}; +} & Omit; + +export type TImportVaultDataDTO = { + vaultAccessToken: string; + vaultNamespace?: string; + mappingType: VaultMappingType; + vaultUrl: string; +} & Omit; export type TImportInfisicalDataCreate = { data: InfisicalImportData; diff --git a/backend/src/services/identity-access-token/identity-access-token-types.ts b/backend/src/services/identity-access-token/identity-access-token-types.ts index 87adfa5dc..e7b73a8c1 100644 --- a/backend/src/services/identity-access-token/identity-access-token-types.ts +++ b/backend/src/services/identity-access-token/identity-access-token-types.ts @@ -15,5 +15,16 @@ export type TIdentityAccessTokenJwtPayload = { namespace: string; name: string; }; + aws?: { + accountId: string; + arn: string; + userId: string; + + // Derived from ARN + partition: string; // "aws", "aws-gov", "aws-cn" + service: string; // "iam", "sts" + resourceType: string; // "user" or "role" + resourceName: string; + }; }; }; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts index 517e9f613..d0fb4d323 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts @@ -1,67 +1,91 @@ +interface PrincipalArnEntity { + Partition: string; + Service: "iam" | "sts"; + AccountNumber: string; + Type: "user" | "role" | "instance-profile"; + Path: string; + FriendlyName: string; + SessionInfo: string; // Only populated for assumed-role +} + +export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => { + // split the ARN into parts using ":" as the delimiter + const fullParts = arn.split(":"); + if (fullParts.length !== 6) { + throw new Error(`Unrecognized ARN: "${arn}" contains ${fullParts.length} colon-separated parts, expected 6`); + } + const [prefix, partition, service, , accountNumber, resource] = fullParts; + if (prefix !== "arn") { + throw new Error(`Unrecognized ARN: "${arn}" does not begin with "arn:"`); + } + + // validate the service is either 'iam' or 'sts' + if (service !== "iam" && service !== "sts") { + throw new Error(`Unrecognized service: "${service}" in ARN "${arn}", expected "iam" or "sts"`); + } + + // parse the last part of the ARN which describes the resource + const parts = resource.split("/"); + if (parts.length < 2) { + throw new Error( + `Unrecognized ARN: "${resource}" in ARN "${arn}" contains fewer than 2 slash-separated parts (expected type/name)` + ); + } + + const [rawType, ...rest] = parts; + + let finalType: PrincipalArnEntity["Type"]; + let friendlyName: string = parts[parts.length - 1]; + let path: string = ""; + let sessionInfo: string = ""; + + // handle different types of resources + switch (rawType) { + case "assumed-role": { + if (rest.length < 2) { + throw new Error( + `Unrecognized ARN: "${resource}" for assumed-role in ARN "${arn}" contains fewer than 3 slash-separated parts (type/roleName/sessionId)` + ); + } + // assumed roles use a special format where the friendly name is the role name + const [roleName, sessionId] = rest; + finalType = "role"; // treat assumed role case as role + friendlyName = roleName; + sessionInfo = sessionId; + break; + } + case "user": + case "role": + case "instance-profile": + finalType = rawType; + path = rest.slice(0, -1).join("/"); + break; + default: + throw new Error( + `Unrecognized principal type: "${rawType}" in ARN "${arn}". Expected "user", "role", "instance-profile", or "assumed-role".` + ); + } + + const entity: PrincipalArnEntity = { + Partition: partition, + Service: service, + AccountNumber: accountNumber, + Type: finalType, + Path: path, + FriendlyName: friendlyName, + SessionInfo: sessionInfo + }; + + return entity; +}; + /** * Extracts the identity ARN from the GetCallerIdentity response to one of the following formats: * - arn:aws:iam::123456789012:user/MyUserName * - arn:aws:iam::123456789012:role/MyRoleName */ export const extractPrincipalArn = (arn: string) => { - // split the ARN into parts using ":" as the delimiter - const fullParts = arn.split(":"); - if (fullParts.length !== 6) { - throw new Error(`Unrecognized ARN: contains ${fullParts.length} colon-separated parts, expected 6`); - } - const [prefix, partition, service, , accountNumber, resource] = fullParts; - if (prefix !== "arn") { - throw new Error('Unrecognized ARN: does not begin with "arn:"'); - } - - // structure to hold the parsed data - const entity = { - Partition: partition, - Service: service, - AccountNumber: accountNumber, - Type: "", - Path: "", - FriendlyName: "", - SessionInfo: "" - }; - - // validate the service is either 'iam' or 'sts' - if (entity.Service !== "iam" && entity.Service !== "sts") { - throw new Error(`Unrecognized service: ${entity.Service}, not one of iam or sts`); - } - - // parse the last part of the ARN which describes the resource - const parts = resource.split("/"); - if (parts.length < 2) { - throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 2 slash-separated parts`); - } - - const [type, ...rest] = parts; - entity.Type = type; - entity.FriendlyName = parts[parts.length - 1]; - - // handle different types of resources - switch (entity.Type) { - case "assumed-role": { - if (rest.length < 2) { - throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 3 slash-separated parts`); - } - // assumed roles use a special format where the friendly name is the role name - const [roleName, sessionId] = rest; - entity.Type = "role"; // treat assumed role case as role - entity.FriendlyName = roleName; - entity.SessionInfo = sessionId; - break; - } - case "user": - case "role": - case "instance-profile": - // standard cases: just join back the path if there's any - entity.Path = rest.slice(0, -1).join("/"); - break; - default: - throw new Error(`Unrecognized principal type: "${entity.Type}"`); - } + const entity = extractPrincipalArnEntity(arn); return `arn:aws:iam::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`; }; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index 7c339f15e..b5035946e 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -22,7 +22,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal"; -import { extractPrincipalArn } from "./identity-aws-auth-fns"; +import { extractPrincipalArn, extractPrincipalArnEntity } from "./identity-aws-auth-fns"; import { TAttachAwsAuthDTO, TAwsGetCallerIdentityHeaders, @@ -107,7 +107,7 @@ export const identityAwsAuthServiceFactory = ({ const { data: { GetCallerIdentityResponse: { - GetCallerIdentityResult: { Account, Arn } + GetCallerIdentityResult: { Account, Arn, UserId } } } }: { data: TGetCallerIdentityResponse } = await axios({ @@ -168,11 +168,25 @@ export const identityAwsAuthServiceFactory = ({ }); const appCfg = getConfig(); + const splitArn = extractPrincipalArnEntity(Arn); const accessToken = crypto.jwt().sign( { identityId: identityAwsAuth.identityId, identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN, + identityAuth: { + aws: { + accountId: Account, + arn: Arn, + userId: UserId, + + // Derived from ARN + partition: splitArn.Partition, + service: splitArn.Service, + resourceType: splitArn.Type, + resourceName: splitArn.FriendlyName + } + } } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error diff --git a/backend/src/services/reminder/reminder-queue.ts b/backend/src/services/reminder/reminder-queue.ts index 4e31c8a6d..1487a63f3 100644 --- a/backend/src/services/reminder/reminder-queue.ts +++ b/backend/src/services/reminder/reminder-queue.ts @@ -11,7 +11,7 @@ import { TReminderServiceFactory } from "./reminder-types"; type TDailyReminderQueueServiceFactoryDep = { reminderService: TReminderServiceFactory; queueService: TQueueServiceFactory; - secretDAL: Pick; + secretDAL: Pick; secretReminderRecipientsDAL: Pick; }; @@ -69,7 +69,7 @@ export const dailyReminderQueueServiceFactory = ({ // Find existing secrets with pagination // eslint-disable-next-line no-await-in-loop - const secrets = await secretDAL.findSecretsWithReminderRecipients(batchIds, REMINDER_PRUNE_BATCH_SIZE); + const secrets = await secretDAL.findSecretsWithReminderRecipientsOld(batchIds, REMINDER_PRUNE_BATCH_SIZE); const secretsWithReminder = secrets.filter((secret) => secret.reminderRepeatDays); const foundSecretIds = new Set(secretsWithReminder.map((secret) => secret.id)); diff --git a/backend/src/services/reminder/reminder-service.ts b/backend/src/services/reminder/reminder-service.ts index ddccbbf62..a03e9cddd 100644 --- a/backend/src/services/reminder/reminder-service.ts +++ b/backend/src/services/reminder/reminder-service.ts @@ -308,12 +308,11 @@ export const reminderServiceFactory = ({ ); const newReminders = await reminderDAL.insertMany( - processedReminders.map(({ secretId, message, repeatDays, nextReminderDate, projectId }) => ({ + processedReminders.map(({ secretId, message, repeatDays, nextReminderDate }) => ({ secretId, message, repeatDays, - nextReminderDate, - projectId + nextReminderDate })), tx ); diff --git a/backend/src/services/secret-sync/render/render-sync-fns.ts b/backend/src/services/secret-sync/render/render-sync-fns.ts index 9140136a0..36e97e620 100644 --- a/backend/src/services/secret-sync/render/render-sync-fns.ts +++ b/backend/src/services/secret-sync/render/render-sync-fns.ts @@ -8,7 +8,26 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types"; -const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials) => { +const MAX_RETRIES = 5; + +const retrySleep = async () => + new Promise((resolve) => { + setTimeout(resolve, 60000); + }); + +const makeRequestWithRetry = async (requestFn: () => Promise, attempt = 0): Promise => { + try { + return await requestFn(); + } catch (error) { + if (isAxiosError(error) && error.response?.status === 429 && attempt < MAX_RETRIES) { + await retrySleep(); + return await makeRequestWithRetry(requestFn, attempt + 1); + } + throw error; + } +}; + +const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise => { const { destinationConfig, connection: { @@ -22,20 +41,23 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential do { const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl; - const { data } = await request.get< - { - envVar: { - key: string; - value: string; - }; - cursor: string; - }[] - >(url, { - headers: { - Authorization: `Bearer ${apiKey}`, - Accept: "application/json" - } - }); + + const { data } = await makeRequestWithRetry(() => + request.get< + { + envVar: { + key: string; + value: string; + }; + cursor: string; + }[] + >(url, { + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json" + } + }) + ); const secrets = data.map((item) => ({ key: item.envVar.key, @@ -44,13 +66,20 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential allSecrets.push(...secrets); - cursor = data[data.length - 1]?.cursor; + if (data.length > 0 && data[data.length - 1]?.cursor) { + cursor = data[data.length - 1].cursor; + } else { + cursor = undefined; + } } while (cursor); return allSecrets; }; -const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap, key: string) => { +const batchUpdateEnvironmentSecrets = async ( + secretSync: TRenderSyncWithCredentials, + envVars: Array<{ key: string; value: string }> +): Promise => { const { destinationConfig, connection: { @@ -58,93 +87,69 @@ const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secr } } = secretSync; - await request.put( - `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${key}`, - { - key, - value: secretMap[key].value - }, - { + await makeRequestWithRetry(() => + request.put(`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`, envVars, { headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" } - } + }) ); }; -const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: Pick) => { - const { - destinationConfig, - connection: { - credentials: { apiKey } - } - } = secretSync; - - try { - await request.delete( - `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`, - { - headers: { - Authorization: `Bearer ${apiKey}`, - Accept: "application/json" - } - } - ); - } catch (error) { - if (isAxiosError(error) && error.response?.status === 404) { - // If the secret does not exist, we can ignore this error - return; - } - - throw error; - } -}; - -const sleep = async () => - new Promise((resolve) => { - setTimeout(resolve, 500); - }); - export const RenderSyncFns = { syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { const renderSecrets = await getRenderEnvironmentSecrets(secretSync); - for await (const key of Object.keys(secretMap)) { - // If value is empty skip it as render does not allow empty variables - if (secretMap[key].value === "") { + + const finalEnvVars: Array<{ key: string; value: string }> = []; + + for (const renderSecret of renderSecrets) { + const shouldKeep = + secretMap[renderSecret.key] || + (secretSync.syncOptions.disableSecretDeletion && + !matchesSchema(renderSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)); + + if (shouldKeep && !secretMap[renderSecret.key]) { + finalEnvVars.push({ + key: renderSecret.key, + value: renderSecret.value + }); + } + } + + for (const [key, secret] of Object.entries(secretMap)) { + // Skip empty values as render does not allow empty variables + if (secret.value === "") { // eslint-disable-next-line no-continue continue; } - await putEnvironmentSecret(secretSync, secretMap, key); - await sleep(); + + finalEnvVars.push({ + key, + value: secret.value + }); } - if (secretSync.syncOptions.disableSecretDeletion) return; - - for await (const renderSecret of renderSecrets) { - if (!matchesSchema(renderSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) - // eslint-disable-next-line no-continue - continue; - - if (!secretMap[renderSecret.key]) { - await deleteEnvironmentSecret(secretSync, renderSecret); - await sleep(); - } - } + await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars); }, + getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise => { const renderSecrets = await getRenderEnvironmentSecrets(secretSync); return Object.fromEntries(renderSecrets.map((secret) => [secret.key, { value: secret.value ?? "" }])); }, removeSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { - const encryptedSecrets = await getRenderEnvironmentSecrets(secretSync); + const renderSecrets = await getRenderEnvironmentSecrets(secretSync); + const finalEnvVars: Array<{ key: string; value: string }> = []; - for await (const encryptedSecret of encryptedSecrets) { - if (encryptedSecret.key in secretMap) { - await deleteEnvironmentSecret(secretSync, encryptedSecret); - await sleep(); + for (const renderSecret of renderSecrets) { + if (!(renderSecret.key in secretMap)) { + finalEnvVars.push({ + key: renderSecret.key, + value: renderSecret.value + }); } } + await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars); } }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 4cbd1d783..c2a72f2e6 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -875,6 +875,48 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { } }; + const findSecretsWithReminderRecipientsOld = async (ids: string[], limit: number, tx?: Knex) => { + try { + // Create a subquery to get limited secret IDs + const limitedSecretIds = (tx || db)(TableName.SecretV2) + .whereIn(`${TableName.SecretV2}.id`, ids) + .limit(limit) + .select("id"); + + // Join with all recipients for the limited secrets + const docs = await (tx || db)(TableName.SecretV2) + .whereIn(`${TableName.SecretV2}.id`, limitedSecretIds) + .leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`) + .leftJoin( + TableName.SecretReminderRecipients, + `${TableName.SecretV2}.id`, + `${TableName.SecretReminderRecipients}.secretId` + ) + .select(selectAllTableCols(TableName.SecretV2)) + .select(db.ref("userId").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientUserId")); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + _id: el.id, + ...SecretsV2Schema.parse(el) + }), + childrenMapper: [ + { + key: "reminderRecipientUserId", + label: "recipients" as const, + mapper: ({ reminderRecipientUserId }) => reminderRecipientUserId + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: "findSecretsWithReminderRecipientsOld" }); + } + }; + return { ...secretOrm, update, @@ -893,6 +935,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { findOne, find, invalidateSecretCacheByProjectId, - findSecretsWithReminderRecipients + findSecretsWithReminderRecipients, + findSecretsWithReminderRecipientsOld }; }; diff --git a/docs/docs.json b/docs/docs.json index 13ec5367e..ebbc31500 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -198,6 +198,14 @@ "documentation/platform/workflow-integrations/microsoft-teams-integration" ] }, + { + "group": "External Migrations", + "pages": [ + "documentation/platform/external-migrations/overview", + "documentation/platform/external-migrations/envkey", + "documentation/platform/external-migrations/vault" + ] + }, { "group": "Admin Consoles", "pages": [ diff --git a/docs/documentation/guides/migrating-from-envkey.mdx b/docs/documentation/guides/migrating-from-envkey.mdx deleted file mode 100644 index e1d75bab0..000000000 --- a/docs/documentation/guides/migrating-from-envkey.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: "Migrating from EnvKey to Infisical" -sidebarTitle: "Migration" -description: "Learn how to migrate from EnvKey to Infisical in the easiest way possible." ---- - -## What is Infisical? - -[Infisical](https://infisical.com) is an open-source all-in-one secret management platform that helps developers manage secrets (e.g., API-keys, DB access tokens, [certificates](https://infisical.com/docs/documentation/platform/pki/overview)) across their infrastructure. In addition, Infisical provides [secret sharing](https://infisical.com/docs/documentation/platform/secret-sharing) functionality, ability to [prevent secret leaks](https://infisical.com/docs/cli/scanning-overview), and more. - -Infisical is used by 10,000+ organizations across all industries including First American Financial Corporation, Delivery Hero, and [Hugging Face](https://infisical.com/customers/hugging-face). - -## Migrating from EnvKey - - - -Open the EnvKey dashboard and go to My Org. -![EnvKey Dashboard](../../images/guides/import-envkey/envkey-dashboard.png) - - -Go to Import/Export on the top right corner, Click on Export Org and save the exported file. -![Export organization](../../images/guides/import-envkey/envkey-export.png) - - -Click on copy to copy the encryption key and save it. -![Copy encryption key](../../images/guides/import-envkey/copy-encryption-key.png) - - -Open the Infisical dashboard and go to Organization Settings > Import. -![Infisical Organization settings](../../images/guides/import-envkey/infisical-import-dashboard.png) - - -Upload the exported file from EnvKey, paste the encryption key and click Import. -![Infisical Import EnvKey](../../images/guides/import-envkey/infisical-import-envkey.png) - - - - -## Talk to our team - -To make the migration process even more seamless, you can [schedule a meeting with our team](https://infisical.cal.com/vlad/migration-from-envkey-to-infisical) to learn more about how Infisical compares to EnvKey and discuss unique needs of your organization. You are also welcome to email us at [support@infisical.com](mailto:support@infisical.com) to ask any questions or get any technical help. diff --git a/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx b/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx index b953a80bf..2f1aec551 100644 --- a/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx +++ b/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx @@ -30,10 +30,10 @@ For methods like OIDC, these come as claims in the token and can be made availab - 1. Navigate to the Identity Authentication settings and select the OIDC Auth Method. - 2. In the **Advanced section**, locate the Claim Mapping configuration. - 3. Map the OIDC claims to permission attributes by specifying: - - **Attribute Name:** The identifier to be used in your policies (e.g., department). + 1. Navigate to the Identity Authentication settings and select the OIDC Auth Method. + 2. In the **Advanced section**, locate the Claim Mapping configuration. + 3. Map the OIDC claims to permission attributes by specifying: + - **Attribute Name:** The identifier to be used in your policies (e.g., department). - **Claim Path:** The dot notation path to the claim in the OIDC token (e.g., user.department). For example, if your OIDC provider returns: @@ -64,7 +64,7 @@ For methods like OIDC, these come as claims in the token and can be made availab - For identities authenticated using Kubernetes, the service account's namespace and name are available in their policy and can be accessed as follows: + For identities authenticated using Kubernetes, the service account's namespace and name are available in their policy and can be accessed as follows: ``` {{ identity.auth.kubernetes.namespace }} @@ -72,9 +72,25 @@ For methods like OIDC, these come as claims in the token and can be made availab ``` + + + For identities authenticated using AWS Auth, several attributes can be accessed. On top of the 3 base attributes, there's 4 derived from the ARN. The example below includes comments showing how each derived attribute looks like based on this ARN: `arn:aws:iam::123456789012:user/example-user` + ``` + {{ identity.auth.aws.accountId }} + {{ identity.auth.aws.arn }} + {{ identity.auth.aws.userId }} + + // Derived from ARN + {{ identity.auth.aws.partition }} // aws + {{ identity.auth.aws.service }} // iam + {{ identity.auth.aws.resourceType }} // user + {{ identity.auth.aws.resourceName }} // example-user + ``` + + - At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible. + At the moment we only support OIDC claims, Kubernetes attributes, and AWS attributes. Payloads on other authentication methods are not yet accessible. diff --git a/docs/documentation/platform/external-migrations/envkey.mdx b/docs/documentation/platform/external-migrations/envkey.mdx new file mode 100644 index 000000000..46051cd49 --- /dev/null +++ b/docs/documentation/platform/external-migrations/envkey.mdx @@ -0,0 +1,44 @@ +--- +title: "Migrating from EnvKey to Infisical" +sidebarTitle: "EnvKey" +description: "Learn how to migrate secrets from EnvKey to Infisical." +--- + +## Migrating from EnvKey + + + + ![EnvKey Dashboard](/images/platform/external-migrations/envkey-dashboard.png) + + + Go to Import/Export on the top right corner, Click on Export Org and save the exported file. + ![Export organization](/images/platform/external-migrations/envkey-export.png) + + + Click on copy to copy the encryption key and save it. + ![Copy encryption key](/images/platform/external-migrations/envkey-copy-encryption-key.png) + + + Open the Infisical dashboard and go to Organization Settings > External Migrations. + ![Infisical Organization settings](/images/platform/external-migrations/infisical-external-migration-dashboard.png) + + + + Select the EnvKey platform and click on Next. + ![Select EnvKey platform](/images/platform/external-migrations/infisical-import-envkey-modal.png) + + + + Upload the exported file from EnvKey, paste the encryption key and click Import data. + ![Infisical Import EnvKey](/images/platform/external-migrations/infisical-import-envkey.png) + + + + + It may take several minutes to complete the migration. You will receive an email when the migration is complete, or if there were any errors during the migration process. + + + +## Talk to our team + +To make the migration process even more seamless, you can [schedule a meeting with our team](https://infisical.cal.com/vlad/migration-from-envkey-to-infisical) to learn more about how Infisical compares to EnvKey and discuss unique needs of your organization. You are also welcome to email us at [support@infisical.com](mailto:support@infisical.com) to ask any questions or get any technical help. diff --git a/docs/documentation/platform/external-migrations/overview.mdx b/docs/documentation/platform/external-migrations/overview.mdx new file mode 100644 index 000000000..cf6c5e5a1 --- /dev/null +++ b/docs/documentation/platform/external-migrations/overview.mdx @@ -0,0 +1,16 @@ +--- +title: "External Migrations" +sidebarTitle: "Overview" +description: "Learn how to migrate secrets from third-party secrets management platforms to Infisical." +--- + +## Overview + +Infisical supports migrating secrets from third-party secrets management platforms to Infisical. This is useful if you're looking to easily switch to Infisical and wish to move over your existing secrets from a different platform. + +## Supported Platforms + +- [EnvKey](./envkey) +- [Vault](./vault) + +We're always looking to add more migration paths for other providers. If we're missing a platform, please open an issue on our [GitHub repository](https://github.com/infisical/infisical/issues). \ No newline at end of file diff --git a/docs/documentation/platform/external-migrations/vault.mdx b/docs/documentation/platform/external-migrations/vault.mdx new file mode 100644 index 000000000..ef139e8e4 --- /dev/null +++ b/docs/documentation/platform/external-migrations/vault.mdx @@ -0,0 +1,127 @@ +--- +title: "Migrating from Vault to Infisical" +sidebarTitle: "Vault" +description: "Learn how to migrate secrets from Vault to Infisical." +--- + +## Migrating from Vault + +Migrating from Vault Self-Hosted or Dedicated Vault is a straight forward process with our inbuilt migration option. In order to migrate from Vault, you'll need to provide Infisical an access token to your Vault instance. + +Currently the Vault migration only supports migrating secrets from the KV v2 secrets engine. If you're using a different secrets engine, please open an issue on our [GitHub repository](https://github.com/infisical/infisical/issues). + + +### Prerequisites + +- A Vault instance with the KV v2 secrets engine enabled. +- An access token to your Vault instance. + + +### Project Mapping + +When migrating from Vault, you'll need to choose how you want to map your Vault resources to Infisical projects. + +There are two options for project mapping: + +- `Namespace`: This will map your selected Vault namespace to a single Infisical project. When you select this option, each KV secret engine within the namespace will be mapped to a single Infisical project. Each KV secret engine will be mapped to a Infisical environment within the project. This means if you have 3 KV secret engines, you'll have 3 environments inside the same project, where the name of the environments correspond to the name of the KV secret engines. +- `Key Vault`: This will map all the KV secret engines within your Vault instance to a Infisical project. Each KV engine will be created as a Infisical project. This means if you have 3 KV secret engines, you'll have 3 Infisical projects. For each of the created projects, a single default environment will be created called `Production`, which will contain all your secrets from the corresponding KV secret engine. + + + + + + + In order to migrate from Vault, you'll need to create a Vault policy that allows Infisical to read the secrets and metadata from the KV v2 secrets engines within your Vault instance. + + + ```python + # Allow listing secret engines/mounts + path "sys/mounts" { + capabilities = ["read", "list"] + } + + # For KV v2 engines - access to both data and metadata + path "*/data/*" { + capabilities = ["read", "list"] + } + + path "*/metadata/*" { + capabilities = ["read", "list"] + } + + # If using Vault Enterprise - allow listing namespaces + path "sys/namespaces" { + capabilities = ["list", "read"] + } + + # Cross-namespace access (Enterprise only) + path "+/*" { + capabilities = ["read", "list"] + } + + path "+/sys/mounts" { + capabilities = ["read", "list"] + } + ``` + + Save this policy with the name `infisical-migration`. + + + + + You can use the Vault CLI to easily generate an access token for the new `infisical-migration` policy that you created in the previous step. + + ```bash + vault token create --policy="infisical-migration" + ``` + + After generating the token, you should see the following output: + + ```t + $ vault token create --policy="infisical-migration" + + Key Value + --- ----- + token + token_accessor p6kJDiBSzYYdabJUIpGCsCBm + token_duration 768h + token_renewable true + token_policies ["default" "infisical-migration"] + identity_policies [] + policies ["default" "infisical-migration"] + ``` + + Copy the `token` field and save it for later, as you'll need this when configuring the migration to Infisical. + + + + Open the Infisical dashboard and go to Organization Settings > External Migrations. + + ![Infisical Organization settings](/images/platform/external-migrations/infisical-external-migration-dashboard.png) + + + + Select the Vault platform and click on Next. + + ![Select Vault platform](/images/platform/external-migrations/infisical-import-vault-modal.png) + + + + Enter the Vault access token that you generated in the previous step and click Import data. + + ![Configure Vault migration](/images/platform/external-migrations/infisical-import-vault.png) + + - `Vault URL`: The URL of your Vault instance. + - `Vault Namespace`: The namespace of your Vault instance. This is optional, and can be left blank if you're not using namespaces for your Vault instance. + - `Vault Access Token`: The access token that you generated in the previous step. + + - `Project Mapping`: Choose how you want to map your Vault resources to Infisical projects. You can review the mapping options in the [Project Mapping](#project-mapping) section. + + Click on Import data to start the migration. + + + + + + It may take several minutes to complete the migration. You will receive an email when the migration is complete, or if there were any errors during the migration process. + \ No newline at end of file diff --git a/docs/images/guides/import-envkey/infisical-import-dashboard.png b/docs/images/guides/import-envkey/infisical-import-dashboard.png deleted file mode 100644 index ab197f126..000000000 Binary files a/docs/images/guides/import-envkey/infisical-import-dashboard.png and /dev/null differ diff --git a/docs/images/guides/import-envkey/infisical-import-envkey.png b/docs/images/guides/import-envkey/infisical-import-envkey.png deleted file mode 100644 index c504c9824..000000000 Binary files a/docs/images/guides/import-envkey/infisical-import-envkey.png and /dev/null differ diff --git a/docs/images/platform/access-controls/abac-policy-aws-format.png b/docs/images/platform/access-controls/abac-policy-aws-format.png new file mode 100644 index 000000000..4b00c0513 Binary files /dev/null and b/docs/images/platform/access-controls/abac-policy-aws-format.png differ diff --git a/docs/images/guides/import-envkey/copy-encryption-key.png b/docs/images/platform/external-migrations/envkey-copy-encryption-key.png similarity index 100% rename from docs/images/guides/import-envkey/copy-encryption-key.png rename to docs/images/platform/external-migrations/envkey-copy-encryption-key.png diff --git a/docs/images/guides/import-envkey/envkey-dashboard.png b/docs/images/platform/external-migrations/envkey-dashboard.png similarity index 100% rename from docs/images/guides/import-envkey/envkey-dashboard.png rename to docs/images/platform/external-migrations/envkey-dashboard.png diff --git a/docs/images/guides/import-envkey/envkey-export.png b/docs/images/platform/external-migrations/envkey-export.png similarity index 100% rename from docs/images/guides/import-envkey/envkey-export.png rename to docs/images/platform/external-migrations/envkey-export.png diff --git a/docs/images/platform/external-migrations/infisical-external-migration-dashboard.png b/docs/images/platform/external-migrations/infisical-external-migration-dashboard.png new file mode 100644 index 000000000..f978719f4 Binary files /dev/null and b/docs/images/platform/external-migrations/infisical-external-migration-dashboard.png differ diff --git a/docs/images/platform/external-migrations/infisical-import-envkey-modal.png b/docs/images/platform/external-migrations/infisical-import-envkey-modal.png new file mode 100644 index 000000000..9abe26fc8 Binary files /dev/null and b/docs/images/platform/external-migrations/infisical-import-envkey-modal.png differ diff --git a/docs/images/platform/external-migrations/infisical-import-envkey.png b/docs/images/platform/external-migrations/infisical-import-envkey.png new file mode 100644 index 000000000..0d73bc5a0 Binary files /dev/null and b/docs/images/platform/external-migrations/infisical-import-envkey.png differ diff --git a/docs/images/platform/external-migrations/infisical-import-vault-modal.png b/docs/images/platform/external-migrations/infisical-import-vault-modal.png new file mode 100644 index 000000000..c5c4a9063 Binary files /dev/null and b/docs/images/platform/external-migrations/infisical-import-vault-modal.png differ diff --git a/docs/images/platform/external-migrations/infisical-import-vault.png b/docs/images/platform/external-migrations/infisical-import-vault.png new file mode 100644 index 000000000..399e0100d Binary files /dev/null and b/docs/images/platform/external-migrations/infisical-import-vault.png differ diff --git a/frontend/src/components/projects/NewProjectModal.tsx b/frontend/src/components/projects/NewProjectModal.tsx index 555b25b7e..fa3ae2c70 100644 --- a/frontend/src/components/projects/NewProjectModal.tsx +++ b/frontend/src/components/projects/NewProjectModal.tsx @@ -271,6 +271,8 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { value={value} onValueChange={onChange} className="w-full" + position="popper" + dropdownContainerClassName="max-w-none" > {projectTemplates.length ? projectTemplates.map((template) => ( @@ -306,6 +308,9 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { onChange(e); }} className="mb-12 w-full bg-mineshaft-600" + position="popper" + dropdownContainerClassName="max-w-none -top-1" + side="top" > Default Infisical KMS diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index f114c0f02..a232c0065 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -20,6 +20,7 @@ type Props = { isMulti?: boolean; iconClassName?: string; dropdownContainerStyle?: React.CSSProperties; + side?: SelectPrimitive.SelectContentProps["side"]; }; export type SelectProps = Omit & Props; @@ -37,6 +38,7 @@ export const Select = forwardRef( containerClassName, iconClassName, dropdownContainerStyle, + side, ...props }, ref @@ -78,6 +80,7 @@ export const Select = forwardRef( { queryClient.invalidateQueries({ queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot }); } }); }; @@ -110,6 +111,7 @@ export const useUpdateIdentity = () => { queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot }); } }); }; @@ -130,6 +132,7 @@ export const useDeleteIdentity = () => { queryClient.invalidateQueries({ queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot }); } }); }; diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 363a1048d..cf7f4be87 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -25,7 +25,9 @@ import { export const identitiesKeys = { getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const, - searchIdentities: (dto: TSearchIdentitiesDTO) => ["identity", "search", dto] as const, + searchIdentitiesRoot: ["identity", "search"] as const, + searchIdentities: (dto: TSearchIdentitiesDTO) => + [...identitiesKeys.searchIdentitiesRoot, dto] as const, getIdentityUniversalAuth: (identityId: string) => [{ identityId }, "identity-universal-auth"] as const, getIdentityUniversalAuthClientSecrets: (identityId: string) => diff --git a/frontend/src/hooks/api/migration/index.ts b/frontend/src/hooks/api/migration/index.ts new file mode 100644 index 000000000..f8dd99d03 --- /dev/null +++ b/frontend/src/hooks/api/migration/index.ts @@ -0,0 +1 @@ +export * from "./mutations"; diff --git a/frontend/src/hooks/api/migration/mutations.tsx b/frontend/src/hooks/api/migration/mutations.tsx index 87ba89eab..529d5c463 100644 --- a/frontend/src/hooks/api/migration/mutations.tsx +++ b/frontend/src/hooks/api/migration/mutations.tsx @@ -15,7 +15,7 @@ export const useImportEnvKey = () => { formData.append("file", file); try { - const response = await apiRequest.post("/api/v3/migrate/env-key/", formData, { + const response = await apiRequest.post("/api/v3/external-migration/env-key/", formData, { headers: { "Content-Type": "multipart/form-data" }, @@ -39,3 +39,26 @@ export const useImportEnvKey = () => { } }); }; + +export const useImportVault = () => { + return useMutation({ + mutationFn: async ({ + vaultAccessToken, + vaultNamespace, + vaultUrl, + mappingType + }: { + vaultAccessToken: string; + vaultNamespace?: string; + vaultUrl: string; + mappingType: string; + }) => { + await apiRequest.post("/api/v3/external-migration/vault/", { + vaultAccessToken, + vaultNamespace, + vaultUrl, + mappingType + }); + } + }); +}; diff --git a/frontend/src/hooks/useResizableColWidth.tsx b/frontend/src/hooks/useResizableColWidth.tsx new file mode 100644 index 000000000..f2ad80625 --- /dev/null +++ b/frontend/src/hooks/useResizableColWidth.tsx @@ -0,0 +1,71 @@ +import { MouseEvent, useCallback, useEffect, useRef, useState } from "react"; + +type Params = { + minWidth: number; + maxWidth: number; + initialWidth: number; +}; + +export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Params) => { + const [colWidth, setColWidth] = useState(initialWidth); + const [isResizing, setIsResizing] = useState(false); + const startX = useRef(0); + const startWidth = useRef(0); + + const handleMouseDown = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsResizing(true); + startX.current = e.clientX; + startWidth.current = colWidth; + }, + [colWidth] + ); + + const handleMouseMove = useCallback( + (e: MouseEvent) => { + if (!isResizing) return; + + const deltaX = e.clientX - startX.current; + const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth.current + deltaX)); + + setColWidth(newWidth); + }, + [isResizing] + ); + + const handleMouseUp = useCallback(() => { + setIsResizing(false); + }, []); + + useEffect(() => { + if (isResizing) { + document.addEventListener( + "mousemove", + // @ts-expect-error native discrepancy + handleMouseMove + ); + document.addEventListener("mouseup", handleMouseUp); + document.body.style.cursor = "ew-resize"; + document.body.style.userSelect = "none"; + } + + return () => { + document.removeEventListener( + "mousemove", + // @ts-expect-error native discrepancy + handleMouseMove + ); + document.removeEventListener("mouseup", handleMouseUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + }, [isResizing, handleMouseMove, handleMouseUp]); + + return { + colWidth, + handleMouseDown, + isResizing + }; +}; diff --git a/frontend/src/layouts/KmsLayout/KmsLayout.tsx b/frontend/src/layouts/KmsLayout/KmsLayout.tsx index dbe1192f7..c34bc3626 100644 --- a/frontend/src/layouts/KmsLayout/KmsLayout.tsx +++ b/frontend/src/layouts/KmsLayout/KmsLayout.tsx @@ -1,4 +1,4 @@ -import { faCog, faCube, faHome, faLock, faUsers } from "@fortawesome/free-solid-svg-icons"; +import { faBook, faCog, faCube, faHome, faLock, faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, Outlet } from "@tanstack/react-router"; import { motion } from "framer-motion"; @@ -84,6 +84,23 @@ export const KmsLayout = () => { )} + + {({ isActive }) => ( + +
+
+ +
+ Audit Logs +
+
+ )} + { )} + + {({ isActive }) => ( + +
+
+ +
+ Audit Logs +
+
+ )} + { )} + + {({ isActive }) => ( + +
+
+ +
+ Audit Logs +
+
+ )} + { )} + + {({ isActive }) => ( + +
+
+ +
+ Audit Logs +
+
+ )} + { )} + + {({ isActive }) => ( + +
+
+ +
+ Audit Logs +
+
+ )} + void; filter: TAuditLogFilterFormData; + project?: Workspace; }; const getActiveFilterCount = (filter: TAuditLogFilterFormData) => { @@ -69,7 +71,7 @@ const getActiveFilterCount = (filter: TAuditLogFilterFormData) => { return filterCount; }; -export const LogsFilter = ({ presets, setFilter, filter }: Props) => { +export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => { const { data: workspaces = [] } = useGetUserWorkspaces(); const { currentOrg } = useOrganization(); @@ -90,7 +92,7 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => { values: filter }); const selectedEventTypes = watch("eventType") as EventType[] | undefined; - const selectedProject = watch("project"); + const selectedProject = project ?? watch("project"); const showSecretsSection = selectedEventTypes?.some( @@ -270,48 +272,50 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => { )} /> - { - resetField("project"); - resetField("environment"); - setValue("secretPath", ""); - setValue("secretKey", ""); - }} - > - ( - - { - if (e === null) { - setValue("secretPath", ""); - setValue("secretKey", ""); - } - resetField("environment"); - onChange(e); - }} - placeholder="All projects" - options={workspacesInOrg.map(({ name, id, type }) => ({ - name, - id, - type - }))} - getOptionValue={(option) => option.id} - getOptionLabel={(option) => option.name} - /> - - )} - /> - + {!project && ( + { + resetField("project"); + resetField("environment"); + setValue("secretPath", ""); + setValue("secretKey", ""); + }} + > + ( + + { + if (e === null) { + setValue("secretPath", ""); + setValue("secretKey", ""); + } + resetField("environment"); + onChange(e); + }} + placeholder="All projects" + options={workspacesInOrg.map(({ name, id, type }) => ({ + name, + id, + type + }))} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + + )} + /> + + )} {showSecretsSection && ( { + ({ presets, refetchInterval, showFilters = true, pageView = false, project }: Props) => { const { subscription } = useSubscription(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); @@ -83,7 +85,12 @@ export const LogsSection = withPermission( /> )} {showFilters && ( - + )} @@ -94,7 +101,7 @@ export const LogsSection = withPermission( secretPath: logFilter.secretPath || undefined, secretKey: logFilter.secretKey || undefined, eventMetadata: logFilter?.eventMetadata, - projectId: logFilter?.project?.id, + projectId: project?.id || logFilter?.project?.id, actorType: presets?.actorType, limit: 15, eventType: logFilter?.eventType, @@ -119,7 +126,7 @@ export const LogsSection = withPermission( return (
-
+
{showFilters && ( { {t("common.head-title", { title: t("settings.members.title") })} - + {!isLoading && !serverDetails?.redisConfigured && (

Announcements

diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/ImportTab.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx similarity index 94% rename from frontend/src/pages/organization/SettingsPage/components/ImportTab/ImportTab.tsx rename to frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx index 596cef9f4..6ca1a5c28 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ImportTab/ImportTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx @@ -9,7 +9,7 @@ import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { SelectImportFromPlatformModal } from "./components/SelectImportFromPlatformModal"; -export const ImportTab = () => { +export const ExternalMigrationsTab = () => { const { membership } = useOrgPermission(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const); @@ -30,7 +30,7 @@ export const ImportTab = () => {
diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/EnvKeyPlatformModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/EnvKeyPlatformModal.tsx similarity index 83% rename from frontend/src/pages/organization/SettingsPage/components/ImportTab/components/EnvKeyPlatformModal.tsx rename to frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/EnvKeyPlatformModal.tsx index 55b360365..2c15cfb8a 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/EnvKeyPlatformModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/EnvKeyPlatformModal.tsx @@ -45,25 +45,21 @@ export const EnvKeyPlatformModal = ({ onClose }: Props) => { return; } - try { - await importEnvKey({ - file: data.file, - decryptionKey: data.encryptionKey - }); - createNotification({ - title: "Import started", - text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.", - type: "info" - }); + await importEnvKey({ + file: data.file, + decryptionKey: data.encryptionKey + }); + createNotification({ + title: "Import started", + text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.", + type: "info" + }); - onClose(); - reset(); + onClose(); + reset(); - if (fileUploadRef.current) { - fileUploadRef.current.value = ""; - } - } catch { - reset(); + if (fileUploadRef.current) { + fileUploadRef.current.value = ""; } }; diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/GenericDropzone.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/GenericDropzone.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ImportTab/components/GenericDropzone.tsx rename to frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/GenericDropzone.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/SelectImportFromPlatformModal.tsx similarity index 70% rename from frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx rename to frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/SelectImportFromPlatformModal.tsx index 06631ffe1..6da5a44c9 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/SelectImportFromPlatformModal.tsx @@ -1,11 +1,12 @@ import { useState } from "react"; -import { faKey } from "@fortawesome/free-solid-svg-icons"; +import { faKey, faVault } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { AnimatePresence, motion } from "framer-motion"; import { Modal, ModalContent } from "@app/components/v2"; import { EnvKeyPlatformModal } from "./EnvKeyPlatformModal"; +import { VaultPlatformModal } from "./VaultPlatformModal"; type Props = { isOpen?: boolean; @@ -22,6 +23,11 @@ const PLATFORM_LIST = [ icon: faKey, platform: "env-key", title: "Env Key" + }, + { + icon: faVault, + platform: "vault", + title: "Vault" } ] as const; @@ -82,18 +88,32 @@ export const SelectImportFromPlatformModal = ({ isOpen, onToggle }: Props) => {
)} - {wizardStep === WizardSteps.PlatformInputs && - selectedPlatform?.platform === "env-key" && ( - - handleFormReset(false)} /> - - )} + {wizardStep === WizardSteps.PlatformInputs && ( + <> + {selectedPlatform?.platform === "env-key" && ( + + handleFormReset(false)} /> + + )} + {selectedPlatform?.platform === "vault" && ( + + handleFormReset(false)} /> + + )} + + )} diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx new file mode 100644 index 000000000..02ab5df1b --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx @@ -0,0 +1,222 @@ +import { Controller, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Tooltip } from "@app/components/v2"; +import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; +import { useImportVault } from "@app/hooks/api/migration/mutations"; + +type Props = { + id?: string; + onClose: () => void; +}; + +enum VaultMappingType { + KeyVault = "key-vault", + Namespace = "namespace" +} + +const MAPPING_TYPE_MENU_ITEMS = [ + { + value: VaultMappingType.KeyVault, + label: "Key Vaults", + tooltip: ( +
+ When using key vaults for mapping, each key vault within Vault will be created in Infisical + as a project. Each secret path inside the key vault, will be created as an environment + inside the corresponding project. When using Key Vaults as the project mapping type, a + default environment called "Production" will be created for each project, which + will contain the secrets from the key vault. +
+
Key Vault → Project
+
Default Environment (Production)
+
Secret Path → Secret Folder
+
Secret data → Secrets
+
+
+ ) + }, + { + value: VaultMappingType.Namespace, + label: "Namespaces", + tooltip: ( +
+ When using namespaces for mapping, each namespace within Vault will be created in Infisical + as a project. Each key vault (KV) inside the namespace, will be created as an environment + inside the corresponding project. +
+
Namespace → Project
+
Key Vault → Project Environment
+
Secret Path → Secret Folder
+
Secret data → Secrets
+
+
+ ) + } +]; + +export const VaultPlatformModal = ({ onClose }: Props) => { + const formSchema = z.object({ + vaultUrl: z.string().min(1), + vaultNamespace: z.string().trim().optional(), + vaultAccessToken: z.string().min(1), + mappingType: z.nativeEnum(VaultMappingType).default(VaultMappingType.KeyVault) + }); + type TFormData = z.infer; + + const { mutateAsync: importVault } = useImportVault(); + + const { + control, + handleSubmit, + reset, + formState: { isLoading, isDirty, isSubmitting, isValid, errors } + } = useForm({ + resolver: zodResolver(formSchema) + }); + + console.log({ + isSubmitting, + isLoading, + isValid, + errors + }); + + const onSubmit = async (data: TFormData) => { + await importVault({ + vaultAccessToken: data.vaultAccessToken, + vaultNamespace: data.vaultNamespace, + vaultUrl: data.vaultUrl, + mappingType: data.mappingType + }); + createNotification({ + title: "Import started", + text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.", + type: "info" + }); + + onClose(); + reset(); + }; + + return ( +
+ +

+ The Vault migration currently supports importing static secrets from Vault + Dedicated/Self-Hosted. +

+ Currently only KV Secret Engine V2 is supported for Vault migrations. +
+

+
+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + +
+ {MAPPING_TYPE_MENU_ITEMS.map((el) => ( +
field.onChange(el.value)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter") { + field.onChange(el.value); + } + }} + > +
+
{el.label}
+ {el.tooltip && ( +
+ + + +
+ )} +
+
+ ))} +
+
+ )} + /> + +
+ + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/index.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/index.tsx new file mode 100644 index 000000000..fea504547 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/index.tsx @@ -0,0 +1 @@ +export { ExternalMigrationsTab } from "./ExternalMigrationsTab"; diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/index.tsx b/frontend/src/pages/organization/SettingsPage/components/ImportTab/index.tsx deleted file mode 100644 index ec2b6ead6..000000000 --- a/frontend/src/pages/organization/SettingsPage/components/ImportTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ImportTab } from "./ImportTab"; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx index a71ee7692..42a8e5839 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx @@ -5,7 +5,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { AuditLogStreamsTab } from "../AuditLogStreamTab"; -import { ImportTab } from "../ImportTab"; +import { ExternalMigrationsTab } from "../ExternalMigrationsTab"; import { KmipTab } from "../KmipTab/OrgKmipTab"; import { OrgEncryptionTab } from "../OrgEncryptionTab"; import { OrgGeneralTab } from "../OrgGeneralTab"; @@ -39,7 +39,11 @@ export const OrgTabGroup = () => { component: OrgWorkflowIntegrationTab }, { name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab }, - { name: "Import", key: "tab-import", component: ImportTab }, + { + name: "External Migrations", + key: "tab-external-migrations", + component: ExternalMigrationsTab + }, { name: "Project Templates", key: "project-templates", diff --git a/frontend/src/pages/project/AuditLogsPage/AuditLogsPage.tsx b/frontend/src/pages/project/AuditLogsPage/AuditLogsPage.tsx new file mode 100644 index 000000000..0c83b0520 --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/AuditLogsPage.tsx @@ -0,0 +1,27 @@ +import { Helmet } from "react-helmet"; + +import { PageHeader } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { LogsSection } from "@app/pages/organization/AuditLogsPage/components"; + +export const AuditLogsPage = () => { + const { currentWorkspace } = useWorkspace(); + + return ( +
+ + Project Audit Logs + + +
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/pages/project/AuditLogsPage/route-cert-manager.tsx b/frontend/src/pages/project/AuditLogsPage/route-cert-manager.tsx new file mode 100644 index 000000000..e87c655e0 --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/route-cert-manager.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AuditLogsPage } from "./AuditLogsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs" +)({ + component: AuditLogsPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Audit Logs" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AuditLogsPage/route-kms.tsx b/frontend/src/pages/project/AuditLogsPage/route-kms.tsx new file mode 100644 index 000000000..7f6883d80 --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/route-kms.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AuditLogsPage } from "./AuditLogsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs" +)({ + component: AuditLogsPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Audit Logs" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AuditLogsPage/route-secret-manager.tsx b/frontend/src/pages/project/AuditLogsPage/route-secret-manager.tsx new file mode 100644 index 000000000..3483f9270 --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/route-secret-manager.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AuditLogsPage } from "./AuditLogsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs" +)({ + component: AuditLogsPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Audit Logs" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AuditLogsPage/route-secret-scanning.tsx b/frontend/src/pages/project/AuditLogsPage/route-secret-scanning.tsx new file mode 100644 index 000000000..28d00c5af --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/route-secret-scanning.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AuditLogsPage } from "./AuditLogsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs" +)({ + component: AuditLogsPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Audit Logs" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AuditLogsPage/route-ssh.tsx b/frontend/src/pages/project/AuditLogsPage/route-ssh.tsx new file mode 100644 index 000000000..267e613b7 --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/route-ssh.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AuditLogsPage } from "./AuditLogsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs" +)({ + component: AuditLogsPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Audit Logs" + } + ] + }; + } +}); diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 61be72832..5e9832763 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -10,13 +10,12 @@ import { faArrowRight, faArrowRightToBracket, faArrowUp, - faFileImport, + faFilter, faFingerprint, faFolder, faFolderBlank, faFolderPlus, faKey, - faList, faPlus, faRotate } from "@fortawesome/free-solid-svg-icons"; @@ -96,7 +95,12 @@ import { OrderByDirection } from "@app/hooks/api/generic/types"; import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries"; import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types"; import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; -import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types"; +import { + SecretType, + SecretV3RawSanitized, + TSecretFolder, + WorkspaceEnv +} from "@app/hooks/api/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { useDynamicSecretOverview, @@ -135,7 +139,6 @@ enum RowType { Folder = "folder", DynamicSecret = "dynamic", Secret = "secret", - Import = "import", SecretRotation = "rotation" } @@ -144,11 +147,10 @@ type Filter = { }; const DEFAULT_FILTER_STATE = { - [RowType.Folder]: true, - [RowType.DynamicSecret]: true, - [RowType.Secret]: true, - [RowType.Import]: true, - [RowType.SecretRotation]: true + [RowType.Folder]: false, + [RowType.DynamicSecret]: false, + [RowType.Secret]: false, + [RowType.SecretRotation]: false }; const DEFAULT_COLLAPSED_HEADER_HEIGHT = 120; @@ -265,11 +267,8 @@ export const OverviewPage = () => { ) ); - const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs); - - useEffect(() => { - setVisibleEnvs(userAvailableEnvs); - }, [userAvailableEnvs]); + const [filteredEnvs, setFilteredEnvs] = useState([]); + const visibleEnvs = filteredEnvs.length ? filteredEnvs : userAvailableEnvs; const { secretImports, @@ -282,6 +281,7 @@ export const OverviewPage = () => { environments: (userAvailableEnvs || []).map(({ slug }) => slug) }); + const isFilteredByResources = Object.values(filter).some(Boolean); const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview( { projectId: workspaceId, @@ -289,11 +289,11 @@ export const OverviewPage = () => { secretPath, orderDirection, orderBy, - includeFolders: filter.folder, - includeDynamicSecrets: filter.dynamic, - includeSecrets: filter.secret, - includeImports: filter.import, - includeSecretRotations: filter.rotation, + includeFolders: isFilteredByResources ? filter.folder : true, + includeDynamicSecrets: isFilteredByResources ? filter.dynamic : true, + includeSecrets: isFilteredByResources ? filter.secret : true, + includeImports: true, + includeSecretRotations: isFilteredByResources ? filter.rotation : true, search: debouncedSearchFilter, limit, offset @@ -529,10 +529,10 @@ export const OverviewPage = () => { }; const handleEnvSelect = (envId: string) => { - if (visibleEnvs.map((env) => env.id).includes(envId)) { - setVisibleEnvs(visibleEnvs.filter((env) => env.id !== envId)); + if (filteredEnvs.map((env) => env.id).includes(envId)) { + setFilteredEnvs(filteredEnvs.filter((env) => env.id !== envId)); } else { - setVisibleEnvs(visibleEnvs.concat(userAvailableEnvs.filter((env) => env.id === envId))); + setFilteredEnvs(filteredEnvs.concat(userAvailableEnvs.filter((env) => env.id === envId))); } }; @@ -792,11 +792,11 @@ export const OverviewPage = () => { envNames: string[], envs: { environment: string; importedBy: ProjectSecretsImportedBy[] }[] ): ProjectSecretsImportedBy[] => { - const filteredEnvs = envs.filter((env) => envNames.includes(env.environment)); + const environments = envs.filter((env) => envNames.includes(env.environment)); - if (filteredEnvs.length === 0) return []; + if (environments.length === 0) return []; - const allImportedBy = filteredEnvs.flatMap((env) => env.importedBy); + const allImportedBy = environments.flatMap((env) => env.importedBy); const groupedBySlug: Record = {}; allImportedBy.forEach((item) => { @@ -902,9 +902,7 @@ export const OverviewPage = () => { const isTableEmpty = totalCount === 0; - const isTableFiltered = - Boolean(Object.values(filter).filter((enabled) => !enabled).length) || - userAvailableEnvs.length !== visibleEnvs.length; + const isTableFiltered = isFilteredByResources || filteredEnvs.length > 0; if (!isProjectV3) return ( @@ -969,26 +967,42 @@ export const OverviewPage = () => {
+ {isTableFiltered && ( + + )} {userAvailableEnvs.length > 0 && ( - + } > - - - - + Filters + {/* */} - Filter project resources - { - e.preventDefault(); - handleToggleRowType(RowType.Import); - }} - icon={filter[RowType.Import] && } - iconPos="right" - > -
- - Imports -
-
+ Filter by Resource + { e.preventDefault(); @@ -1070,11 +1072,11 @@ export const OverviewPage = () => { Secrets
- Choose visible environments + Filter by Environment {userAvailableEnvs.map((availableEnv) => { const { id: envId, name } = availableEnv; - const isEnvSelected = visibleEnvs.map((env) => env.id).includes(envId); + const isEnvSelected = filteredEnvs.map((env) => env.id).includes(envId); return ( { @@ -1082,7 +1084,6 @@ export const OverviewPage = () => { handleEnvSelect(envId); }} key={envId} - disabled={visibleEnvs?.length === 1} icon={isEnvSelected && } iconPos="right" > @@ -1405,13 +1406,6 @@ export const OverviewPage = () => { className="bg-mineshaft-700" /> )} - {userAvailableEnvs.length > 0 && visibleEnvs.length === 0 && ( - - - - - - )} {userAvailableEnvs.length === 0 && ( @@ -1444,8 +1438,8 @@ export const OverviewPage = () => { { const { permission } = useProjectPermission(); const { mutateAsync: createCommit } = useCreateCommit(); + const tableRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); const { isBatchMode, pendingChanges } = useBatchMode(); const { loadPendingChanges, setExistingKeys } = useBatchModeActions(); @@ -212,11 +215,11 @@ const Page = () => { searchFilter: (routerQueryParams.search as string) || "", // these should always be on by default for the UI, they will be disabled for the query below based off permissions include: { - [RowType.Folder]: true, - [RowType.Import]: true, - [RowType.DynamicSecret]: true, - [RowType.Secret]: true, - [RowType.SecretRotation]: true + [RowType.Folder]: false, + [RowType.Import]: false, + [RowType.DynamicSecret]: false, + [RowType.Secret]: false, + [RowType.SecretRotation]: false } }; @@ -242,6 +245,7 @@ const Page = () => { } }, [currentWorkspace, environment]); + const isResourceTypeFiltered = Object.values(filter.include).some(Boolean); const { data, isPending: isDetailsLoading, @@ -255,12 +259,14 @@ const Page = () => { orderBy, search: debouncedSearchFilter, orderDirection, - includeImports: canReadSecretImports && filter.include.import, - includeFolders: filter.include.folder, + includeImports: canReadSecretImports && (isResourceTypeFiltered ? filter.include.import : true), + includeFolders: isResourceTypeFiltered ? filter.include.folder : true, viewSecretValue: canReadSecretValue, - includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, - includeSecrets: canReadSecret && filter.include.secret, - includeSecretRotations: canReadSecretRotations && filter.include.rotation, + includeDynamicSecrets: + canReadDynamicSecret && (isResourceTypeFiltered ? filter.include.dynamic : true), + includeSecrets: canReadSecret && (isResourceTypeFiltered ? filter.include.secret : true), + includeSecretRotations: + canReadSecretRotations && (isResourceTypeFiltered ? filter.include.rotation : true), tags: filter.tags }); @@ -480,6 +486,14 @@ const Page = () => { handlePopUpClose("snapshots"); }, []); + const { handleMouseDown, isResizing, colWidth } = useResizableColWidth({ + initialWidth: 320, + minWidth: 100, + maxWidth: tableRef.current + ? tableRef.current.clientWidth - 148 // ensure value column can't collapse completely + : 800 + }); + useEffect(() => { // restore filters for path if set const restore = filterHistory.get(secretPath); @@ -769,8 +783,22 @@ const Page = () => { isPITEnabled={isPITEnabled} hasPathPolicies={hasPathPolicies} onRequestAccess={(params) => handlePopUpOpen("requestAccess", params)} + onClearFilters={() => + setFilter((prev) => ({ + ...prev, + tags: {}, + include: { + secret: false, + import: false, + dynamic: false, + rotation: false, + folder: false + } + })) + } />
{ />
-
{ - if (evt.key === "Enter") handleSortToggle(); - }} - > - Key - +
+
+
+
+
{ + if (evt.key === "Enter") handleSortToggle(); + }} + > + Key + +
Value
@@ -911,6 +953,7 @@ const Page = () => { )} {canReadSecret && Boolean(mergedSecrets?.length) && ( void; hasPathPolicies: boolean; + onClearFilters: () => void; }; export const ActionBar = ({ @@ -159,7 +160,8 @@ export const ActionBar = ({ isPITEnabled = false, usedBySecretSyncs, onRequestAccess, - hasPathPolicies + hasPathPolicies, + onClearFilters }: Props) => { const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "addFolder", @@ -661,6 +663,9 @@ export const ActionBar = ({ } }; + const isTableFiltered = + Object.values(filter.tags).some(Boolean) || Object.values(filter.include).some(Boolean); + return ( <>
@@ -676,18 +681,22 @@ export const ActionBar = ({
- !include).length) && - "border-primary/50 text-primary" + "flex h-[2.5rem]", + isTableFiltered && "border-primary/40 bg-primary/10" )} + leftIcon={ + + } > - - + Filters + Filter By @@ -762,51 +771,62 @@ export const ActionBar = ({ Secrets
- - } - > - Tags - - - - Apply Tags to Filter Secrets - - {tags.map(({ id, slug, color }) => ( - { - evt.preventDefault(); - onToggleTagFilter(slug); - }} - key={id} - icon={filter?.tags[slug] && } - iconPos="right" - > -
-
- {slug} -
- - ))} - - + {Boolean(tags.length) && ( + + } + > + Tags + + + + Apply Tags to Filter Secrets + + {tags.map(({ id, slug, color }) => ( + { + evt.preventDefault(); + onToggleTagFilter(slug); + }} + key={id} + icon={filter?.tags[slug] && } + iconPos="right" + > +
+
+ {slug} +
+ + ))} + + + )}
+ {isTableFiltered && ( + + )} +
{isProtectedBranch && ( - + )}
-
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx index e0cc3f78b..ff6066c90 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderListView/FolderListView.tsx @@ -235,7 +235,7 @@ export const FolderListView = ({ )}
{isPending ? ( -
+
{ const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "editSecret" @@ -383,7 +385,10 @@ export const SecretItem = memo( )}
-
+
{ const queryClient = useQueryClient(); const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ @@ -554,6 +556,7 @@ export const SecretListView = ({ ))} {secrets.map((secret) => ( sshLayoutRoute, + } as any) + const projectAccessControlPageRouteSshRoute = projectAccessControlPageRouteSshImport.update({ id: '/access-management', @@ -919,6 +931,13 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecret } as any, ) +const projectAuditLogsPageRouteSecretScanningRoute = + projectAuditLogsPageRouteSecretScanningImport.update({ + id: '/audit-logs', + path: '/audit-logs', + getParentRoute: () => secretScanningLayoutRoute, + } as any) + const projectAccessControlPageRouteSecretScanningRoute = projectAccessControlPageRouteSecretScanningImport.update({ id: '/access-management', @@ -935,6 +954,13 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecr } as any, ) +const projectAuditLogsPageRouteSecretManagerRoute = + projectAuditLogsPageRouteSecretManagerImport.update({ + id: '/audit-logs', + path: '/audit-logs', + getParentRoute: () => secretManagerLayoutRoute, + } as any) + const projectAccessControlPageRouteSecretManagerRoute = projectAccessControlPageRouteSecretManagerImport.update({ id: '/access-management', @@ -942,6 +968,13 @@ const projectAccessControlPageRouteSecretManagerRoute = getParentRoute: () => secretManagerLayoutRoute, } as any) +const projectAuditLogsPageRouteKmsRoute = + projectAuditLogsPageRouteKmsImport.update({ + id: '/audit-logs', + path: '/audit-logs', + getParentRoute: () => kmsLayoutRoute, + } as any) + const projectAccessControlPageRouteKmsRoute = projectAccessControlPageRouteKmsImport.update({ id: '/access-management', @@ -967,6 +1000,13 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertMa } as any, ) +const projectAuditLogsPageRouteCertManagerRoute = + projectAuditLogsPageRouteCertManagerImport.update({ + id: '/audit-logs', + path: '/audit-logs', + getParentRoute: () => certManagerLayoutRoute, + } as any) + const projectAccessControlPageRouteCertManagerRoute = projectAccessControlPageRouteCertManagerImport.update({ id: '/access-management', @@ -2722,6 +2762,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs' + path: '/audit-logs' + fullPath: '/projects/cert-management/$projectId/audit-logs' + preLoaderRoute: typeof projectAuditLogsPageRouteCertManagerImport + parentRoute: typeof certManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates': { id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates' path: '/certificate-templates' @@ -2743,6 +2790,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteKmsImport parentRoute: typeof kmsLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs' + path: '/audit-logs' + fullPath: '/projects/kms/$projectId/audit-logs' + preLoaderRoute: typeof projectAuditLogsPageRouteKmsImport + parentRoute: typeof kmsLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': { id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management' path: '/access-management' @@ -2750,6 +2804,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSecretManagerImport parentRoute: typeof secretManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs' + path: '/audit-logs' + fullPath: '/projects/secret-management/$projectId/audit-logs' + preLoaderRoute: typeof projectAuditLogsPageRouteSecretManagerImport + parentRoute: typeof secretManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations': { id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations' path: '/integrations' @@ -2764,6 +2825,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSecretScanningImport parentRoute: typeof secretScanningLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs' + path: '/audit-logs' + fullPath: '/projects/secret-scanning/$projectId/audit-logs' + preLoaderRoute: typeof projectAuditLogsPageRouteSecretScanningImport + parentRoute: typeof secretScanningLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources': { id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources' path: '/data-sources' @@ -2778,6 +2846,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSshImport parentRoute: typeof sshLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs' + path: '/audit-logs' + fullPath: '/projects/ssh/$projectId/audit-logs' + preLoaderRoute: typeof projectAuditLogsPageRouteSshImport + parentRoute: typeof sshLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/': { id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/' path: '/' @@ -3781,6 +3856,7 @@ interface certManagerLayoutRouteChildren { certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute + projectAuditLogsPageRouteCertManagerRoute: typeof projectAuditLogsPageRouteCertManagerRoute AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute @@ -3799,6 +3875,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = { certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute, projectAccessControlPageRouteCertManagerRoute: projectAccessControlPageRouteCertManagerRoute, + projectAuditLogsPageRouteCertManagerRoute: + projectAuditLogsPageRouteCertManagerRoute, AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute: AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute: @@ -3839,6 +3917,7 @@ interface kmsLayoutRouteChildren { kmsOverviewPageRouteRoute: typeof kmsOverviewPageRouteRoute kmsSettingsPageRouteRoute: typeof kmsSettingsPageRouteRoute projectAccessControlPageRouteKmsRoute: typeof projectAccessControlPageRouteKmsRoute + projectAuditLogsPageRouteKmsRoute: typeof projectAuditLogsPageRouteKmsRoute projectGroupDetailsByIDPageRouteKmsRoute: typeof projectGroupDetailsByIDPageRouteKmsRoute projectIdentityDetailsByIDPageRouteKmsRoute: typeof projectIdentityDetailsByIDPageRouteKmsRoute projectMemberDetailsByIDPageRouteKmsRoute: typeof projectMemberDetailsByIDPageRouteKmsRoute @@ -3850,6 +3929,7 @@ const kmsLayoutRouteChildren: kmsLayoutRouteChildren = { kmsOverviewPageRouteRoute: kmsOverviewPageRouteRoute, kmsSettingsPageRouteRoute: kmsSettingsPageRouteRoute, projectAccessControlPageRouteKmsRoute: projectAccessControlPageRouteKmsRoute, + projectAuditLogsPageRouteKmsRoute: projectAuditLogsPageRouteKmsRoute, projectGroupDetailsByIDPageRouteKmsRoute: projectGroupDetailsByIDPageRouteKmsRoute, projectIdentityDetailsByIDPageRouteKmsRoute: @@ -4166,6 +4246,7 @@ interface secretManagerLayoutRouteChildren { secretManagerSecretRotationPageRouteRoute: typeof secretManagerSecretRotationPageRouteRoute secretManagerSettingsPageRouteRoute: typeof secretManagerSettingsPageRouteRoute projectAccessControlPageRouteSecretManagerRoute: typeof projectAccessControlPageRouteSecretManagerRoute + projectAuditLogsPageRouteSecretManagerRoute: typeof projectAuditLogsPageRouteSecretManagerRoute AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren secretManagerSecretDashboardPageRouteRoute: typeof secretManagerSecretDashboardPageRouteRoute projectGroupDetailsByIDPageRouteSecretManagerRoute: typeof projectGroupDetailsByIDPageRouteSecretManagerRoute @@ -4186,6 +4267,8 @@ const secretManagerLayoutRouteChildren: secretManagerLayoutRouteChildren = { secretManagerSettingsPageRouteRoute: secretManagerSettingsPageRouteRoute, projectAccessControlPageRouteSecretManagerRoute: projectAccessControlPageRouteSecretManagerRoute, + projectAuditLogsPageRouteSecretManagerRoute: + projectAuditLogsPageRouteSecretManagerRoute, AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute: AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren, secretManagerSecretDashboardPageRouteRoute: @@ -4241,6 +4324,7 @@ interface secretScanningLayoutRouteChildren { secretScanningSecretScanningFindingsPageRouteRoute: typeof secretScanningSecretScanningFindingsPageRouteRoute secretScanningSettingsPageRouteRoute: typeof secretScanningSettingsPageRouteRoute projectAccessControlPageRouteSecretScanningRoute: typeof projectAccessControlPageRouteSecretScanningRoute + projectAuditLogsPageRouteSecretScanningRoute: typeof projectAuditLogsPageRouteSecretScanningRoute AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren projectGroupDetailsByIDPageRouteSecretScanningRoute: typeof projectGroupDetailsByIDPageRouteSecretScanningRoute projectIdentityDetailsByIDPageRouteSecretScanningRoute: typeof projectIdentityDetailsByIDPageRouteSecretScanningRoute @@ -4254,6 +4338,8 @@ const secretScanningLayoutRouteChildren: secretScanningLayoutRouteChildren = { secretScanningSettingsPageRouteRoute: secretScanningSettingsPageRouteRoute, projectAccessControlPageRouteSecretScanningRoute: projectAccessControlPageRouteSecretScanningRoute, + projectAuditLogsPageRouteSecretScanningRoute: + projectAuditLogsPageRouteSecretScanningRoute, AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren, projectGroupDetailsByIDPageRouteSecretScanningRoute: @@ -4289,6 +4375,7 @@ interface sshLayoutRouteChildren { sshSshHostsPageRouteRoute: typeof sshSshHostsPageRouteRoute sshSettingsPageRouteRoute: typeof sshSettingsPageRouteRoute projectAccessControlPageRouteSshRoute: typeof projectAccessControlPageRouteSshRoute + projectAuditLogsPageRouteSshRoute: typeof projectAuditLogsPageRouteSshRoute sshSshCaByIDPageRouteRoute: typeof sshSshCaByIDPageRouteRoute sshSshHostGroupDetailsByIDPageRouteRoute: typeof sshSshHostGroupDetailsByIDPageRouteRoute projectGroupDetailsByIDPageRouteSshRoute: typeof projectGroupDetailsByIDPageRouteSshRoute @@ -4303,6 +4390,7 @@ const sshLayoutRouteChildren: sshLayoutRouteChildren = { sshSshHostsPageRouteRoute: sshSshHostsPageRouteRoute, sshSettingsPageRouteRoute: sshSettingsPageRouteRoute, projectAccessControlPageRouteSshRoute: projectAccessControlPageRouteSshRoute, + projectAuditLogsPageRouteSshRoute: projectAuditLogsPageRouteSshRoute, sshSshCaByIDPageRouteRoute: sshSshCaByIDPageRouteRoute, sshSshHostGroupDetailsByIDPageRouteRoute: sshSshHostGroupDetailsByIDPageRouteRoute, @@ -4642,14 +4730,19 @@ export interface FileRoutesByFullPath { '/projects/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute '/projects/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute '/projects/cert-management/$projectId/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren '/projects/cert-management/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren '/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute + '/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute '/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute + '/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute '/projects/secret-management/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/projects/secret-scanning/$projectId/access-management': typeof projectAccessControlPageRouteSecretScanningRoute + '/projects/secret-scanning/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute '/projects/secret-scanning/$projectId/data-sources': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren '/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute + '/projects/ssh/$projectId/audit-logs': typeof projectAuditLogsPageRouteSshRoute '/projects/cert-management/$projectId/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute '/projects/cert-management/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/projects/secret-management/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute @@ -4852,10 +4945,15 @@ export interface FileRoutesByTo { '/projects/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute '/projects/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute '/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute + '/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute '/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute + '/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute '/projects/secret-scanning/$projectId/access-management': typeof projectAccessControlPageRouteSecretScanningRoute + '/projects/secret-scanning/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute '/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute + '/projects/ssh/$projectId/audit-logs': typeof projectAuditLogsPageRouteSshRoute '/projects/cert-management/$projectId/certificate-templates': typeof certManagerPkiTemplateListPageRouteRoute '/projects/cert-management/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute '/projects/secret-management/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute @@ -5075,14 +5173,19 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management': typeof projectAccessControlPageRouteKmsRoute + '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs': typeof projectAuditLogsPageRouteKmsRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute + '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management': typeof projectAccessControlPageRouteSecretScanningRoute + '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute + '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs': typeof projectAuditLogsPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute @@ -5295,14 +5398,19 @@ export interface FileRouteTypes { | '/projects/ssh/$projectId/overview' | '/projects/ssh/$projectId/settings' | '/projects/cert-management/$projectId/access-management' + | '/projects/cert-management/$projectId/audit-logs' | '/projects/cert-management/$projectId/certificate-templates' | '/projects/cert-management/$projectId/subscribers' | '/projects/kms/$projectId/access-management' + | '/projects/kms/$projectId/audit-logs' | '/projects/secret-management/$projectId/access-management' + | '/projects/secret-management/$projectId/audit-logs' | '/projects/secret-management/$projectId/integrations' | '/projects/secret-scanning/$projectId/access-management' + | '/projects/secret-scanning/$projectId/audit-logs' | '/projects/secret-scanning/$projectId/data-sources' | '/projects/ssh/$projectId/access-management' + | '/projects/ssh/$projectId/audit-logs' | '/projects/cert-management/$projectId/certificate-templates/' | '/projects/cert-management/$projectId/subscribers/' | '/projects/secret-management/$projectId/integrations/' @@ -5504,10 +5612,15 @@ export interface FileRouteTypes { | '/projects/ssh/$projectId/overview' | '/projects/ssh/$projectId/settings' | '/projects/cert-management/$projectId/access-management' + | '/projects/cert-management/$projectId/audit-logs' | '/projects/kms/$projectId/access-management' + | '/projects/kms/$projectId/audit-logs' | '/projects/secret-management/$projectId/access-management' + | '/projects/secret-management/$projectId/audit-logs' | '/projects/secret-scanning/$projectId/access-management' + | '/projects/secret-scanning/$projectId/audit-logs' | '/projects/ssh/$projectId/access-management' + | '/projects/ssh/$projectId/audit-logs' | '/projects/cert-management/$projectId/certificate-templates' | '/projects/cert-management/$projectId/subscribers' | '/projects/secret-management/$projectId/integrations' @@ -5725,14 +5838,19 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers' | '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources' | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/' @@ -6331,6 +6449,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName", @@ -6349,6 +6468,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview", "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/members/$membershipId", @@ -6365,6 +6485,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secret-rotation", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId", @@ -6381,6 +6502,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId", @@ -6397,6 +6519,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview", "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ca/$caId", "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId", "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/groups/$groupId", @@ -6485,6 +6608,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-cert-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs": { + "filePath": "project/AuditLogsPage/route-cert-manager.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout", @@ -6504,10 +6631,18 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-kms.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs": { + "filePath": "project/AuditLogsPage/route-kms.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management": { "filePath": "project/AccessControlPage/route-secret-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs": { + "filePath": "project/AuditLogsPage/route-secret-manager.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout", @@ -6596,6 +6731,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-secret-scanning.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs": { + "filePath": "project/AuditLogsPage/route-secret-scanning.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout", @@ -6608,6 +6747,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-ssh.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs": { + "filePath": "project/AuditLogsPage/route-ssh.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/": { "filePath": "cert-manager/PkiTemplateListPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 353aa7a5d..2e63be383 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -63,6 +63,7 @@ const secretManagerRoutes = route("/projects/secret-management/$projectId", [ ) ]) ]), + route("/audit-logs", "project/AuditLogsPage/route-secret-manager.tsx"), route("/access-management", "project/AccessControlPage/route-secret-manager.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-manager.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-manager.tsx"), @@ -312,6 +313,7 @@ const certManagerRoutes = route("/projects/cert-management/$projectId", [ route("/ca/$caName", "cert-manager/CertAuthDetailsByIDPage/route.tsx"), route("/pki-collections/$collectionId", "cert-manager/PkiCollectionDetailsByIDPage/routes.tsx"), route("/settings", "cert-manager/SettingsPage/route.tsx"), + route("/audit-logs", "project/AuditLogsPage/route-cert-manager.tsx"), route("/access-management", "project/AccessControlPage/route-cert-manager.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-cert-manager.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-cert-manager.tsx"), @@ -325,6 +327,7 @@ const kmsRoutes = route("/projects/kms/$projectId", [ route("/overview", "kms/OverviewPage/route.tsx"), route("/kmip", "kms/KmipPage/route.tsx"), route("/settings", "kms/SettingsPage/route.tsx"), + route("/audit-logs", "project/AuditLogsPage/route-kms.tsx"), route("/access-management", "project/AccessControlPage/route-kms.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-kms.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-kms.tsx"), @@ -341,6 +344,7 @@ const sshRoutes = route("/projects/ssh/$projectId", [ route("/ca/$caId", "ssh/SshCaByIDPage/route.tsx"), route("/ssh-host-groups/$sshHostGroupId", "ssh/SshHostGroupDetailsByIDPage/route.tsx"), route("/settings", "ssh/SettingsPage/route.tsx"), + route("/audit-logs", "project/AuditLogsPage/route-ssh.tsx"), route("/access-management", "project/AccessControlPage/route-ssh.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-ssh.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-ssh.tsx"), @@ -357,6 +361,7 @@ const secretScanningRoutes = route("/projects/secret-scanning/$projectId", [ ]), route("/findings", "secret-scanning/SecretScanningFindingsPage/route.tsx"), route("/settings", "secret-scanning/SettingsPage/route.tsx"), + route("/audit-logs", "project/AuditLogsPage/route-secret-scanning.tsx"), route("/access-management", "project/AccessControlPage/route-secret-scanning.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-scanning.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-scanning.tsx"),