diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 1ece89639..737067534 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -1,6 +1,7 @@ ARG POSTHOG_HOST=https://app.posthog.com ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id +ARG SAML_ORG_SLUG=saml-org-slug-default FROM node:20-alpine AS base @@ -35,6 +36,8 @@ ARG INTERCOM_ID ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID ARG INFISICAL_PLATFORM_VERSION ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION +ARG SAML_ORG_SLUG +ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG # Build RUN npm run build @@ -100,6 +103,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ ARG INTERCOM_ID=intercom-id ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID +ARG SAML_ORG_SLUG +ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \ + BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG WORKDIR / diff --git a/backend/e2e-test/routes/v1/secret-import.spec.ts b/backend/e2e-test/routes/v1/secret-import.spec.ts index ba37b5f42..c184e44e5 100644 --- a/backend/e2e-test/routes/v1/secret-import.spec.ts +++ b/backend/e2e-test/routes/v1/secret-import.spec.ts @@ -46,7 +46,7 @@ const deleteSecretImport = async (id: string) => { describe("Secret Import Router", async () => { test.each([ - { importEnv: "dev", importPath: "/" }, // one in root + { importEnv: "prod", importPath: "/" }, // one in root { importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones ])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => { // check for default environments @@ -66,7 +66,7 @@ describe("Secret Import Router", async () => { }); test("Get secret imports", async () => { - const createdImport1 = await createSecretImport("/", "dev"); + const createdImport1 = await createSecretImport("/", "prod"); const createdImport2 = await createSecretImport("/", "staging"); const res = await testServer.inject({ method: "GET", @@ -103,10 +103,10 @@ describe("Secret Import Router", async () => { }); test("Update secret import position", async () => { - const devImportDetails = { path: "/", envSlug: "dev" }; + const prodImportDetails = { path: "/", envSlug: "prod" }; const stagingImportDetails = { path: "/", envSlug: "staging" }; - const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug); + const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug); const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug); const updateImportRes = await testServer.inject({ @@ -136,7 +136,7 @@ describe("Secret Import Router", async () => { position: 2, importEnv: expect.objectContaining({ name: expect.any(String), - slug: expect.stringMatching(devImportDetails.envSlug), + slug: expect.stringMatching(prodImportDetails.envSlug), id: expect.any(String) }) }) @@ -166,7 +166,7 @@ describe("Secret Import Router", async () => { }); test("Delete secret import position", async () => { - const createdImport1 = await createSecretImport("/", "dev"); + const createdImport1 = await createSecretImport("/", "prod"); const createdImport2 = await createSecretImport("/", "staging"); const deletedImport = await deleteSecretImport(createdImport1.id); // check for default environments diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 7cb443ae1..e1149120d 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -61,11 +61,11 @@ export type TQueueJobTypes = { }; [QueueName.SecretWebhook]: { name: QueueJobs.SecWebhook; - payload: { projectId: string; environment: string; secretPath: string }; + payload: { projectId: string; environment: string; secretPath: string; depth?: number }; }; [QueueName.IntegrationSync]: { name: QueueJobs.IntegrationSync; - payload: { projectId: string; environment: string; secretPath: string }; + payload: { projectId: string; environment: string; secretPath: string; depth?: number }; }; [QueueName.SecretFullRepoScan]: { name: QueueJobs.SecretScan; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index d9d843e15..5d77c340b 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -447,7 +447,12 @@ export const registerRoutes = async ( folderDAL }); - const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL }); + const projectRoleService = projectRoleServiceFactory({ + permissionService, + projectRoleDAL, + projectUserMembershipRoleDAL, + identityProjectMembershipRoleDAL + }); const snapshotService = secretSnapshotServiceFactory({ permissionService, @@ -476,14 +481,6 @@ export const registerRoutes = async ( projectEnvDAL, snapshotService }); - const secretImportService = secretImportServiceFactory({ - projectEnvDAL, - folderDAL, - permissionService, - secretImportDAL, - projectDAL, - secretDAL - }); const integrationAuthService = integrationAuthServiceFactory({ integrationAuthDAL, integrationDAL, @@ -511,6 +508,15 @@ export const registerRoutes = async ( secretTagDAL, secretVersionTagDAL }); + const secretImportService = secretImportServiceFactory({ + projectEnvDAL, + folderDAL, + permissionService, + secretImportDAL, + projectDAL, + secretDAL, + secretQueueService + }); const secretBlindIndexService = secretBlindIndexServiceFactory({ permissionService, secretDAL, diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index e1b10530d..fa7439af8 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -153,7 +153,7 @@ export const authLoginServiceFactory = ({ username: email }); if (!userEnc || (userEnc && !userEnc.isAccepted)) { - throw new Error("Failed to find user"); + throw new Error("Failed to find user"); } if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { validateProviderAuthToken(providerAuthToken as string, email); diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 42b8c2c39..4025e4903 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -192,7 +192,7 @@ export const authPaswordServiceFactory = ({ }: TCreateBackupPrivateKeyDTO) => { const userEnc = await userDAL.findUserEncKeyByUserId(userId); if (!userEnc || (userEnc && !userEnc.isAccepted)) { - throw new Error("Failed to find user"); + throw new Error("Failed to find user"); } if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key"); @@ -239,7 +239,7 @@ export const authPaswordServiceFactory = ({ const getBackupPrivateKeyOfUser = async (userId: string) => { const user = await userDAL.findUserEncKeyByUserId(userId); if (!user || (user && !user.isAccepted)) { - throw new Error("Failed to find user"); + throw new Error("Failed to find user"); } const backupKey = await authDAL.getBackupPrivateKeyByUserId(userId); if (!backupKey) throw new Error("Failed to find user backup key"); diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 5bf412c9b..a3c4c84db 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -146,7 +146,27 @@ export const integrationServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); - const deletedIntegration = await integrationDAL.deleteById(id); + const deletedIntegration = await integrationDAL.transaction(async (tx) => { + // delete integration + const deletedIntegrationResult = await integrationDAL.deleteById(id, tx); + + // check if there are other integrations that share the same integration auth + const integrations = await integrationDAL.find( + { + integrationAuthId: integration.integrationAuthId + }, + tx + ); + + if (integrations.length === 0) { + // no other integration shares the same integration auth + // -> delete the integration auth + await integrationAuthDAL.deleteById(integration.integrationAuthId, tx); + } + + return deletedIntegrationResult; + }); + return { ...integration, ...deletedIntegration }; }; diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 5c8ecdcff..831af3200 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -14,16 +14,25 @@ import { import { BadRequestError } from "@app/lib/errors"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; +import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectRoleDALFactory } from "./project-role-dal"; type TProjectRoleServiceFactoryDep = { projectRoleDAL: TProjectRoleDALFactory; permissionService: Pick; + identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory; + projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory; }; export type TProjectRoleServiceFactory = ReturnType; -export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: TProjectRoleServiceFactoryDep) => { +export const projectRoleServiceFactory = ({ + projectRoleDAL, + permissionService, + identityProjectMembershipRoleDAL, + projectUserMembershipRoleDAL +}: TProjectRoleServiceFactoryDep) => { const createRole = async ( actor: ActorType, actorId: string, @@ -96,8 +105,25 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); + + const identityRole = await identityProjectMembershipRoleDAL.findOne({ customRoleId: roleId }); + const projectUserRole = await projectUserMembershipRoleDAL.findOne({ customRoleId: roleId }); + + if (identityRole) { + throw new BadRequestError({ + message: "The role is assigned to one or more identities. Make sure to unassign them before deleting the role.", + name: "Delete role" + }); + } + if (projectUserRole) { + throw new BadRequestError({ + message: "The role is assigned to one or more users. Make sure to unassign them before deleting the role.", + name: "Delete role" + }); + } + const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId }); - if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); + if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" }); return deletedRole; }; diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index 023d039ca..b3147d1fa 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -170,7 +170,8 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str // if the given folder id is root folder id then intial path is set as / instead of /root // if not root folder the path here will be / path: db.raw(`CONCAT('/', (CASE WHEN "parentId" is NULL THEN '' ELSE ${TableName.SecretFolder}.name END))`), - child: db.raw("NULL::uuid") + child: db.raw("NULL::uuid"), + environmentSlug: `${TableName.Environment}.slug` }) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .where({ projectId }) @@ -190,14 +191,15 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str ELSE CONCAT('/', secret_folders.name) END, parent.path )` ), - child: db.raw("COALESCE(parent.child, parent.id)") + child: db.raw("COALESCE(parent.child, parent.id)"), + environmentSlug: "parent.environmentSlug" }) .from(TableName.SecretFolder) .join("parent", "parent.parentId", `${TableName.SecretFolder}.id`) ); }) .select("*") - .from("parent"); + .from("parent"); export type TSecretFolderDALFactory = ReturnType; // never change this. If u do write a migration for it @@ -257,10 +259,12 @@ export const secretFolderDALFactory = (db: TDbClient) => { const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => { try { const folders = await sqlFindSecretPathByFolderId(tx || db, projectId, folderIds); + const rootFolders = groupBy( folders.filter(({ parentId }) => parentId === null), (i) => i.child || i.id // root condition then child and parent will null ); + return folderIds.map((folderId) => rootFolders[folderId]?.[0]); } catch (error) { throw new DatabaseError({ error, name: "Find by secret path" }); diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index cbc8936e1..aa45d410d 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -49,7 +49,7 @@ export const secretImportDALFactory = (db: TDbClient) => { } }; - const find = async (filter: Partial, tx?: Knex) => { + const find = async (filter: Partial, tx?: Knex) => { try { const docs = await (tx || db)(TableName.SecretImport) .where(filter) diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 40f9797e4..2d59284d4 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -7,6 +7,7 @@ import { BadRequestError } from "@app/lib/errors"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretDALFactory } from "../secret/secret-dal"; +import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "./secret-import-dal"; import { fnSecretsFromImports } from "./secret-import-fns"; @@ -25,6 +26,7 @@ type TSecretImportServiceFactoryDep = { projectDAL: Pick; projectEnvDAL: TProjectEnvDALFactory; permissionService: Pick; + secretQueueService: Pick; }; const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" }); @@ -37,7 +39,8 @@ export const secretImportServiceFactory = ({ permissionService, folderDAL, projectDAL, - secretDAL + secretDAL, + secretQueueService }: TSecretImportServiceFactoryDep) => { const createImport = async ({ environment, @@ -77,10 +80,19 @@ export const secretImportServiceFactory = ({ const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" }); - // TODO(akhilmhdh-pg): updated permission check add here const [importEnv] = await projectEnvDAL.findBySlugs(projectId, [data.environment]); if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" }); + const sourceFolder = await folderDAL.findBySecretPath(projectId, data.environment, data.path); + if (sourceFolder) { + const existingImport = await secretImportDAL.findOne({ + folderId: sourceFolder.id, + importEnv: folder.environment.id, + importPath: path + }); + if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" }); + } + const secImport = await secretImportDAL.transaction(async (tx) => { const lastPos = await secretImportDAL.findLastImportPosition(folder.id, tx); return secretImportDAL.create( @@ -94,6 +106,12 @@ export const secretImportServiceFactory = ({ ); }); + await secretQueueService.syncSecrets({ + secretPath: secImport.importPath, + projectId, + environment: importEnv.slug + }); + return { ...secImport, importEnv }; }; @@ -131,6 +149,20 @@ export const secretImportServiceFactory = ({ : await projectEnvDAL.findById(secImpDoc.importEnv); if (!importedEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" }); + const sourceFolder = await folderDAL.findBySecretPath( + projectId, + importedEnv.slug, + data.path || secImpDoc.importPath + ); + if (sourceFolder) { + const existingImport = await secretImportDAL.findOne({ + folderId: sourceFolder.id, + importEnv: folder.environment.id, + importPath: path + }); + if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" }); + } + const updatedSecImport = await secretImportDAL.transaction(async (tx) => { const secImp = await secretImportDAL.findOne({ folderId: folder.id, id }); if (!secImp) throw ERR_SEC_IMP_NOT_FOUND; @@ -185,6 +217,13 @@ export const secretImportServiceFactory = ({ if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" }); return { ...doc, importEnv }; }); + + await secretQueueService.syncSecrets({ + secretPath: path, + projectId, + environment + }); + return secImport; }; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index d815875df..1fc6b1109 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -3,7 +3,7 @@ import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { BadRequestError } from "@app/lib/errors"; -import { isSamePath } from "@app/lib/fn"; +import { groupBy, isSamePath, unique } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -23,7 +23,6 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; -import { fnSecretsFromImports } from "../secret-import/secret-import-fns"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TWebhookDALFactory } from "../webhook/webhook-dal"; import { fnTriggerWebhook } from "../webhook/webhook-fns"; @@ -32,7 +31,6 @@ import { interpolateSecrets } from "./secret-fns"; import { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types"; export type TSecretQueueFactory = ReturnType; - type TSecretQueueFactoryDep = { queueService: TQueueServiceFactory; integrationDAL: Pick; @@ -60,6 +58,8 @@ export type TGetSecrets = { environment: string; }; +const MAX_SYNC_SECRET_DEPTH = 5; + export const secretQueueFactory = ({ queueService, integrationDAL, @@ -117,7 +117,10 @@ export const secretQueueFactory = ({ }); }; - const syncSecrets = async (dto: TGetSecrets) => { + const syncSecrets = async (dto: TGetSecrets & { depth?: number }) => { + logger.info( + `syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environment}] [path=${dto.secretPath}]` + ); await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, { jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`, removeOnFail: { count: 5 }, @@ -227,62 +230,42 @@ export const secretQueueFactory = ({ } }; - const getIntegrationSecrets = async (dto: TGetSecrets & { folderId: string }, key: string) => { + type Content = Record; + + /** + * Return the secrets in a given [folderId] including secrets from + * nested imported folders recursively. + */ + const getIntegrationSecrets = async (dto: { + projectId: string; + environment: string; + folderId: string; + key: string; + depth: number; + }) => { + let content: Content = {}; + if (dto.depth > MAX_SYNC_SECRET_DEPTH) { + logger.info( + `getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]` + ); + return content; + } + + // process secrets in current folder const secrets = await secretDAL.findByFolderId(dto.folderId); - - // get imported secrets - const secretImport = await secretImportDAL.find({ folderId: dto.folderId }); - const importedSecrets = await fnSecretsFromImports({ - allowedImports: secretImport, - secretDAL, - folderDAL - }); - - if (!secrets.length && !importedSecrets.length) return {}; - - const content: Record = {}; - - importedSecrets.forEach(({ secrets: secs }) => { - secs.forEach((secret) => { - const secretKey = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - key - }); - const secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); - content[secretKey] = { value: secretValue }; - content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); - - if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { - const commentValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretCommentCiphertext, - iv: secret.secretCommentIV, - tag: secret.secretCommentTag, - key - }); - content[secretKey].comment = commentValue; - } - }); - }); secrets.forEach((secret) => { const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, - key + key: dto.key }); const secretValue = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretValueCiphertext, iv: secret.secretValueIV, tag: secret.secretValueTag, - key + key: dto.key }); content[secretKey] = { value: secretValue }; @@ -292,38 +275,111 @@ export const secretQueueFactory = ({ ciphertext: secret.secretCommentCiphertext, iv: secret.secretCommentIV, tag: secret.secretCommentTag, - key + key: dto.key }); content[secretKey].comment = commentValue; } content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); }); + const expandSecrets = interpolateSecrets({ projectId: dto.projectId, - secretEncKey: key, + secretEncKey: dto.key, folderDAL, secretDAL }); + await expandSecrets(content); + + // check if current folder has any imports from other folders + const secretImport = await secretImportDAL.find({ folderId: dto.folderId }); + + // if no imports then return secrets in the current folder + if (!secretImport) return content; + + const importedFolders = await folderDAL.findByManySecretPath( + secretImport.map(({ importEnv, importPath }) => ({ + envId: importEnv.id, + secretPath: importPath + })) + ); + + for await (const folder of importedFolders) { + if (folder) { + // get secrets contained in each imported folder by recursively calling + // this function against the imported folder + const importedSecrets = await getIntegrationSecrets({ + environment: dto.environment, + projectId: dto.projectId, + folderId: folder.id, + key: dto.key, + depth: dto.depth + 1 + }); + + // add the imported secrets to the current folder secrets + content = { ...content, ...importedSecrets }; + } + } + return content; }; queueService.start(QueueName.IntegrationSync, async (job) => { - const { environment, projectId, secretPath } = job.data; + const { environment, projectId, secretPath, depth = 1 } = job.data; + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) { - logger.error("Secret path not found"); + logger.error(new Error("Secret path not found")); return; } - const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); + // start syncing all linked imports also + if (depth < MAX_SYNC_SECRET_DEPTH) { + // find all imports made with the given environment and secret path + const linkSourceDto = { + projectId, + importEnv: folder.environment.id, + importPath: secretPath + }; + const imports = await secretImportDAL.find(linkSourceDto); + + if (imports.length) { + // keep calling sync secret for all the imports made + const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId); + const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds); + const foldersGroupedById = groupBy(importedFolders, (i) => i.child || i.id); + await Promise.all( + imports + .filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path)) + .map(({ folderId }) => { + const syncDto = { + depth: depth + 1, + projectId, + secretPath: foldersGroupedById[folderId][0].path, + environment: foldersGroupedById[folderId][0].environmentSlug + }; + logger.info( + `getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` + ); + return syncSecrets(syncDto); + }) + ); + } + } else { + logger.info(`getIntegrationSecrets: Secret depth exceeded for [projectId=${projectId}] [folderId=${folder.id}]`); + } + + const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment const toBeSyncedIntegrations = integrations.filter( + // note: sync only the integrations sourced from secretPath ({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath) ); if (!integrations.length) return; - logger.info("Secret integration sync started", job.data, job.id); + logger.info( + `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` + ); for (const integration of toBeSyncedIntegrations) { const integrationAuth = { ...integration.integrationAuth, @@ -334,7 +390,13 @@ export const secretQueueFactory = ({ const botKey = await projectBotService.getBotKey(projectId); const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(integrationAuth, botKey); - const secrets = await getIntegrationSecrets({ environment, projectId, secretPath, folderId: folder.id }, botKey); + const secrets = await getIntegrationSecrets({ + environment, + projectId, + folderId: folder.id, + key: botKey, + depth: 1 + }); const suffixedSecrets: typeof secrets = {}; const metadata = integration.metadata as Record; if (metadata) { @@ -362,7 +424,7 @@ export const secretQueueFactory = ({ }); } - logger.info("Secret integration sync ended", job.id); + logger.info("Secret integration sync ended: %s", job.id); }); queueService.start(QueueName.SecretReminder, async ({ data }) => { @@ -403,7 +465,7 @@ export const secretQueueFactory = ({ }); queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => { - logger.error("Failed to sync integration", job?.data, err); + logger.error(err, "Failed to sync integration %s", job?.id); }); queueService.start(QueueName.SecretWebhook, async (job) => { @@ -411,7 +473,8 @@ export const secretQueueFactory = ({ }); return { - syncSecrets, + // depth is internal only field thus no need to make it available outside + syncSecrets: (dto: TGetSecrets) => syncSecrets(dto), syncIntegrations, addSecretReminder, removeSecretReminder, diff --git a/docs/api-reference/overview/examples/e2ee-disabled.mdx b/docs/api-reference/overview/examples/e2ee-disabled.mdx deleted file mode 100644 index 1a9e57552..000000000 --- a/docs/api-reference/overview/examples/e2ee-disabled.mdx +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: "E2EE Disabled" ---- - -Using Infisical's API to read/write secrets with E2EE disabled allows you to create, update, and retrieve secrets -in plaintext. Effectively, this means each such secret operation only requires 1 HTTP call. - - - - Retrieve all secrets for an Infisical project and environment. - - - ```bash - curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw?environment=environment&workspaceId=workspaceId' \ - --header 'Authorization: Bearer serviceToken' - - ``` - - - #### - - When using a [service token](../../../documentation/platform/token) with access to a single environment and path, you don't need to provide request parameters because the server will automatically scope the request to the defined environment/secrets path of the service token used. - For all other cases, request parameters are required. - - #### - - The ID of the workspace - - - The environment slug - - - Path to secrets in workspace - - - - Create a secret in Infisical. - - - - ```bash - curl --location --request POST 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ - --header 'Authorization: Bearer serviceToken' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "workspaceId", - "environment": "environment", - "type": "shared", - "secretValue": "secretValue", - "secretPath": "/" - }' - ``` - - - - - Name of secret to create - - - The ID of the workspace - - - The environment slug - - - Value of secret - - - Comment of secret - - - Path to secret in workspace - - - The type of the secret. Valid options are “shared” or “personal” - - - - Retrieve a secret from Infisical. - - - - ```bash - curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw/secretName?workspaceId=workspaceId&environment=environment' \ - --header 'Authorization: Bearer serviceToken' - ``` - - - - - Name of secret to retrieve - - - The ID of the workspace - - - The environment slug - - - Path to secrets in workspace - - - The type of the secret. Valid options are “shared” or “personal” - - - - Update an existing secret in Infisical. - - - - ```bash - curl --location --request PATCH 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ - --header 'Authorization: Bearer serviceToken' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "workspaceId", - "environment": "environment", - "type": "shared", - "secretValue": "secretValue", - "secretPath": "/" - }' - ``` - - - - - Name of secret to update - - - The ID of the workspace - - - The environment slug - - - Value of secret - - - Path to secret in workspace. - - - The type of the secret. Valid options are “shared” or “personal” - - - - Delete a secret in Infisical. - - - - ```bash - curl --location --request DELETE 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ - --header 'Authorization: Bearer serviceToken' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "workspaceId", - "environment": "environment", - "type": "shared", - "secretPath": "/" - }' - ``` - - - - - Name of secret to update - - - The ID of the workspace - - - The environment slug - - - Path to secret in workspace. - - - The type of the secret. Valid options are “shared” or “personal” - - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/e2ee-enabled.mdx b/docs/api-reference/overview/examples/e2ee-enabled.mdx deleted file mode 100644 index 1de9c2290..000000000 --- a/docs/api-reference/overview/examples/e2ee-enabled.mdx +++ /dev/null @@ -1,862 +0,0 @@ ---- -title: "E2EE Enabled" ---- - - - E2EE enabled mode only works with [Service Tokens](/documentation/platform/token) and cannot be used with [Identities](/documentation/platform/identities/overview). - - -Using Infisical's API to read/write secrets with E2EE enabled allows you to create, update, and retrieve secrets -but requires you to perform client-side encryption/decryption operations. For this reason, we recommend using one of the available -SDKs instead. - - - - - - Retrieve all secrets for an Infisical project and environment. -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const getSecrets = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Get secrets for your project and environment - const { data } = await axios.get( - `${BASE_URL}/api/v3/secrets?${new URLSearchParams({ - environment: serviceTokenData.environment, - workspaceId: serviceTokenData.workspace - })}`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - const encryptedSecrets = data.secrets; - - // 3. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 4. Decrypt the (encrypted) secrets - const secrets = encryptedSecrets.map((secret) => { - const secretKey = decrypt({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - secret: projectKey - }); - - const secretValue = decrypt({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - secret: projectKey - }); - - return ({ - secretKey, - secretValue - }); - }); - - console.log('secrets: ', secrets); -} - -getSecrets(); - -``` - - - -```Python -import requests -import base64 -from Cryptodome.Cipher import AES - - -BASE_URL = "http://app.infisical.com" - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def get_secrets(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Get secrets for your project and environment - data = requests.get( - f"{BASE_URL}/api/v3/secrets", - params={ - "environment": service_token_data["environment"], - "workspaceId": service_token_data["workspace"], - }, - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - encrypted_secrets = data["secrets"] - - # 3. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 4. Decrypt the (encrypted) secrets - secrets = [] - for secret in encrypted_secrets: - secret_key = decrypt( - ciphertext=secret["secretKeyCiphertext"], - iv=secret["secretKeyIV"], - tag=secret["secretKeyTag"], - secret=project_key, - ) - - secret_value = decrypt( - ciphertext=secret["secretValueCiphertext"], - iv=secret["secretValueIV"], - tag=secret["secretValueTag"], - secret=project_key, - ) - - secrets.append( - { - "secret_key": secret_key, - "secret_value": secret_value, - } - ) - - print("secrets:", secrets) - - -get_secrets() - -``` - - - - - - -Create a secret in Infisical. -```js -const crypto = require('crypto'); -const axios = require('axios'); -const nacl = require('tweetnacl'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ({ text, secret }) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const createSecrets = async () => { - const serviceToken = ''; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared'; // 'shared' or 'personal' - const secretKey = 'some_key'; - const secretValue = 'some_value'; - const secretComment = 'some_comment'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 3. Encrypt your secret with the project key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encrypt({ - text: secretKey, - secret: projectKey - }); - - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encrypt({ - text: secretValue, - secret: projectKey - }); - - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encrypt({ - text: secretComment, - secret: projectKey - }); - - // 4. Send (encrypted) secret to Infisical - await axios.post( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); -} - -createSecrets(); -``` - - - -```Python -import base64 -import requests -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes - - -BASE_URL = "https://app.infisical.com" -BLOCK_SIZE_BYTES = 16 - - -def encrypt(text, secret): - iv = get_random_bytes(BLOCK_SIZE_BYTES) - secret = bytes(secret, "utf-8") - cipher = AES.new(secret, AES.MODE_GCM, iv) - ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) - return { - "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), - "tag": base64.standard_b64encode(tag).decode("utf-8"), - "iv": base64.standard_b64encode(iv).decode("utf-8"), - } - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def create_secrets(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared or "personal" - secret_key = "some_key" - secret_value = "some_value" - secret_comment = "some_comment" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 3. Encrypt your secret with the project key - encrypted_key_data = encrypt(text=secret_key, secret=project_key) - encrypted_value_data = encrypt(text=secret_value, secret=project_key) - encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) - - # 4. Send (encrypted) secret to Infisical - requests.post( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type, - "secretKeyCiphertext": encrypted_key_data["ciphertext"], - "secretKeyIV": encrypted_key_data["iv"], - "secretKeyTag": encrypted_key_data["tag"], - "secretValueCiphertext": encrypted_value_data["ciphertext"], - "secretValueIV": encrypted_value_data["iv"], - "secretValueTag": encrypted_value_data["tag"], - "secretCommentCiphertext": encrypted_comment_data["ciphertext"], - "secretCommentIV": encrypted_comment_data["iv"], - "secretCommentTag": encrypted_comment_data["tag"] - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -create_secrets() - -``` - - - - - - - Retrieve a secret from Infisical. -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const getSecret = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Get the secret from your project and environment - const { data } = await axios.get( - `${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({ - environment: serviceTokenData.environment, - workspaceId: serviceTokenData.workspace, - type: secretType // optional, defaults to 'shared' - })}`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - const encryptedSecret = data.secret; - - // 3. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 4. Decrypt the (encrypted) secret value - - const secretValue = decrypt({ - ciphertext: encryptedSecret.secretValueCiphertext, - iv: encryptedSecret.secretValueIV, - tag: encryptedSecret.secretValueTag, - secret: projectKey - }); - - console.log('secret: ', ({ - secretKey, - secretValue - })); -} - -getSecret(); - -``` - - - -```Python -import requests -import base64 -from Cryptodome.Cipher import AES - - -BASE_URL = "http://app.infisical.com" - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def get_secret(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Get secret from your project and environment - data = requests.get( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - params={ - "environment": service_token_data["environment"], - "workspaceId": service_token_data["workspace"], - "type": secret_type # optional, defaults to "shared" - }, - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - encrypted_secret = data["secret"] - - # 3. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 4. Decrypt the (encrypted) secret value - secret_value = decrypt( - ciphertext=encrypted_secret["secretValueCiphertext"], - iv=encrypted_secret["secretValueIV"], - tag=encrypted_secret["secretValueTag"], - secret=project_key, - ) - - print("secret: ", { - "secret_key": secret_key, - "secret_value": secret_value - }) - - -get_secret() - -``` - - - - - - -Update an existing secret in Infisical. -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ({ text, secret }) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const updateSecrets = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key'; - const secretValue = 'updated_value'; - const secretComment = 'updated_comment'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 3. Encrypt your updated secret with the project key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encrypt({ - text: secretKey, - secret: projectKey - }); - - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encrypt({ - text: secretValue, - secret: projectKey - }); - - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encrypt({ - text: secretComment, - secret: projectKey - }); - - // 4. Send (encrypted) updated secret to Infisical - await axios.patch( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); -} - -updateSecrets(); -``` - - - -```Python -import base64 -import requests -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes - - -BASE_URL = "https://app.infisical.com" -BLOCK_SIZE_BYTES = 16 - - -def encrypt(text, secret): - iv = get_random_bytes(BLOCK_SIZE_BYTES) - secret = bytes(secret, "utf-8") - cipher = AES.new(secret, AES.MODE_GCM, iv) - ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) - return { - "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), - "tag": base64.standard_b64encode(tag).decode("utf-8"), - "iv": base64.standard_b64encode(iv).decode("utf-8"), - } - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def update_secret(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - secret_value = "updated_value" - secret_comment = "updated_comment" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 3. Encrypt your updated secret with the project key - encrypted_key_data = encrypt(text=secret_key, secret=project_key) - encrypted_value_data = encrypt(text=secret_value, secret=project_key) - encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) - - # 4. Send (encrypted) updated secret to Infisical - requests.patch( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type, - "secretKeyCiphertext": encrypted_key_data["ciphertext"], - "secretKeyIV": encrypted_key_data["iv"], - "secretKeyTag": encrypted_key_data["tag"], - "secretValueCiphertext": encrypted_value_data["ciphertext"], - "secretValueIV": encrypted_value_data["iv"], - "secretValueTag": encrypted_value_data["tag"], - "secretCommentCiphertext": encrypted_comment_data["ciphertext"], - "secretCommentIV": encrypted_comment_data["iv"], - "secretCommentTag": encrypted_comment_data["tag"] - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -update_secret() - -``` - - - - - - - Delete a secret in Infisical. -```js -const axios = require('axios'); -const BASE_URL = 'https://app.infisical.com'; - -const deleteSecrets = async () => { - const serviceToken = 'your_service_token'; - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key' - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Delete secret from Infisical - await axios.delete( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - }, - } - ); -}; - -deleteSecrets(); -``` - - - -```Python -import requests - -BASE_URL = "https://app.infisical.com" - - -def delete_secrets(): - service_token = "" - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Delete secret from Infisical - requests.delete( - f"{BASE_URL}/api/v2/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -delete_secrets() - -``` - - - - If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header. - - - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/integration.mdx b/docs/api-reference/overview/examples/integration.mdx new file mode 100644 index 000000000..71f5b6de4 --- /dev/null +++ b/docs/api-reference/overview/examples/integration.mdx @@ -0,0 +1,90 @@ +--- +title: "Configure native integrations via API" +description: "How to use Infisical API to sync secrets to external secret managers" +--- + +The Infisical API allows you to create programmatic integrations that connect with third-party secret managers to synchronize secrets from Infisical. + +This guide will primarily demonstrate the process using AWS Secret Store Manager (AWS SSM), but the steps are generally applicable to other secret management integrations. + + + For details on setting up AWS SSM synchronization and understanding its prerequisites, refer to the [AWS SSM integration setup documentation](../../../integrations/cloud/aws-secret-manager). + + + + + Authentication is required for all integrations. Use the [Integration Auth API](../../endpoints/integrations/create-auth) with the following parameters to authenticate. + + + Set this parameter to **aws-secret-manager**. + + + The Infisical project ID for the integration. + + + The AWS IAM User Access ID. + + + The AWS IAM User Access Secret Key. + + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/integration-auth/access-token \ + --header 'Authorization: ' \ + --header 'Content-Type: application/json' \ + --data '{ + "workspaceId": "", + "integration": "aws-secret-manager", + "accessId": "", + "accessToken": "" + }' + ``` + + + + Once authentication between AWS SSM and Infisical is established, you can configure the synchronization behavior. + This involves specifying the source (environment and secret path in Infisical) and the destination in SSM to which the secrets will be synchronized. + + Use the [integration API](../../endpoints/integrations/create) with the following parameters to configure the sync source and destination. + + + The ID of the integration authentication object used with AWS, obtained from the previous API response. + + + Indicates whether the integration should be active or inactive. + + + The secret name for saving in AWS SSM, which can be arbitrarily chosen. + + + The AWS region where the SSM is located, e.g., `us-east-1`. + + + The Infisical environment slug from which secrets will be synchronized, e.g., `dev`. + + + The Infisical folder path from which secrets will be synchronized, e.g., `/some/path`. The root path is `/`. + + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/integration \ + --header 'Authorization: ' \ + --header 'Content-Type: application/json' \ + --data '{ + "integrationAuthId": "", + "sourceEnvironment": "", + "secretPath": "", + "app": "", + "region": "" + }' + ``` + + + + + +Congratulations! You have successfully set up an integration to synchronize secrets from Infisical with AWS SSM. +For more information, [view the integration API reference](../../endpoints/integrations). + \ No newline at end of file diff --git a/docs/api-reference/overview/examples/note.mdx b/docs/api-reference/overview/examples/note.mdx deleted file mode 100644 index 8491dfaae..000000000 --- a/docs/api-reference/overview/examples/note.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: "Note on E2EE" ---- - -Each project in Infisical can have **End-to-End Encryption (E2EE)** enabled or disabled. - -By default, all projects have **E2EE** enabled which means the server is not able to decrypt any values because all secret encryption/decryption operations occur on the client-side; this can be (optionally) disabled. However, this has limitations around functionality and ease-of-use: - -- You cannot make HTTP calls to Infisical to read/write secrets in plaintext. -- You cannot leverage non-E2EE features like native integrations and in-platform automations like dynamic secrets and secret rotation. - - - - Example read/write secrets without client-side encryption/decryption - - - Example read/write secrets with client-side encryption/decryption - - - -## FAQ - - - - We recommend starting with having **E2EE** enabled and disabling it if: - - - You're self-hosting Infisical, so having your instance of Infisical be able to read your secrets isn't an issue. - - You want an easier way to read/write secrets with Infisical. - - You need more power out of non-E2EE features such as secret rotation, dynamic secrets, etc. - - - - You can enable/disable E2EE for your project in Infisical in the Project Settings. - - - It is secure and in fact how most vendors in our industry are able to offer features like secret rotation. In this mode, secrets are encrypted at rest by - a series of keys, secured ultimately by a top-level `ROOT_ENCRYPTION_KEY` located on the server. - - If you're concerned about Infisical Cloud's ability to read your secrets, then you may wish to - use it with **E2EE** enabled or self-host Infisical on your own infrastructure and disable E2EE there. - - As an organization, we do not read any customer secrets without explicit permission; access to the `ROOT_ENCRYPTION_KEY` is restricted to one individual in the organization. - - \ No newline at end of file diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index bd9354aa1..0f414c62a 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -38,7 +38,7 @@ Infisical helps developers achieve secure centralized secret management and prov - Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments. - Secure and compliant secret management practices in **[production environments](/sdks/overview)**. - **Facilitated workflows** around [secret change management](/documentation/platform/pr-workflows), [access requests](/documentation/platform/access-controls/access-requests), [temporary access provisioning](/documentation/platform/access-controls/temporary-access), and more. -- **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](http://localhost:3000/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities. +- **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities. ## How does Infisical work? diff --git a/docs/documentation/platform/token.mdx b/docs/documentation/platform/token.mdx index efb1e05d4..78445f4f1 100644 --- a/docs/documentation/platform/token.mdx +++ b/docs/documentation/platform/token.mdx @@ -44,7 +44,7 @@ In the above screenshot, you can see that we are creating a token token with `re of the `/common` path within the development environment of the project; the token expires in 6 months and can be used from any IP address. -For a deeper understanding of service tokens, it is recommended to read [this guide](http://localhost:3000/internals/service-tokens). +For a deeper understanding of service tokens, it is recommended to read [this guide](https://infisical.com/docs/internals/service-tokens). **FAQ** diff --git a/docs/mint.json b/docs/mint.json index 03605cba2..864d115ff 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -388,9 +388,7 @@ { "group": "Examples", "pages": [ - "api-reference/overview/examples/note", - "api-reference/overview/examples/e2ee-disabled", - "api-reference/overview/examples/e2ee-enabled" + "api-reference/overview/examples/integration" ] } ] @@ -473,7 +471,7 @@ ] }, { - "group": "Secret tags", + "group": "Secret Tags", "pages": [ "api-reference/endpoints/secret-tags/list", "api-reference/endpoints/secret-tags/create", @@ -493,7 +491,7 @@ ] }, { - "group": "Secret imports", + "group": "Secret Imports", "pages": [ "api-reference/endpoints/secret-imports/list", "api-reference/endpoints/secret-imports/create", diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index dae2e8044..db8034199 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -335,6 +335,10 @@ To login into Infisical with OAuth providers such as Google, configure the assoc Requires enterprise license. Please contact team@infisical.com to get more information. + + Configure SAML organization slug to automatically redirect all users of your Infisical instance to the identity provider. + + diff --git a/docs/style.css b/docs/style.css index 6674ce2c6..b76d06450 100644 --- a/docs/style.css +++ b/docs/style.css @@ -63,6 +63,30 @@ border-color: #ebebeb; } +#content-area .mt-8 .rounded-xl{ + border-radius: 0; +} + +#content-area .mt-8 .rounded-lg{ + border-radius: 0; +} + +#content-area .mt-6 .rounded-xl{ + border-radius: 0; +} + +#content-area .mt-6 .rounded-lg{ + border-radius: 0; +} + +#content-area .mt-6 .rounded-md{ + border-radius: 0; +} + +#content-area .mt-8 .rounded-md{ + border-radius: 0; +} + #content-area div.my-4{ border-radius: 0; border-width: 1px; @@ -78,6 +102,10 @@ border-radius: 0; } +#content-area a { + border-radius: 0; +} + #content-area .not-prose { border-radius: 0; } diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 2060c214a..9090fc603 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -52,6 +52,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ ARG INTERCOM_ID ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID +ARG SAML_ORG_SLUG +ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \ + BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG ARG NEXT_INFISICAL_PLATFORM_VERSION ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION=$NEXT_INFISICAL_PLATFORM_VERSION diff --git a/frontend/scripts/initialize-standalone-build.sh b/frontend/scripts/initialize-standalone-build.sh index d9138bb77..859814eda 100755 --- a/frontend/scripts/initialize-standalone-build.sh +++ b/frontend/scripts/initialize-standalone-build.sh @@ -4,6 +4,8 @@ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID" +scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG" + if [ "$TELEMETRY_ENABLED" != "false" ]; then echo "Telemetry is enabled" scripts/set-standalone-build-telemetry.sh true diff --git a/frontend/scripts/start.sh b/frontend/scripts/start.sh index 1db867e15..1488ad328 100644 --- a/frontend/scripts/start.sh +++ b/frontend/scripts/start.sh @@ -4,6 +4,8 @@ scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY" "$NEXT_PUBLIC_P scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID" +scripts/replace-variable.sh "$BAKED_NEXT_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG" + if [ "$TELEMETRY_ENABLED" != "false" ]; then echo "Telemetry is enabled" scripts/set-telemetry.sh true diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index d2b41abcd..b6cc4ee46 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -483,10 +483,10 @@ const OrganizationPage = withPermission( const addUsersToProject = useAddUserToWsNonE2EE(); - const { data: updateClosed } = useGetUserAction("april_2024_db_update_closed"); + const { data: updateClosed } = useGetUserAction("april_13_2024_db_update_closed"); const registerUserAction = useRegisterUserAction(); const closeUpdate = async () => { - await registerUserAction.mutateAsync("april_2024_db_update_closed"); + await registerUserAction.mutateAsync("april_13_2024_db_update_closed"); }; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -594,6 +594,7 @@ const OrganizationPage = withPermission( )}
+ {window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080") && (
- Scheduled maintenance on April 6th 2024 {" "} + Scheduled maintenance on April 13th 2024 {" "}
- Infisical will undergo scheduled maintenance for approximately 1 hour on Saturday, April 6th, 11am EST. During these hours, read + Infisical will undergo scheduled maintenance for approximately 1 hour on Saturday, April 13th, 11am EST. During these hours, read operations will continue to function normally but no resources will be editable. - No action is required on your end — your applications can continue to fetch secrets. + No action is required on your end — your applications will continue to fetch secrets.
-
+
)} +

Projects

{ + if (process.env.NEXT_PUBLIC_SAML_ORG_SLUG && process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default") { + const callbackPort = queryParams.get("callback_port"); + window.open( + `/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${ + callbackPort ? `?callback_port=${callbackPort}` : "" + }` + ); + window.close(); + } + }, []) + const handleLogin = async (e: FormEvent) => { e.preventDefault(); try { @@ -244,7 +256,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
) : ( -
+
)}
diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 8cc2e6b8d..99edd5015 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -30,9 +30,6 @@ export const MembersPage = withPermission(

Machine Identities

-
- New -
Organization Roles diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx index e2086a6ab..b4e08f51a 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -31,7 +31,7 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { const [searchRoles, setSearchRoles] = useState(""); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; - + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(orgId); @@ -49,7 +49,7 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { handlePopUpClose("deleteRole"); } catch (err) { console.log(err); - createNotification({ type: "error", text: "Failed to create role" }); + createNotification({ type: "error", text: "Failed to delete role" }); } }; diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index aa29a363b..d8a22bdb4 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -29,7 +29,7 @@ type Props = { export const ProjectRoleList = ({ onSelectRole }: Props) => { const [searchRoles, setSearchRoles] = useState(""); - + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?.id || ""; @@ -50,7 +50,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { handlePopUpClose("deleteRole"); } catch (err) { console.log(err); - createNotification({ type: "error", text: "Failed to create role" }); + createNotification({ type: "error", text: "Failed to delete role" }); } }; diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index 270fd8c65..122135c03 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -313,6 +313,7 @@ export const SecretMainPage = () => { workspaceId={workspaceId} secretPath={secretPath} sortDir={sortDir} + searchTerm={filter.searchFilter} /> {canReadSecret && ( { @@ -35,8 +37,6 @@ export const FolderListView = ({ ] as const); const router = useRouter(); - - const { mutateAsync: updateFolder } = useUpdateFolder(); const { mutateAsync: deleteFolder } = useDeleteFolder(); @@ -100,6 +100,7 @@ export const FolderListView = ({ return ( <> {folders + .filter(({ name }) => name.toUpperCase().includes(String(searchTerm?.toUpperCase()))) .sort((a, b) => sortDir === SortDir.ASC ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) diff --git a/k8-operator/README.md b/k8-operator/README.md index 807476c0c..f2e349f07 100644 --- a/k8-operator/README.md +++ b/k8-operator/README.md @@ -72,6 +72,12 @@ If you are editing the API definitions, generate the manifests such as CRs or CR make manifests ``` +Also, after editing the API definitions, update the kubectl-install folder: + +```sh +make kubectl-install +``` + **NOTE:** Run `make --help` for more information on all potential `make` targets More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) diff --git a/k8-operator/kubectl-install/install-secrets-operator.yaml b/k8-operator/kubectl-install/install-secrets-operator.yaml index 5a11db30a..4e66a03cd 100644 --- a/k8-operator/kubectl-install/install-secrets-operator.yaml +++ b/k8-operator/kubectl-install/install-secrets-operator.yaml @@ -96,12 +96,47 @@ spec: - secretsScope - serviceTokenSecretReference type: object + universalAuth: + properties: + credentialsRef: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - secretName + - secretNamespace + type: object + secretsScope: + properties: + envSlug: + type: string + projectSlug: + type: string + secretsPath: + type: string + required: + - envSlug + - projectSlug + - secretsPath + type: object + required: + - credentialsRef + - secretsScope + type: object type: object hostAPI: description: Infisical host to pull secrets from type: string managedSecretReference: properties: + creationPolicy: + default: Orphan + description: 'The Kubernetes Secret creation policy. Enum with values: ''Owner'', ''Orphan''. Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it. Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted.' + type: string secretName: description: The name of the Kubernetes Secret type: string