diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 5c1f15ca3..0fb2a6671 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -1,7 +1,6 @@ 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,9 +34,7 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY 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 +ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION # Build RUN npm run build @@ -113,9 +110,6 @@ 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/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index deeaf5cd4..f4ffb87ba 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -676,7 +676,8 @@ export const INTEGRATION = { kmsKeyId: "The ID of the encryption key from AWS KMS.", shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.", shouldMaskSecrets: "The flag to determine the visibility of secrets to sync. Used by Gitlab", - shouldProtectSecrets: "The flag to determine usage rules of secrets to sync. Used by Gitlab" + shouldProtectSecrets: "The flag to determine usage rules of secrets to sync. Used by Gitlab", + shouldEnableDelete: "The flag to enable deletion of secrets" } }, UPDATE: { diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 6ae8bf02d..2caae9ec5 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -75,6 +75,7 @@ const envSchema = z .optional() .default(process.env.URL_GITLAB_LOGIN ?? GITLAB_URL) ), // fallback since URL_GITLAB_LOGIN has been renamed + DEFAULT_SAML_ORG_SLUG: zpStr(z.string().optional()).default(process.env.NEXT_PUBLIC_SAML_ORG_SLUG), // integration client secrets // heroku CLIENT_ID_HEROKU: zpStr(z.string().optional()), @@ -131,7 +132,8 @@ const envSchema = z isSecretScanningConfigured: Boolean(data.SECRET_SCANNING_GIT_APP_ID) && Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && - Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET) + Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET), + samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG })); let envCfg: Readonly>; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index cdd3e3bd8..00590386a 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -919,7 +919,8 @@ export const registerRoutes = async ( emailConfigured: z.boolean().optional(), inviteOnlySignup: z.boolean().optional(), redisConfigured: z.boolean().optional(), - secretScanningConfigured: z.boolean().optional() + secretScanningConfigured: z.boolean().optional(), + samlDefaultOrgSlug: z.string().optional() }) } }, @@ -932,7 +933,8 @@ export const registerRoutes = async ( emailConfigured: cfg.isSmtpConfigured, inviteOnlySignup: Boolean(serverCfg.allowSignUp), redisConfigured: cfg.isRedisConfigured, - secretScanningConfigured: cfg.isSecretScanningConfigured + secretScanningConfigured: cfg.isSecretScanningConfigured, + samlDefaultOrgSlug: cfg.samlDefaultOrgSlug }; } }); diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 1c6bad7c7..97a7f4d7a 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -8,7 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { IntegrationMappingBehavior } from "@app/services/integration-auth/integration-list"; +import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema"; import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types"; export const registerIntegrationRouter = async (server: FastifyZodProvider) => { @@ -46,38 +46,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { path: z.string().trim().optional().describe(INTEGRATION.CREATE.path), region: z.string().trim().optional().describe(INTEGRATION.CREATE.region), scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope), - metadata: z - .object({ - secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), - secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), - initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), - mappingBehavior: z - .nativeEnum(IntegrationMappingBehavior) - .optional() - .describe(INTEGRATION.CREATE.metadata.mappingBehavior), - shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), - secretGCPLabel: z - .object({ - labelName: z.string(), - labelValue: z.string() - }) - .optional() - .describe(INTEGRATION.CREATE.metadata.secretGCPLabel), - secretAWSTag: z - .array( - z.object({ - key: z.string(), - value: z.string() - }) - ) - .optional() - .describe(INTEGRATION.CREATE.metadata.secretAWSTag), - kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId), - shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete), - shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets), - shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets) - }) - .default({}) + metadata: IntegrationMetadataSchema.default({}) }), response: { 200: z.object({ @@ -163,33 +132,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment), owner: z.string().trim().describe(INTEGRATION.UPDATE.owner), environment: z.string().trim().describe(INTEGRATION.UPDATE.environment), - metadata: z - .object({ - secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), - secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), - initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), - mappingBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.mappingBehavior), - shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), - secretGCPLabel: z - .object({ - labelName: z.string(), - labelValue: z.string() - }) - .optional() - .describe(INTEGRATION.CREATE.metadata.secretGCPLabel), - secretAWSTag: z - .array( - z.object({ - key: z.string(), - value: z.string() - }) - ) - .optional() - .describe(INTEGRATION.CREATE.metadata.secretAWSTag), - kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId), - shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete) - }) - .optional() + metadata: IntegrationMetadataSchema.optional() }), response: { 200: z.object({ diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 15c80068d..f1d4d51ed 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -31,6 +31,7 @@ import { logger } from "@app/lib/logger"; import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types"; import { TIntegrationDALFactory } from "../integration/integration-dal"; +import { IntegrationMetadataSchema } from "../integration/integration-schema"; import { IntegrationInitialSyncBehavior, IntegrationMappingBehavior, @@ -1363,38 +1364,41 @@ const syncSecretsGitHub = async ({ } } - for await (const encryptedSecret of encryptedSecrets) { - if ( - !(encryptedSecret.name in secrets) && - !(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) && - !(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix)) - ) { - switch (integration.scope) { - case GithubScope.Org: { - await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", { - org: integration.owner as string, - secret_name: encryptedSecret.name - }); - break; - } - case GithubScope.Env: { - await octokit.request( - "DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}", - { - repository_id: Number(integration.appId), - environment_name: integration.targetEnvironmentId as string, + const metadata = IntegrationMetadataSchema.parse(integration.metadata); + if (metadata.shouldEnableDelete) { + for await (const encryptedSecret of encryptedSecrets) { + if ( + !(encryptedSecret.name in secrets) && + !(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) && + !(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix)) + ) { + switch (integration.scope) { + case GithubScope.Org: { + await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", { + org: integration.owner as string, secret_name: encryptedSecret.name - } - ); - break; - } - default: { - await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { - owner: integration.owner as string, - repo: integration.app as string, - secret_name: encryptedSecret.name - }); - break; + }); + break; + } + case GithubScope.Env: { + await octokit.request( + "DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}", + { + repository_id: Number(integration.appId), + environment_name: integration.targetEnvironmentId as string, + secret_name: encryptedSecret.name + } + ); + break; + } + default: { + await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: integration.owner as string, + repo: integration.app as string, + secret_name: encryptedSecret.name + }); + break; + } } } } @@ -2752,6 +2756,20 @@ const syncSecretsCloudflarePages = async ({ } } ); + + const metadata = z.record(z.any()).parse(integration.metadata); + if (metadata.shouldAutoRedeploy) { + await request.post( + `${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}/deployments`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } }; /** diff --git a/backend/src/services/integration/integration-schema.ts b/backend/src/services/integration/integration-schema.ts new file mode 100644 index 000000000..1ea01e56a --- /dev/null +++ b/backend/src/services/integration/integration-schema.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +import { INTEGRATION } from "@app/lib/api-docs"; + +import { IntegrationMappingBehavior } from "../integration-auth/integration-list"; + +export const IntegrationMetadataSchema = z.object({ + secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), + secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), + initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), + mappingBehavior: z + .nativeEnum(IntegrationMappingBehavior) + .optional() + .describe(INTEGRATION.CREATE.metadata.mappingBehavior), + shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), + secretGCPLabel: z + .object({ + labelName: z.string(), + labelValue: z.string() + }) + .optional() + .describe(INTEGRATION.CREATE.metadata.secretGCPLabel), + secretAWSTag: z + .array( + z.object({ + key: z.string(), + value: z.string() + }) + ) + .optional() + .describe(INTEGRATION.CREATE.metadata.secretAWSTag), + kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId), + shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete), + shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete), + shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets), + shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets) +}); diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts index b96bb6ef3..abbccbe90 100644 --- a/backend/src/services/integration/integration-types.ts +++ b/backend/src/services/integration/integration-types.ts @@ -31,6 +31,7 @@ export type TCreateIntegrationDTO = { shouldDisableDelete?: boolean; shouldMaskSecrets?: boolean; shouldProtectSecrets?: boolean; + shouldEnableDelete?: boolean; }; } & Omit; @@ -56,6 +57,7 @@ export type TUpdateIntegrationDTO = { }[]; kmsKeyId?: string; shouldDisableDelete?: boolean; + shouldEnableDelete?: boolean; }; } & Omit; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 24d901867..97d2b29d6 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -77,7 +77,7 @@ export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsSe // This will switch to a seal process and HMS flow in future const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; // if root key its base64 encoded - const isBase64 = Boolean(appCfg.ROOT_ENCRYPTION_KEY); + const isBase64 = !appCfg.ENCRYPTION_KEY; if (!encryptionKey) throw new Error("Root encryption key not found for KMS service."); const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index 3d1b72331..41a41726c 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -496,7 +496,6 @@ To enable auto redeployment you simply have to add the following annotation to t ```yaml secrets.infisical.com/auto-reload: "true" ``` - ```yaml apiVersion: apps/v1 @@ -527,7 +526,11 @@ spec: - containerPort: 80 ``` - + + #### How it works + When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. + Then, for each deployment that has this annotation present, a rolling update will be triggered. + ## Global configuration To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap. diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 5233ae910..33e6c697c 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -318,6 +318,11 @@ SMTP_FROM_NAME=Infisical By default, users can only login via email/password based login method. To login into Infisical with OAuth providers such as Google, configure the associated variables. + + + When set, all visits to the Infisical login page will automatically redirect users of your Infisical instance to the SAML identity provider associated with the specified organization slug. + + Follow detailed guide to configure [Google SSO](/documentation/platform/sso/google) @@ -369,11 +374,6 @@ To login into Infisical with OAuth providers such as Google, configure the assoc information. - - Configure SAML organization slug to automatically redirect all users of your - Infisical instance to the identity provider. - - ## Native secret integrations To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box. diff --git a/frontend/scripts/initialize-standalone-build.sh b/frontend/scripts/initialize-standalone-build.sh index 859814eda..d9138bb77 100755 --- a/frontend/scripts/initialize-standalone-build.sh +++ b/frontend/scripts/initialize-standalone-build.sh @@ -4,8 +4,6 @@ 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/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 2036f6718..81d0f00ca 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -75,6 +75,7 @@ export const useCreateIntegration = () => { shouldDisableDelete?: boolean; shouldMaskSecrets?: boolean; shouldProtectSecrets?: boolean; + shouldEnableDelete?: boolean; }; }) => { const { diff --git a/frontend/src/hooks/api/serverDetails/types.ts b/frontend/src/hooks/api/serverDetails/types.ts index 80d34a150..911526404 100644 --- a/frontend/src/hooks/api/serverDetails/types.ts +++ b/frontend/src/hooks/api/serverDetails/types.ts @@ -4,4 +4,5 @@ export type ServerStatus = { emailConfigured: boolean; secretScanningConfigured: boolean; redisConfigured: boolean; + samlDefaultOrgSlug: boolean }; diff --git a/frontend/src/pages/integrations/cloudflare-pages/create.tsx b/frontend/src/pages/integrations/cloudflare-pages/create.tsx index 570b2b83a..d7cb6bdab 100644 --- a/frontend/src/pages/integrations/cloudflare-pages/create.tsx +++ b/frontend/src/pages/integrations/cloudflare-pages/create.tsx @@ -7,7 +7,15 @@ import { createNotification } from "@app/components/notifications"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api"; -import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2"; +import { + Button, + Card, + CardTitle, + FormControl, + Select, + SelectItem, + Switch +} from "../../../components/v2"; import { useGetIntegrationAuthApps, useGetIntegrationAuthById @@ -34,6 +42,7 @@ export default function CloudflarePagesIntegrationPage() { const [targetApp, setTargetApp] = useState(""); const [targetAppId, setTargetAppId] = useState(""); const [targetEnvironment, setTargetEnvironment] = useState(""); + const [shouldAutoRedeploy, setShouldAutoRedeploy] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -69,7 +78,10 @@ export default function CloudflarePagesIntegrationPage() { appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, targetEnvironment, - secretPath + secretPath, + metadata: { + shouldAutoRedeploy + } }); setIsLoading(false); @@ -169,6 +181,15 @@ export default function CloudflarePagesIntegrationPage() { ))} +
+ setShouldAutoRedeploy(isChecked)} + isChecked={shouldAutoRedeploy} + > + Auto-redeploy service upon secret change + +