From 60749cfc43a7b415f8b52031c04f0f04565c4140 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 9 Jan 2025 02:19:24 +0800 Subject: [PATCH 01/13] misc: made is active optional for integration patch --- backend/src/server/routes/v1/integration-router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 059d244638..f0c28478cc 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -131,7 +131,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { body: z.object({ app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app), appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId), - isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive), + isActive: z.boolean().optional().describe(INTEGRATION.UPDATE.isActive), secretPath: z .string() .trim() From 02dc23425c61238ad1b637ab9743232a230cf56c Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 10 Jan 2025 02:29:39 +0800 Subject: [PATCH 02/13] fix: address infinite hang when infisical run watch fails --- cli/packages/cmd/run.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index a232896f1e..7f11a3f95e 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -419,22 +419,23 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInt for { <-recheckSecretsChannel - watchMutex.Lock() - - newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token) - if err != nil { - log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets") - continue - } - - if newEnvironmentVariables.ETag != currentETag { - runCommandWithWatcher(newEnvironmentVariables) - } else { - log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process") - } + func() { + watchMutex.Lock() + defer watchMutex.Unlock() + + newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token) + if err != nil { + log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets") + return + } - watchMutex.Unlock() + if newEnvironmentVariables.ETag != currentETag { + runCommandWithWatcher(newEnvironmentVariables) + } else { + log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process") + } + }() } } From e741b63e631d9bc639805ff6c9c553c0fb2b91f9 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 10 Jan 2025 14:59:49 +0530 Subject: [PATCH 03/13] fix: resolved mssql self signed error --- .../secret-rotation-queue/secret-rotation-queue.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index 015cce0e5e..e31e2f2a41 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -180,6 +180,8 @@ export const secretRotationQueueFactory = ({ provider.template.client === TDbProviderClients.MsSqlServer ? ({ encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT, + // when ca is provided use that + trustServerCertificate: ca ? false : true, cryptoCredentialsDetails: ca ? { ca } : {} } as Record) : undefined; From 3c89a69410108850fab857d1cb36837d9b74aef3 Mon Sep 17 00:00:00 2001 From: Vlad Matsiiako <78047717+vmatsiiako@users.noreply.github.com> Date: Fri, 10 Jan 2025 01:39:12 -0800 Subject: [PATCH 04/13] Update infisical-dynamic-secret-crd.mdx --- .../platforms/kubernetes/infisical-dynamic-secret-crd.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx index 9bdb43e6a6..4f543e9590 100644 --- a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -11,7 +11,7 @@ This means any Pod, Deployment, or other Kubernetes resource can make use of dyn This CRD offers the following features: - **Generate a dynamic secret lease** in Infisical and track its lifecycle. - **Write** the dynamic secret from Infisical to your cluster as native Kubernetes secret. -- **Automatically rotate** the dyanmic secret value before it expires to make sure your cluster always has valid credentials. +- **Automatically rotate** the dynamic secret value before it expires to make sure your cluster always has valid credentials. - **Optionally trigger redeployments** of any workloads that consume the secret if you enable auto-reload. ### Prerequisites From 4d43accc8a8c1270e649c84214590d07c6483f8e Mon Sep 17 00:00:00 2001 From: = Date: Fri, 10 Jan 2025 15:12:04 +0530 Subject: [PATCH 05/13] fix: did same resolution for dynamic secret ops as well --- .../dynamic-secret/providers/sql-database.ts | 13 ++++++++++++- .../secret-rotation-queue/secret-rotation-queue.ts | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index 6c9bffd0ac..a9bfad525b 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -34,6 +34,8 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { const $getClient = async (providerInputs: z.infer) => { const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined; + const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL; + const db = knex({ client: providerInputs.client, connection: { @@ -43,7 +45,16 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { user: providerInputs.username, password: providerInputs.password, ssl, - pool: { min: 0, max: 1 } + pool: { min: 0, max: 1 }, + // @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver + // https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66 + // https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19 + options: isMsSQLClient + ? { + trustServerCertificate: !providerInputs.ca, + cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {} + } + : undefined }, acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT }); diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index e31e2f2a41..355507ecfe 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -181,7 +181,7 @@ export const secretRotationQueueFactory = ({ ? ({ encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT, // when ca is provided use that - trustServerCertificate: ca ? false : true, + trustServerCertificate: !ca, cryptoCredentialsDetails: ca ? { ca } : {} } as Record) : undefined; From 612c29225dafbc99918df210c126610083a3a19f Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 10 Jan 2025 19:06:10 +0800 Subject: [PATCH 06/13] misc: add pagination handling for gitlab groups fetch --- .../integration-auth/integration-team.ts | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/backend/src/services/integration-auth/integration-team.ts b/backend/src/services/integration-auth/integration-team.ts index c39b2c44f5..ccbbb72e7f 100644 --- a/backend/src/services/integration-auth/integration-team.ts +++ b/backend/src/services/integration-auth/integration-team.ts @@ -1,3 +1,5 @@ +import { AxiosResponse } from "axios"; + import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; @@ -11,19 +13,27 @@ const getTeamsGitLab = async ({ url, accessToken }: { url: string; accessToken: const gitLabApiUrl = url ? `${url}/api` : IntegrationUrls.GITLAB_API_URL; let teams: Team[] = []; - const res = ( - await request.get<{ name: string; id: string }[]>(`${gitLabApiUrl}/v4/groups`, { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" + let page: number | undefined = 1; + while (page > 0) { + // eslint-disable-next-line no-await-in-loop + const { data, headers }: AxiosResponse<{ name: string; id: string }[]> = await request.get( + `${gitLabApiUrl}/v4/groups?page=${page}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } } - }) - ).data; + ); - teams = res.map((t) => ({ - name: t.name, - id: t.id.toString() - })); + page = Number(headers["x-next-page"]); + teams = teams.concat( + data.map((t) => ({ + name: t.name, + id: t.id.toString() + })) + ); + } return teams; }; From 4bc9bca2879720b80af128dcb4c7505f14429f45 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 10 Jan 2025 19:08:49 +0800 Subject: [PATCH 07/13] removed undefined type --- backend/src/services/integration-auth/integration-team.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/services/integration-auth/integration-team.ts b/backend/src/services/integration-auth/integration-team.ts index ccbbb72e7f..d738625e23 100644 --- a/backend/src/services/integration-auth/integration-team.ts +++ b/backend/src/services/integration-auth/integration-team.ts @@ -13,7 +13,7 @@ const getTeamsGitLab = async ({ url, accessToken }: { url: string; accessToken: const gitLabApiUrl = url ? `${url}/api` : IntegrationUrls.GITLAB_API_URL; let teams: Team[] = []; - let page: number | undefined = 1; + let page: number = 1; while (page > 0) { // eslint-disable-next-line no-await-in-loop const { data, headers }: AxiosResponse<{ name: string; id: string }[]> = await request.get( @@ -26,7 +26,7 @@ const getTeamsGitLab = async ({ url, accessToken }: { url: string; accessToken: } ); - page = Number(headers["x-next-page"]); + page = Number(headers["x-next-page"] ?? ""); teams = teams.concat( data.map((t) => ({ name: t.name, From 502429d914f7a24ddbf834dda1b8e9c2f3b24d4d Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 10 Jan 2025 23:55:21 +0800 Subject: [PATCH 08/13] misc: resolve shared secret within org and added login redirect --- .../secret-sharing/secret-sharing-service.ts | 7 ++- frontend/src/const.ts | 3 +- frontend/src/hooks/api/auth/queries.tsx | 16 +++++++ .../ViewSharedSecretByIDPage.tsx | 47 +++++++++++++++++-- .../public/ViewSharedSecretByIDPage/route.tsx | 13 ++++- 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 171ab54db5..b262cad5d5 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -206,8 +206,13 @@ export const secretSharingServiceFactory = ({ const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; - if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) + if (accessType === SecretSharingAccessType.Organization && orgId === undefined) { + throw new UnauthorizedError(); + } + + if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) { throw new ForbiddenRequestError(); + } // all secrets pass through here, meaning we check if its expired first and then check if it needs verification // or can be safely sent to the client. diff --git a/frontend/src/const.ts b/frontend/src/const.ts index d2623431d3..797254893c 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -58,7 +58,8 @@ export const leaveConfirmDefaultMessage = "Your changes will be lost if you leave the page. Are you sure you want to continue?"; export enum SessionStorageKeys { - CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN" + CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN", + ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL" } export const secretTagsColors = [ diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 07baf011ea..8d8ee0c3f4 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { apiRequest } from "@app/config/request"; +import { SessionStorageKeys } from "@app/const"; import { organizationKeys } from "../organization/queries"; import { setAuthToken } from "../reactQuery"; @@ -86,6 +87,21 @@ export const useSelectOrganization = () => { SecurityClient.setProviderAuthToken(""); } + if (data.token && !data.isMfaEnabled) { + // We check if there is a pending callback after organization login success and redirect to it if valid + const loginRedirectInfo = sessionStorage.getItem( + SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL + ); + sessionStorage.removeItem(SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL); + + if (loginRedirectInfo) { + const { expiry, data: redirectUrl } = JSON.parse(loginRedirectInfo); + if (new Date() < new Date(expiry)) { + window.location.assign(redirectUrl); + } + } + } + return data; }, onSuccess: () => { diff --git a/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx b/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx index 387982fbde..5112bd47ed 100644 --- a/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx +++ b/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx @@ -1,10 +1,13 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Helmet } from "react-helmet"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useParams, useSearch } from "@tanstack/react-router"; +import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { AxiosError } from "axios"; +import { addSeconds, formatISO } from "date-fns"; +import { createNotification } from "@app/components/notifications"; +import { SessionStorageKeys } from "@app/const"; import { ROUTE_PATHS } from "@app/const/routes"; import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; @@ -36,7 +39,6 @@ export const ViewSharedSecretByIDPage = () => { select: (el) => el.key }); const [password, setPassword] = useState(); - const { hashedHex, key } = extractDetailsFromUrl(urlEncodedKey); const { @@ -50,10 +52,47 @@ export const ViewSharedSecretByIDPage = () => { password }); + const navigate = useNavigate(); + + const isUnauthorized = + ((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 401; + + const isForbidden = + ((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 403; + const isInvalidCredential = ((error as AxiosError)?.response?.data as { message: string })?.message === "Invalid credentials"; + useEffect(() => { + if (isUnauthorized && !isInvalidCredential) { + // persist current URL in session storage so that we can come back to this after successful login + sessionStorage.setItem( + SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL, + JSON.stringify({ + expiry: formatISO(addSeconds(new Date(), 60)), + data: window.location.href + }) + ); + + createNotification({ + type: "info", + text: "Login is required in order to access the shared secret." + }); + + navigate({ + to: "/login" + }); + } + + if (isForbidden) { + createNotification({ + type: "error", + text: "You do not have access to this shared secret." + }); + } + }, [error]); + const shouldShowPasswordPrompt = isInvalidCredential || (fetchSecret?.isPasswordProtected && !fetchSecret.secret); const isValidatingPassword = Boolean(password) && isFetching; @@ -111,7 +150,7 @@ export const ViewSharedSecretByIDPage = () => { {!error && fetchSecret?.secret && ( )} - {error && !isInvalidCredential && } + {error && !isInvalidCredential && !isUnauthorized && } )}
diff --git a/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx b/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx index f503685032..7b98dded34 100644 --- a/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx +++ b/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries"; + import { ViewSharedSecretByIDPage } from "./ViewSharedSecretByIDPage"; const SharedSecretByIDPageQuerySchema = z.object({ @@ -9,9 +11,18 @@ const SharedSecretByIDPageQuerySchema = z.object({ }); export const Route = createFileRoute("/shared/secret/$secretId")({ - component: ViewSharedSecretByIDPage, validateSearch: zodValidator(SharedSecretByIDPageQuerySchema), + component: ViewSharedSecretByIDPage, search: { middlewares: [stripSearchParams({ key: "" })] + }, + beforeLoad: async ({ context }) => { + // we load the auth token because the view shared secret screen serves both public and authenticated users + await context.queryClient + .ensureQueryData({ + queryKey: authKeys.getAuthToken, + queryFn: fetchAuthToken + }) + .catch(() => undefined); } }); From 36a5f728a1031c7aad31c06e75bb27a6b0ed9277 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 11 Jan 2025 00:00:17 +0800 Subject: [PATCH 09/13] misc: add secret key indicator for failed AWS integration syncs --- .../integration-auth/integration-sync-secret.ts | 5 ++++- backend/src/services/secret/secret-queue.ts | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 1446bb44ff..8d9ca0d3ac 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -1289,7 +1289,10 @@ const syncSecretsAWSSecretManager = async ({ if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) { for await (const [key, value] of Object.entries(secrets)) { - await processAwsSecret(key, value.value, value.secretMetadata); + await processAwsSecret(key, value.value, value.secretMetadata).catch((error) => { + error.secretKey = key; + throw error; + }); } } else { await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets)); diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 8bb1e03d2d..661da23628 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -971,6 +971,8 @@ export const secretQueueFactory = ({ }); } + const { secretKey } = (err as { secretKey: string }) || {}; + const message = // eslint-disable-next-line no-nested-ternary (err instanceof AxiosError @@ -979,6 +981,8 @@ export const secretQueueFactory = ({ : err?.message : (err as Error)?.message) || "Unknown error occurred."; + const errorLog = `${secretKey ? `[Secret Key: ${secretKey}] ` : ""}${message}`; + await auditLogService.createAuditLog({ projectId, actor: await $generateActor(actorId, isManual), @@ -989,7 +993,7 @@ export const secretQueueFactory = ({ isSynced: false, lastSyncJobId: job?.id ?? "", lastUsed: new Date(), - syncMessage: message + syncMessage: errorLog } } }); @@ -1001,13 +1005,13 @@ export const secretQueueFactory = ({ await integrationDAL.updateById(integration.id, { lastSyncJobId: job.id, - syncMessage: message, + syncMessage: errorLog, isSynced: false }); integrationsFailedToSync.push({ integrationId: integration.id, - syncMessage: message + syncMessage: errorLog }); } } From 9bdff9c5045654b6862f8a66088f1b0295b27838 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 10 Jan 2025 17:15:14 +0100 Subject: [PATCH 10/13] fix: explicit postgres time conversion --- backend/src/ee/services/audit-log/audit-log-dal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index b2c80aa0b5..bcef06e100 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -100,10 +100,10 @@ export const auditLogDALFactory = (db: TDbClient) => { // Filter by date range if (startDate) { - void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate); + void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]); } if (endDate) { - void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate); + void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" <= ?::timestamptz`, [endDate]); } // we timeout long running queries to prevent DB resource issues (2 minutes) From 17249d603b2b3ae0db325b690de6b268aa27d95d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 10 Jan 2025 17:15:26 +0100 Subject: [PATCH 11/13] fix: add delete secrets event type to frontend --- frontend/src/hooks/api/auditLogs/constants.tsx | 1 + frontend/src/hooks/api/auditLogs/enums.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index a75767108c..fd764f1f5d 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -3,6 +3,7 @@ import { EventType, UserAgentType } from "./enums"; export const eventToNameMap: { [K in EventType]: string } = { [EventType.GET_SECRETS]: "List secrets", [EventType.GET_SECRET]: "Read secret", + [EventType.DELETE_SECRETS]: "Delete secrets", [EventType.CREATE_SECRET]: "Create secret", [EventType.UPDATE_SECRET]: "Update secret", [EventType.DELETE_SECRET]: "Delete secret", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 0b0c44d7b0..5e8adf5b13 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -17,6 +17,7 @@ export enum UserAgentType { export enum EventType { GET_SECRETS = "get-secrets", + DELETE_SECRETS = "delete-secrets", GET_SECRET = "get-secret", CREATE_SECRET = "create-secret", UPDATE_SECRET = "update-secret", From 2baacfcd8f38848685050e1ea7e46a726368f107 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 11 Jan 2025 02:07:04 +0800 Subject: [PATCH 12/13] misc: add support for array values --- .../services/identity-oidc-auth/identity-oidc-auth-fns.ts | 8 ++++++++ .../identity-oidc-auth/identity-oidc-auth-service.ts | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts index c6d65d836a..7d386afcb0 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts @@ -2,3 +2,11 @@ import picomatch from "picomatch"; export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) => policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue); + +export const doesAudValueMatchOidcPolicy = (fieldValue: string | string[], policyValue: string) => { + if (Array.isArray(fieldValue)) { + return fieldValue.some((entry) => entry === policyValue || picomatch.isMatch(entry, policyValue)); + } + + return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue); +}; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 7949e94666..dc3b1baa3f 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -27,7 +27,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TOrgBotDALFactory } from "../org/org-bot-dal"; import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; -import { doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; +import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; import { TAttachOidcAuthDTO, TGetOidcAuthDTO, @@ -148,7 +148,7 @@ export const identityOidcAuthServiceFactory = ({ if ( !identityOidcAuth.boundAudiences .split(", ") - .some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue)) + .some((policyValue) => doesAudValueMatchOidcPolicy(tokenData.aud, policyValue)) ) { throw new UnauthorizedError({ message: "Access denied: OIDC audience not allowed." From 3d278b0925caa803164a5ae742b019ebdbd0244b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 10 Jan 2025 21:37:10 +0100 Subject: [PATCH 13/13] feat(audit-logs): shared secrets audit logs --- .../ee/services/audit-log/audit-log-types.ts | 48 +++++++++++++++++-- backend/src/server/plugins/audit-log.ts | 10 +++- .../server/routes/v1/secret-sharing-router.ts | 45 +++++++++++++++++ backend/src/services/auth/auth-type.ts | 3 +- .../src/hooks/api/auditLogs/constants.tsx | 5 +- frontend/src/hooks/api/auditLogs/enums.tsx | 8 +++- frontend/src/hooks/api/auditLogs/types.tsx | 6 ++- .../AuditLogsPage/components/LogsFilter.tsx | 10 +--- .../AuditLogsPage/components/LogsTableRow.tsx | 16 ++++++- 9 files changed, 133 insertions(+), 18 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index a8d98977f3..2a5657cf0c 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -31,7 +31,7 @@ export type TListProjectAuditLogDTO = { export type TCreateAuditLogDTO = { event: Event; - actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor; + actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor | UnknownUserActor; orgId?: string; projectId?: string; } & BaseAuthData; @@ -229,7 +229,10 @@ export enum EventType { GET_APP_CONNECTION = "get-app-connection", CREATE_APP_CONNECTION = "create-app-connection", UPDATE_APP_CONNECTION = "update-app-connection", - DELETE_APP_CONNECTION = "delete-app-connection" + DELETE_APP_CONNECTION = "delete-app-connection", + CREATE_SHARED_SECRET = "create-shared-secret", + DELETE_SHARED_SECRET = "delete-shared-secret", + READ_SHARED_SECRET = "read-shared-secret" } interface UserActorMetadata { @@ -252,6 +255,8 @@ interface ScimClientActorMetadata {} interface PlatformActorMetadata {} +interface UnknownUserActorMetadata {} + export interface UserActor { type: ActorType.USER; metadata: UserActorMetadata; @@ -267,6 +272,11 @@ export interface PlatformActor { metadata: PlatformActorMetadata; } +export interface UnknownUserActor { + type: ActorType.UNKNOWN_USER; + metadata: UnknownUserActorMetadata; +} + export interface IdentityActor { type: ActorType.IDENTITY; metadata: IdentityActorMetadata; @@ -1907,6 +1917,35 @@ interface DeleteAppConnectionEvent { }; } +interface CreateSharedSecretEvent { + type: EventType.CREATE_SHARED_SECRET; + metadata: { + id: string; + accessType: string; + name?: string; + expiresAfterViews?: number; + usingPassword: boolean; + expiresAt: string; + }; +} + +interface DeleteSharedSecretEvent { + type: EventType.DELETE_SHARED_SECRET; + metadata: { + id: string; + name?: string; + }; +} + +interface ReadSharedSecretEvent { + type: EventType.READ_SHARED_SECRET; + metadata: { + id: string; + name?: string; + accessType: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -2083,4 +2122,7 @@ export type Event = | GetAppConnectionEvent | CreateAppConnectionEvent | UpdateAppConnectionEvent - | DeleteAppConnectionEvent; + | DeleteAppConnectionEvent + | CreateSharedSecretEvent + | DeleteSharedSecretEvent + | ReadSharedSecretEvent; diff --git a/backend/src/server/plugins/audit-log.ts b/backend/src/server/plugins/audit-log.ts index 3f49778e85..3b02b15280 100644 --- a/backend/src/server/plugins/audit-log.ts +++ b/backend/src/server/plugins/audit-log.ts @@ -32,13 +32,21 @@ export const getUserAgentType = (userAgent: string | undefined) => { export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => { server.decorateRequest("auditLogInfo", null); server.addHook("onRequest", async (req) => { - if (!req.auth) return; const userAgent = req.headers["user-agent"] ?? ""; const payload = { ipAddress: req.realIp, userAgent, userAgentType: getUserAgentType(userAgent) } as typeof req.auditLogInfo; + + if (!req.auth) { + payload.actor = { + type: ActorType.UNKNOWN_USER, + metadata: {} + }; + req.auditLogInfo = payload; + return; + } if (req.auth.actor === ActorType.USER) { payload.actor = { type: ActorType.USER, diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 3363cc6c0a..59e59ede78 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { SecretSharingSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { SecretSharingAccessType } from "@app/lib/types"; import { publicEndpointLimit, @@ -88,6 +89,21 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => orgId: req.permission?.orgId }); + if (sharedSecret.secret?.orgId) { + await server.services.auditLog.createAuditLog({ + orgId: sharedSecret.secret.orgId, + ...req.auditLogInfo, + event: { + type: EventType.READ_SHARED_SECRET, + metadata: { + id: req.params.id, + name: sharedSecret.secret.name || undefined, + accessType: sharedSecret.secret.accessType + } + } + }); + } + return sharedSecret; } }); @@ -151,6 +167,23 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + orgId: req.permission.orgId, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SHARED_SECRET, + metadata: { + accessType: req.body.accessType, + expiresAt: req.body.expiresAt, + expiresAfterViews: req.body.expiresAfterViews, + name: req.body.name, + id: sharedSecret.id, + usingPassword: !!req.body.password + } + } + }); + return { id: sharedSecret.id }; } }); @@ -181,6 +214,18 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => sharedSecretId }); + await server.services.auditLog.createAuditLog({ + orgId: req.permission.orgId, + ...req.auditLogInfo, + event: { + type: EventType.DELETE_SHARED_SECRET, + metadata: { + id: sharedSecretId, + name: deletedSharedSecret.name || undefined + } + } + }); + return { ...deletedSharedSecret }; } }); diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index c1bf2b6fbd..05412a73a0 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -39,7 +39,8 @@ export enum ActorType { // would extend to AWS, Azure, ... SERVICE = "service", IDENTITY = "identity", Machine = "machine", - SCIM_CLIENT = "scimClient" + SCIM_CLIENT = "scimClient", + UNKNOWN_USER = "unknownUser" } // This will be null unless the token-type is JWT diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index fd764f1f5d..650798bc7f 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -82,7 +82,10 @@ export const eventToNameMap: { [K in EventType]: string } = { "Update certificate template EST configuration", [EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration", [EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration", - [EventType.INTEGRATION_SYNCED]: "Integration sync" + [EventType.INTEGRATION_SYNCED]: "Integration sync", + [EventType.CREATE_SHARED_SECRET]: "Create shared secret", + [EventType.DELETE_SHARED_SECRET]: "Delete shared secret", + [EventType.READ_SHARED_SECRET]: "Read shared secret" }; export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 5e8adf5b13..c2c306a965 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -2,7 +2,8 @@ export enum ActorType { PLATFORM = "platform", USER = "user", SERVICE = "service", - IDENTITY = "identity" + IDENTITY = "identity", + UNKNOWN_USER = "unknownUser" } export enum UserAgentType { @@ -95,5 +96,8 @@ export enum EventType { GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", - INTEGRATION_SYNCED = "integration-synced" + INTEGRATION_SYNCED = "integration-synced", + CREATE_SHARED_SECRET = "create-shared-secret", + DELETE_SHARED_SECRET = "delete-shared-secret", + READ_SHARED_SECRET = "read-shared-secret" } diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 62cf1b823a..3a5070ef54 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -50,7 +50,11 @@ export interface PlatformActor { metadata: object; } -export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor; +export interface UnknownUserActor { + type: ActorType.UNKNOWN_USER; +} + +export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor | UnknownUserActor; interface GetSecretsEvent { type: EventType.GET_SECRETS; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index ba7457128c..3a21958aea 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-nested-ternary */ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form"; import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -49,7 +49,6 @@ export const LogsFilter = ({ isOrgAuditLogs, className, control, - setValue, reset, watch }: Props) => { @@ -63,12 +62,6 @@ export const LogsFilter = ({ const { data, isPending } = useGetAuditLogActorFilterOpts(workspaces?.[0]?.id ?? ""); - useEffect(() => { - if (workspacesInOrg.length) { - setValue("project", workspacesInOrg[0]); - } - }, [workspaces]); - const renderActorSelectItem = (actor: Actor) => { switch (actor.type) { case ActorType.USER: @@ -129,6 +122,7 @@ export const LogsFilter = ({ > ({ name, id }))} diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx index 4da98613e0..5e663f2834 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx @@ -1,4 +1,7 @@ -import { Td, Tr } from "@app/components/v2"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Td, Tooltip, Tr } from "@app/components/v2"; import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants"; import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums"; import { Actor, AuditLog } from "@app/hooks/api/auditLogs/types"; @@ -37,6 +40,17 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop

Machine Identity

); + case ActorType.UNKNOWN_USER: + return ( + +
+

Unknown User

+ + + +
+ + ); default: return ; }