From fb2b64cb19bed0ad69e4e066293fc564af40443e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 12 May 2025 15:19:42 +0400 Subject: [PATCH 1/9] feat(identities/k8s): gateway support --- ...103022_identity-kubernetes-auth-gateway.ts | 25 ++++ .../db/schemas/identity-kubernetes-auths.ts | 3 +- .../ee/services/gateway/gateway-service.ts | 20 ++- backend/src/lib/api-docs/constants.ts | 2 + backend/src/lib/gateway/index.ts | 60 ++++++-- backend/src/server/routes/index.ts | 22 +-- .../v1/identity-kubernetes-auth-router.ts | 5 +- .../identity-kubernetes-auth-service.ts | 140 +++++++++++++----- .../identity-kubernetes-auth-types.ts | 2 + .../src/hooks/api/identities/mutations.tsx | 12 +- frontend/src/hooks/api/identities/types.ts | 3 + .../IdentityKubernetesAuthForm.tsx | 47 ++++++ 12 files changed, 267 insertions(+), 74 deletions(-) create mode 100644 backend/src/db/migrations/20250512103022_identity-kubernetes-auth-gateway.ts diff --git a/backend/src/db/migrations/20250512103022_identity-kubernetes-auth-gateway.ts b/backend/src/db/migrations/20250512103022_identity-kubernetes-auth-gateway.ts new file mode 100644 index 000000000..fcd9bfc3e --- /dev/null +++ b/backend/src/db/migrations/20250512103022_identity-kubernetes-auth-gateway.ts @@ -0,0 +1,25 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId"); + + if (!hasGatewayIdColumn) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => { + table.uuid("gatewayId").nullable(); + table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId"); + + if (hasGatewayIdColumn) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => { + table.dropForeign("gatewayId"); + table.dropColumn("gatewayId"); + }); + } +} diff --git a/backend/src/db/schemas/identity-kubernetes-auths.ts b/backend/src/db/schemas/identity-kubernetes-auths.ts index 448cec386..3c9dd400c 100644 --- a/backend/src/db/schemas/identity-kubernetes-auths.ts +++ b/backend/src/db/schemas/identity-kubernetes-auths.ts @@ -29,7 +29,8 @@ export const IdentityKubernetesAuthsSchema = z.object({ allowedNames: z.string(), allowedAudience: z.string(), encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(), - encryptedKubernetesCaCertificate: zodBuffer.nullable().optional() + encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(), + gatewayId: z.string().uuid().nullable().optional() }); export type TIdentityKubernetesAuths = z.infer; diff --git a/backend/src/ee/services/gateway/gateway-service.ts b/backend/src/ee/services/gateway/gateway-service.ts index 5a17bc028..141797c98 100644 --- a/backend/src/ee/services/gateway/gateway-service.ts +++ b/backend/src/ee/services/gateway/gateway-service.ts @@ -590,13 +590,7 @@ export const gatewayServiceFactory = ({ return gateways; }; - // this has no permission check and used for dynamic secrets directly - // assumes permission check is already done - const fnGetGatewayClientTls = async (projectGatewayId: string) => { - const projectGateway = await projectGatewayDAL.findById(projectGatewayId); - if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` }); - - const { gatewayId } = projectGateway; + const fnGetGatewayClientTlsByGatewayId = async (gatewayId: string) => { const gateway = await gatewayDAL.findById(gatewayId); if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` }); @@ -638,6 +632,17 @@ export const gatewayServiceFactory = ({ }; }; + // this has no permission check and used for dynamic secrets directly + // assumes permission check is already done + const fnGetGatewayClientTls = async (projectGatewayId: string) => { + const projectGateway = await projectGatewayDAL.findById(projectGatewayId); + if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` }); + + const gatewayDetails = await fnGetGatewayClientTlsByGatewayId(projectGateway.gatewayId); + + return gatewayDetails; + }; + return { getGatewayRelayDetails, exchangeAllocatedRelayAddress, @@ -647,6 +652,7 @@ export const gatewayServiceFactory = ({ deleteGatewayById, getProjectGateways, fnGetGatewayClientTls, + fnGetGatewayClientTlsByGatewayId, heartbeat }; }; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 6805575f2..d09760d8c 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -357,6 +357,7 @@ export const KUBERNETES_AUTH = { allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.", allowedAudience: "The optional audience claim that the service account JWT token must have to authenticate with Infisical.", + gatewayId: "The ID of the gateway to use when performing kubernetes API requests.", accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", accessTokenTTL: "The lifetime for an access token in seconds.", accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", @@ -373,6 +374,7 @@ export const KUBERNETES_AUTH = { allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.", allowedAudience: "The new optional audience claim that the service account JWT token must have to authenticate with Infisical.", + gatewayId: "The ID of the gateway to use when performing kubernetes API requests.", accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", accessTokenTTL: "The new lifetime for an acccess token in seconds.", accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.", diff --git a/backend/src/lib/gateway/index.ts b/backend/src/lib/gateway/index.ts index 84d801dda..7a94c6384 100644 --- a/backend/src/lib/gateway/index.ts +++ b/backend/src/lib/gateway/index.ts @@ -174,6 +174,8 @@ const setupProxyServer = async ({ return new Promise((resolve, reject) => { const server = net.createServer(); + let streamClosed = false; + // eslint-disable-next-line @typescript-eslint/no-misused-promises server.on("connection", async (clientConn) => { try { @@ -202,9 +204,15 @@ const setupProxyServer = async ({ // Handle client connection close clientConn.on("end", () => { - writer.close().catch((err) => { - logger.error(err); - }); + if (!streamClosed) { + try { + writer.close().catch((err) => { + logger.debug(err, "Error closing writer (already closed)"); + }); + } catch (error) { + logger.debug(error, "Error in writer close"); + } + } }); clientConn.on("error", (clientConnErr) => { @@ -249,14 +257,29 @@ const setupProxyServer = async ({ setupCopy(); // Handle connection closure clientConn.on("close", () => { - stream.destroy().catch((err) => { - proxyErrorMsg.push((err as Error)?.message); - }); + if (!streamClosed) { + streamClosed = true; + stream.destroy().catch((err) => { + logger.debug(err, "Stream already destroyed during close event"); + }); + } }); const cleanup = async () => { - clientConn?.destroy(); - await stream.destroy(); + try { + clientConn?.destroy(); + } catch (err) { + logger.debug(err, "Error destroying client connection"); + } + + if (!streamClosed) { + streamClosed = true; + try { + await stream.destroy(); + } catch (err) { + logger.debug(err, "Error destroying stream (might be already closed)"); + } + } }; clientConn.on("error", (clientConnErr) => { @@ -301,8 +324,17 @@ const setupProxyServer = async ({ server, port: address.port, cleanup: async () => { - server.close(); - await quicClient?.destroy(); + try { + server.close(); + } catch (err) { + logger.debug(err, "Error closing server"); + } + + try { + await quicClient?.destroy(); + } catch (err) { + logger.debug(err, "Error destroying QUIC client"); + } }, getProxyError: () => proxyErrorMsg.join(",") }); @@ -320,10 +352,10 @@ interface ProxyOptions { orgId: string; } -export const withGatewayProxy = async ( - callback: (port: number) => Promise, +export const withGatewayProxy = async ( + callback: (port: number) => Promise, options: ProxyOptions -): Promise => { +): Promise => { const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options; // Setup the proxy server @@ -339,7 +371,7 @@ export const withGatewayProxy = async ( try { // Execute the callback with the allocated port - await callback(port); + return await callback(port); } catch (err) { const proxyErrorMessage = getProxyError(); if (proxyErrorMessage) { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index cb9931ec2..6367496f1 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1401,12 +1401,24 @@ export const registerRoutes = async ( identityUaDAL, licenseService }); + + const gatewayService = gatewayServiceFactory({ + permissionService, + gatewayDAL, + kmsService, + licenseService, + orgGatewayConfigDAL, + keyStore, + projectGatewayDAL + }); + const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ identityKubernetesAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, permissionService, licenseService, + gatewayService, kmsService }); const identityGcpAuthService = identityGcpAuthServiceFactory({ @@ -1461,16 +1473,6 @@ export const registerRoutes = async ( identityDAL }); - const gatewayService = gatewayServiceFactory({ - permissionService, - gatewayDAL, - kmsService, - licenseService, - orgGatewayConfigDAL, - keyStore, - projectGatewayDAL - }); - const dynamicSecretProviders = buildDynamicSecretProviders({ gatewayService }); diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 21759e0cd..fd01b9957 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -21,7 +21,8 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick( kubernetesHost: true, allowedNamespaces: true, allowedNames: true, - allowedAudience: true + allowedAudience: true, + gatewayId: true }).extend({ caCert: z.string(), tokenReviewerJwt: z.string().optional().nullable() @@ -106,6 +107,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), + gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.ATTACH.gatewayId), accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() @@ -205,6 +207,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), + gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.UPDATE.gatewayId), accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 9c0e8d2dd..2e7950f80 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -4,6 +4,7 @@ import https from "https"; import jwt from "jsonwebtoken"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -13,6 +14,7 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { withGatewayProxy } from "@app/lib/gateway"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -43,6 +45,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = { permissionService: Pick; licenseService: Pick; kmsService: Pick; + gatewayService: TGatewayServiceFactory; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; @@ -53,8 +56,43 @@ export const identityKubernetesAuthServiceFactory = ({ identityAccessTokenDAL, permissionService, licenseService, + gatewayService, kmsService }: TIdentityKubernetesAuthServiceFactoryDep) => { + const $gatewayProxyWrapper = async ( + inputs: { + gatewayId: string; + targetHost: string; + targetPort: number; + }, + gatewayCallback: (host: string, port: number) => Promise + ): Promise => { + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId); + const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); + + const callbackResult = await withGatewayProxy( + async (port) => { + const res = await gatewayCallback("localhost", port); + return res; + }, + { + targetHost: inputs.targetHost, + targetPort: inputs.targetPort, + relayHost, + relayPort: Number(relayPort), + identityId: relayDetails.identityId, + orgId: relayDetails.orgId, + tlsOptions: { + ca: relayDetails.certChain, + cert: relayDetails.certificate, + key: relayDetails.privateKey.toString() + } + } + ); + + return callbackResult; + }; + const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); if (!identityKubernetesAuth) { @@ -92,46 +130,70 @@ export const identityKubernetesAuthServiceFactory = ({ tokenReviewerJwt = serviceAccountJwt; } - const { data } = await axios - .post( - `${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`, - { - apiVersion: "authentication.k8s.io/v1", - kind: "TokenReview", - spec: { - token: serviceAccountJwt, - ...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {}) - } - }, - { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${tokenReviewerJwt}` - }, - signal: AbortSignal.timeout(10000), - timeout: 10000, - // if ca cert, rejectUnauthorized: true - httpsAgent: new https.Agent({ - ca: caCert, - rejectUnauthorized: !!caCert - }) - } - ) - .catch((err) => { - if (err instanceof AxiosError) { - if (err.response) { - const { message } = err?.response?.data as unknown as { message?: string }; + const tokenReviewCallback = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => { + let baseUrl = `https://${host}`; - if (message) { - throw new UnauthorizedError({ - message, - name: "KubernetesTokenReviewRequestError" - }); + if (port) { + baseUrl += `:${port}`; + } + + const res = await axios + .post( + `${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`, + { + apiVersion: "authentication.k8s.io/v1", + kind: "TokenReview", + spec: { + token: serviceAccountJwt, + ...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {}) + } + }, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${tokenReviewerJwt}` + }, + signal: AbortSignal.timeout(10000), + timeout: 10000, + // if ca cert, rejectUnauthorized: true + httpsAgent: new https.Agent({ + ca: caCert, + // rejectUnauthorized: !!caCert, + rejectUnauthorized: false + }) + } + ) + .catch((err) => { + if (err instanceof AxiosError) { + if (err.response) { + const { message } = err?.response?.data as unknown as { message?: string }; + + if (message) { + throw new UnauthorizedError({ + message, + name: "KubernetesTokenReviewRequestError" + }); + } } } - } - throw err; - }); + throw err; + }); + + return res.data; + }; + + const [k8sHost, k8sPort] = identityKubernetesAuth.kubernetesHost.split(":"); + + const data = identityKubernetesAuth.gatewayId + ? await $gatewayProxyWrapper( + { + gatewayId: identityKubernetesAuth.gatewayId, + targetHost: k8sHost, + targetPort: k8sPort ? Number(k8sPort) : 443 + }, + tokenReviewCallback + ) + : await tokenReviewCallback(); if ("error" in data.status) throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" }); @@ -222,6 +284,7 @@ export const identityKubernetesAuthServiceFactory = ({ const attachKubernetesAuth = async ({ identityId, + gatewayId, kubernetesHost, caCert, tokenReviewerJwt, @@ -296,6 +359,7 @@ export const identityKubernetesAuthServiceFactory = ({ accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, + gatewayId, accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt ? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob @@ -318,6 +382,7 @@ export const identityKubernetesAuthServiceFactory = ({ allowedNamespaces, allowedNames, allowedAudience, + gatewayId, accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, @@ -378,6 +443,7 @@ export const identityKubernetesAuthServiceFactory = ({ allowedNamespaces, allowedNames, allowedAudience, + gatewayId, accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts index b3bbcb49e..7a9cb88b5 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts @@ -13,6 +13,7 @@ export type TAttachKubernetesAuthDTO = { allowedNamespaces: string; allowedNames: string; allowedAudience: string; + gatewayId?: string | null; accessTokenTTL: number; accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; @@ -28,6 +29,7 @@ export type TUpdateKubernetesAuthDTO = { allowedNamespaces?: string; allowedNames?: string; allowedAudience?: string; + gatewayId?: string | null; accessTokenTTL?: number; accessTokenMaxTTL?: number; accessTokenNumUsesLimit?: number; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index e0077527f..9209a601c 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -741,7 +741,8 @@ export const useAddIdentityKubernetesAuth = () => { accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps + accessTokenTrustedIps, + gatewayId }) => { const { data: { identityKubernetesAuth } @@ -757,7 +758,8 @@ export const useAddIdentityKubernetesAuth = () => { accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps + accessTokenTrustedIps, + gatewayId } ); @@ -846,7 +848,8 @@ export const useUpdateIdentityKubernetesAuth = () => { accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps + accessTokenTrustedIps, + gatewayId }) => { const { data: { identityKubernetesAuth } @@ -862,7 +865,8 @@ export const useUpdateIdentityKubernetesAuth = () => { accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps + accessTokenTrustedIps, + gatewayId } ); diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index c5f8cbc4a..e5b9609a1 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -346,6 +346,7 @@ export type IdentityKubernetesAuth = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: IdentityTrustedIp[]; + gatewayId?: string | null; }; export type AddIdentityKubernetesAuthDTO = { @@ -356,6 +357,7 @@ export type AddIdentityKubernetesAuthDTO = { allowedNamespaces: string; allowedNames: string; allowedAudience: string; + gatewayId?: string | null; caCert: string; accessTokenTTL: number; accessTokenMaxTTL: number; @@ -373,6 +375,7 @@ export type UpdateIdentityKubernetesAuthDTO = { allowedNamespaces?: string; allowedNames?: string; allowedAudience?: string; + gatewayId?: string | null; caCert?: string; accessTokenTTL?: number; accessTokenMaxTTL?: number; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index 87dc7dbfd..d7832fff8 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -11,6 +12,8 @@ import { FormControl, IconButton, Input, + Select, + SelectItem, Tab, TabList, TabPanel, @@ -19,6 +22,7 @@ import { } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { + gatewaysQueryKeys, useAddIdentityKubernetesAuth, useGetIdentityKubernetesAuth, useUpdateIdentityKubernetesAuth @@ -32,6 +36,7 @@ const schema = z .object({ kubernetesHost: z.string().min(1), tokenReviewerJwt: z.string().optional(), + gatewayId: z.string().optional().nullable(), allowedNames: z.string(), allowedNamespaces: z.string(), allowedAudience: z.string(), @@ -79,6 +84,8 @@ export const IdentityKubernetesAuthForm = ({ const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); + const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list()); + const { data } = useGetIdentityKubernetesAuth(identityId ?? "", { enabled: isUpdate }); @@ -96,6 +103,7 @@ export const IdentityKubernetesAuthForm = ({ tokenReviewerJwt: "", allowedNames: "", allowedNamespaces: "", + gatewayId: null, allowedAudience: "", caCert: "", accessTokenTTL: "2592000", @@ -120,6 +128,7 @@ export const IdentityKubernetesAuthForm = ({ allowedNamespaces: data.allowedNamespaces, allowedAudience: data.allowedAudience, caCert: data.caCert, + gatewayId: data.gatewayId || null, accessTokenTTL: String(data.accessTokenTTL), accessTokenMaxTTL: String(data.accessTokenMaxTTL), accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), @@ -157,6 +166,7 @@ export const IdentityKubernetesAuthForm = ({ accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, + gatewayId, accessTokenTrustedIps }: FormData) => { try { @@ -172,6 +182,7 @@ export const IdentityKubernetesAuthForm = ({ allowedAudience, caCert, identityId, + gatewayId: gatewayId || null, accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), @@ -186,6 +197,7 @@ export const IdentityKubernetesAuthForm = ({ allowedNames: allowedNames || "", allowedNamespaces: allowedNamespaces || "", allowedAudience: allowedAudience || "", + gatewayId: gatewayId || null, caCert: caCert || "", accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), @@ -217,6 +229,7 @@ export const IdentityKubernetesAuthForm = ({ [ "kubernetesHost", "tokenReviewerJwt", + "gatewayId", "accessTokenTTL", "accessTokenMaxTTL", "accessTokenNumUsesLimit", @@ -280,6 +293,40 @@ export const IdentityKubernetesAuthForm = ({ )} /> + + ( + + + + )} + /> + Date: Tue, 13 May 2025 14:59:58 +0400 Subject: [PATCH 2/9] feat(gateways): decouple gateways from projects --- backend/src/@types/knex.d.ts | 8 -- .../20250212191958_create-gateway.ts | 12 +- ...50513081738_remove-gateway-project-link.ts | 20 ++++ backend/src/db/schemas/dynamic-secrets.ts | 3 +- backend/src/db/schemas/index.ts | 1 - backend/src/db/schemas/models.ts | 1 - backend/src/ee/routes/v1/gateway-router.ts | 23 +--- .../dynamic-secret/dynamic-secret-service.ts | 73 ++++++++---- .../dynamic-secret/providers/index.ts | 2 +- .../dynamic-secret/providers/models.ts | 2 +- .../dynamic-secret/providers/sql-database.ts | 14 +-- .../src/ee/services/gateway/gateway-dal.ts | 74 +++--------- .../ee/services/gateway/gateway-service.ts | 45 +------ .../src/ee/services/gateway/gateway-types.ts | 1 - .../services/gateway/project-gateway-dal.ts | 10 -- .../ee/services/permission/org-permission.ts | 5 +- backend/src/server/routes/index.ts | 8 +- .../identity-kubernetes-auth-service.ts | 51 +++++++- .../src/context/OrgPermissionContext/types.ts | 3 +- frontend/src/hooks/api/gateways/mutation.tsx | 4 +- frontend/src/hooks/api/gateways/queries.tsx | 17 +-- frontend/src/hooks/api/gateways/types.ts | 26 ----- .../IdentityKubernetesAuthForm.tsx | 94 +++++++++------ .../GatewayListPage/GatewayListPage.tsx | 9 -- .../components/EditGatewayDetailsModal.tsx | 48 +------- .../components/OrgRoleModifySection.utils.ts | 3 +- .../OrgPermissionGatewayRow.tsx | 3 +- .../SqlDatabaseInputForm.tsx | 96 ++++++++------- .../EditDynamicSecretSqlProviderForm.tsx | 110 ++++++++++-------- 29 files changed, 361 insertions(+), 405 deletions(-) create mode 100644 backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts delete mode 100644 backend/src/ee/services/gateway/project-gateway-dal.ts diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index c26f1128e..552425a45 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -215,9 +215,6 @@ import { TProjectEnvironments, TProjectEnvironmentsInsert, TProjectEnvironmentsUpdate, - TProjectGateways, - TProjectGatewaysInsert, - TProjectGatewaysUpdate, TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate, @@ -1018,11 +1015,6 @@ declare module "knex/types/tables" { TKmipClientCertificatesUpdate >; [TableName.Gateway]: KnexOriginal.CompositeTableType; - [TableName.ProjectGateway]: KnexOriginal.CompositeTableType< - TProjectGateways, - TProjectGatewaysInsert, - TProjectGatewaysUpdate - >; [TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType< TOrgGatewayConfig, TOrgGatewayConfigInsert, diff --git a/backend/src/db/migrations/20250212191958_create-gateway.ts b/backend/src/db/migrations/20250212191958_create-gateway.ts index 14c498ca9..73f4c5fd7 100644 --- a/backend/src/db/migrations/20250212191958_create-gateway.ts +++ b/backend/src/db/migrations/20250212191958_create-gateway.ts @@ -68,8 +68,8 @@ export async function up(knex: Knex): Promise { await createOnUpdateTrigger(knex, TableName.Gateway); } - if (!(await knex.schema.hasTable(TableName.ProjectGateway))) { - await knex.schema.createTable(TableName.ProjectGateway, (t) => { + if (!(await knex.schema.hasTable("project_gateways"))) { + await knex.schema.createTable("project_gateways", (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.string("projectId").notNullable(); @@ -81,7 +81,7 @@ export async function up(knex: Knex): Promise { t.timestamps(true, true, true); }); - await createOnUpdateTrigger(knex, TableName.ProjectGateway); + await createOnUpdateTrigger(knex, "project_gateways"); } if (await knex.schema.hasTable(TableName.DynamicSecret)) { @@ -90,7 +90,7 @@ export async function up(knex: Knex): Promise { // not setting a foreign constraint so that cascade effects are not triggered if (!doesGatewayColExist) { t.uuid("projectGatewayId"); - t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway); + t.foreign("projectGatewayId").references("id").inTable("project_gateways"); } }); } @@ -104,8 +104,8 @@ export async function down(knex: Knex): Promise { }); } - await knex.schema.dropTableIfExists(TableName.ProjectGateway); - await dropOnUpdateTrigger(knex, TableName.ProjectGateway); + await knex.schema.dropTableIfExists("project_gateways"); + await dropOnUpdateTrigger(knex, "project_gateways"); await knex.schema.dropTableIfExists(TableName.Gateway); await dropOnUpdateTrigger(knex, TableName.Gateway); diff --git a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts new file mode 100644 index 000000000..86936eca0 --- /dev/null +++ b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts @@ -0,0 +1,20 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +// Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed. +// In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely. + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.DynamicSecret, (table) => { + table.uuid("gatewayId").nullable(); + table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL"); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.DynamicSecret, (table) => { + table.dropForeign("gatewayId"); + table.dropColumn("gatewayId"); + }); +} diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index 913a6d475..350a32b7a 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -27,7 +27,8 @@ export const DynamicSecretsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), encryptedInput: zodBuffer, - projectGatewayId: z.string().uuid().nullable().optional() + projectGatewayId: z.string().uuid().nullable().optional(), + gatewayId: z.string().uuid().nullable().optional() }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index b71d51908..63f2e082c 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -71,7 +71,6 @@ export * from "./pki-collection-items"; export * from "./pki-collections"; export * from "./project-bots"; export * from "./project-environments"; -export * from "./project-gateways"; export * from "./project-keys"; export * from "./project-memberships"; export * from "./project-roles"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 81d5319e1..eb44ffe1c 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -123,7 +123,6 @@ export enum TableName { // Gateway OrgGatewayConfig = "org_gateway_config", Gateway = "gateways", - ProjectGateway = "project_gateways", // junction tables with tags SecretV2JnTag = "secret_v2_tag_junction", JnSecretTag = "secret_tag_junction", diff --git a/backend/src/ee/routes/v1/gateway-router.ts b/backend/src/ee/routes/v1/gateway-router.ts index c916e229e..40e9c1580 100644 --- a/backend/src/ee/routes/v1/gateway-router.ts +++ b/backend/src/ee/routes/v1/gateway-router.ts @@ -121,14 +121,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => { identity: z.object({ name: z.string(), id: z.string() - }), - projects: z - .object({ - name: z.string(), - id: z.string(), - slug: z.string() - }) - .array() + }) }).array() }) } @@ -158,17 +151,15 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => { identity: z.object({ name: z.string(), id: z.string() - }), - projectGatewayId: z.string() + }) }).array() }) } }, onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), handler: async (req) => { - const gateways = await server.services.gateway.getProjectGateways({ - projectId: req.params.projectId, - projectPermission: req.permission + const gateways = await server.services.gateway.listGateways({ + orgPermission: req.permission }); return { gateways }; } @@ -216,8 +207,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => { id: z.string() }), body: z.object({ - name: slugSchema({ field: "name" }).optional(), - projectIds: z.string().array().optional() + name: slugSchema({ field: "name" }).optional() }), response: { 200: z.object({ @@ -230,8 +220,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => { const gateway = await server.services.gateway.updateGatewayById({ orgPermission: req.permission, id: req.params.id, - name: req.body.name, - projectIds: req.body.projectIds + name: req.body.name }); return { gateway }; } diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 44c18b001..5a74236a9 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -17,7 +17,8 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal"; import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue"; -import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal"; +import { TGatewayDALFactory } from "../gateway/gateway-dal"; +import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { DynamicSecretStatus, @@ -44,9 +45,9 @@ type TDynamicSecretServiceFactoryDep = { licenseService: Pick; folderDAL: Pick; projectDAL: Pick; - permissionService: Pick; + permissionService: Pick; kmsService: Pick; - projectGatewayDAL: Pick; + gatewayDAL: Pick; resourceMetadataDAL: Pick; }; @@ -62,7 +63,7 @@ export const dynamicSecretServiceFactory = ({ dynamicSecretQueueService, projectDAL, kmsService, - projectGatewayDAL, + gatewayDAL, resourceMetadataDAL }: TDynamicSecretServiceFactoryDep) => { const create = async ({ @@ -117,15 +118,31 @@ export const dynamicSecretServiceFactory = ({ const inputs = await selectedProvider.validateProviderInputs(provider.inputs); let selectedGatewayId: string | null = null; - if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) { - const projectGatewayId = inputs.projectGatewayId as string; + if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) { + const gatewayId = inputs.gatewayId as string; - const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); - if (!projectGateway) + const [gateway] = await gatewayDAL.find({ id: gatewayId }); + + if (!gateway) { throw new NotFoundError({ - message: `Project gateway with ${projectGatewayId} not found` + message: `Gateway with ID ${gatewayId} not found` }); - selectedGatewayId = projectGateway.id; + } + + const { permission: orgPermission } = await permissionService.getOrgPermission( + actor, + actorId, + gateway.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(orgPermission).throwUnlessCan( + OrgPermissionGatewayActions.AttachGateways, + OrgPermissionSubjects.Gateway + ); + + selectedGatewayId = gateway.id; } const isConnected = await selectedProvider.validateConnection(provider.inputs); @@ -146,7 +163,7 @@ export const dynamicSecretServiceFactory = ({ defaultTTL, folderId: folder.id, name, - projectGatewayId: selectedGatewayId + gatewayId: selectedGatewayId }, tx ); @@ -255,20 +272,30 @@ export const dynamicSecretServiceFactory = ({ const updatedInput = await selectedProvider.validateProviderInputs(newInput); let selectedGatewayId: string | null = null; - if ( - updatedInput && - typeof updatedInput === "object" && - "projectGatewayId" in updatedInput && - updatedInput?.projectGatewayId - ) { - const projectGatewayId = updatedInput.projectGatewayId as string; + if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) { + const gatewayId = updatedInput.gatewayId as string; - const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); - if (!projectGateway) + const [gateway] = await gatewayDAL.find({ id: gatewayId }); + if (!gateway) { throw new NotFoundError({ - message: `Project gateway with ${projectGatewayId} not found` + message: `Gateway with ID ${gatewayId} not found` }); - selectedGatewayId = projectGateway.id; + } + + const { permission: orgPermission } = await permissionService.getOrgPermission( + actor, + actorId, + gateway.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(orgPermission).throwUnlessCan( + OrgPermissionGatewayActions.AttachGateways, + OrgPermissionSubjects.Gateway + ); + + selectedGatewayId = gateway.id; } const isConnected = await selectedProvider.validateConnection(newInput); @@ -284,7 +311,7 @@ export const dynamicSecretServiceFactory = ({ defaultTTL, name: newName ?? name, status: null, - projectGatewayId: selectedGatewayId + gatewayId: selectedGatewayId }, tx ); diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index faa671980..737aaadea 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -18,7 +18,7 @@ import { SqlDatabaseProvider } from "./sql-database"; import { TotpProvider } from "./totp"; type TBuildDynamicSecretProviderDTO = { - gatewayService: Pick; + gatewayService: Pick; }; export const buildDynamicSecretProviders = ({ diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 449f6d8f6..0c6eaf151 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -137,7 +137,7 @@ export const DynamicSecretSqlDBSchema = z.object({ revocationStatement: z.string().trim(), renewStatement: z.string().trim().optional(), ca: z.string().optional(), - projectGatewayId: z.string().nullable().optional() + gatewayId: z.string().nullable().optional() }); export const DynamicSecretCassandraSchema = z.object({ 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 178ca4ef9..3ae85ed7b 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -112,14 +112,14 @@ const generateUsername = (provider: SqlProviders) => { }; type TSqlDatabaseProviderDTO = { - gatewayService: Pick; + gatewayService: Pick; }; export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); - const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)); + const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId)); validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, { allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val) }); @@ -168,7 +168,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) providerInputs: z.infer, gatewayCallback: (host: string, port: number) => Promise ) => { - const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string); + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); await withGatewayProxy( async (port) => { @@ -202,7 +202,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) await db.destroy(); }; - if (providerInputs.projectGatewayId) { + if (providerInputs.gatewayId) { await gatewayProxyWrapper(providerInputs, gatewayCallback); } else { await gatewayCallback(); @@ -238,7 +238,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) await db.destroy(); } }; - if (providerInputs.projectGatewayId) { + if (providerInputs.gatewayId) { await gatewayProxyWrapper(providerInputs, gatewayCallback); } else { await gatewayCallback(); @@ -265,7 +265,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) await db.destroy(); } }; - if (providerInputs.projectGatewayId) { + if (providerInputs.gatewayId) { await gatewayProxyWrapper(providerInputs, gatewayCallback); } else { await gatewayCallback(); @@ -301,7 +301,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) await db.destroy(); } }; - if (providerInputs.projectGatewayId) { + if (providerInputs.gatewayId) { await gatewayProxyWrapper(providerInputs, gatewayCallback); } else { await gatewayCallback(); diff --git a/backend/src/ee/services/gateway/gateway-dal.ts b/backend/src/ee/services/gateway/gateway-dal.ts index fbf5558e4..b51c781ee 100644 --- a/backend/src/ee/services/gateway/gateway-dal.ts +++ b/backend/src/ee/services/gateway/gateway-dal.ts @@ -1,16 +1,7 @@ -import { Knex } from "knex"; - import { TDbClient } from "@app/db"; import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { - buildFindFilter, - ormify, - selectAllTableCols, - sqlNestRelationships, - TFindFilter, - TFindOpt -} from "@app/lib/knex"; +import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; export type TGatewayDALFactory = ReturnType; @@ -21,17 +12,16 @@ export const gatewayDALFactory = (db: TDbClient) => { try { const query = (tx || db)(TableName.Gateway) // eslint-disable-next-line @typescript-eslint/no-misused-promises - .where(buildFindFilter(filter)) + .where(buildFindFilter(filter, TableName.Gateway)) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) - .leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`) - .leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`) + .join( + TableName.IdentityOrgMembership, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Gateway}.identityId` + ) .select(selectAllTableCols(TableName.Gateway)) - .select( - db.ref("name").withSchema(TableName.Identity).as("identityName"), - db.ref("name").withSchema(TableName.Project).as("projectName"), - db.ref("slug").withSchema(TableName.Project).as("projectSlug"), - db.ref("id").withSchema(TableName.Project).as("projectId") - ); + .select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId")) + .select(db.ref("name").withSchema(TableName.Identity).as("identityName")); if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { @@ -39,48 +29,16 @@ export const gatewayDALFactory = (db: TDbClient) => { } const docs = await query; - return sqlNestRelationships({ - data: docs, - key: "id", - parentMapper: (data) => ({ - ...GatewaysSchema.parse(data), - identity: { id: data.identityId, name: data.identityName } - }), - childrenMapper: [ - { - key: "projectId", - label: "projects" as const, - mapper: ({ projectId, projectName, projectSlug }) => ({ - id: projectId, - name: projectName, - slug: projectSlug - }) - } - ] - }); + + return docs.map((el) => ({ + ...GatewaysSchema.parse(el), + orgId: el.identityOrgId as string, // todo(daniel): figure out why typescript is not inferring this as a string + identity: { id: el.identityId, name: el.identityName } + })); } catch (error) { throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` }); } }; - const findByProjectId = async (projectId: string, tx?: Knex) => { - try { - const query = (tx || db)(TableName.Gateway) - .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) - .join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`) - .select(selectAllTableCols(TableName.Gateway)) - .select( - db.ref("name").withSchema(TableName.Identity).as("identityName"), - db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId") - ) - .where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId }); - - const docs = await query; - return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } })); - } catch (error) { - throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` }); - } - }; - - return { ...orm, find, findByProjectId }; + return { ...orm, find }; }; diff --git a/backend/src/ee/services/gateway/gateway-service.ts b/backend/src/ee/services/gateway/gateway-service.ts index 141797c98..25f0b384a 100644 --- a/backend/src/ee/services/gateway/gateway-service.ts +++ b/backend/src/ee/services/gateway/gateway-service.ts @@ -4,7 +4,6 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import { z } from "zod"; -import { ActionProjectType } from "@app/db/schemas"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -27,17 +26,14 @@ import { TGatewayDALFactory } from "./gateway-dal"; import { TExchangeAllocatedRelayAddressDTO, TGetGatewayByIdDTO, - TGetProjectGatewayByIdDTO, THeartBeatDTO, TListGatewaysDTO, TUpdateGatewayByIdDTO } from "./gateway-types"; import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal"; -import { TProjectGatewayDALFactory } from "./project-gateway-dal"; type TGatewayServiceFactoryDep = { gatewayDAL: TGatewayDALFactory; - projectGatewayDAL: TProjectGatewayDALFactory; orgGatewayConfigDAL: Pick; licenseService: Pick; kmsService: Pick; @@ -57,8 +53,7 @@ export const gatewayServiceFactory = ({ kmsService, permissionService, orgGatewayConfigDAL, - keyStore, - projectGatewayDAL + keyStore }: TGatewayServiceFactoryDep) => { const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => { // if (!licenseService.onPremFeatures.gateway) { @@ -526,7 +521,7 @@ export const gatewayServiceFactory = ({ return gateway; }; - const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => { + const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => { const { permission } = await permissionService.getOrgPermission( orgPermission.type, orgPermission.id, @@ -543,15 +538,6 @@ export const gatewayServiceFactory = ({ const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name }); if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); - if (projectIds) { - await projectGatewayDAL.transaction(async (tx) => { - await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx); - await projectGatewayDAL.insertMany( - projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })), - tx - ); - }); - } return gateway; }; @@ -576,20 +562,6 @@ export const gatewayServiceFactory = ({ return gateway; }; - const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => { - await permissionService.getProjectPermission({ - projectId, - actor: projectPermission.type, - actorId: projectPermission.id, - actorOrgId: projectPermission.orgId, - actorAuthMethod: projectPermission.authMethod, - actionProjectType: ActionProjectType.Any - }); - - const gateways = await gatewayDAL.findByProjectId(projectId); - return gateways; - }; - const fnGetGatewayClientTlsByGatewayId = async (gatewayId: string) => { const gateway = await gatewayDAL.findById(gatewayId); if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` }); @@ -632,17 +604,6 @@ export const gatewayServiceFactory = ({ }; }; - // this has no permission check and used for dynamic secrets directly - // assumes permission check is already done - const fnGetGatewayClientTls = async (projectGatewayId: string) => { - const projectGateway = await projectGatewayDAL.findById(projectGatewayId); - if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` }); - - const gatewayDetails = await fnGetGatewayClientTlsByGatewayId(projectGateway.gatewayId); - - return gatewayDetails; - }; - return { getGatewayRelayDetails, exchangeAllocatedRelayAddress, @@ -650,8 +611,6 @@ export const gatewayServiceFactory = ({ getGatewayById, updateGatewayById, deleteGatewayById, - getProjectGateways, - fnGetGatewayClientTls, fnGetGatewayClientTlsByGatewayId, heartbeat }; diff --git a/backend/src/ee/services/gateway/gateway-types.ts b/backend/src/ee/services/gateway/gateway-types.ts index 220dc7147..823028154 100644 --- a/backend/src/ee/services/gateway/gateway-types.ts +++ b/backend/src/ee/services/gateway/gateway-types.ts @@ -20,7 +20,6 @@ export type TGetGatewayByIdDTO = { export type TUpdateGatewayByIdDTO = { id: string; name?: string; - projectIds?: string[]; orgPermission: OrgServiceActor; }; diff --git a/backend/src/ee/services/gateway/project-gateway-dal.ts b/backend/src/ee/services/gateway/project-gateway-dal.ts deleted file mode 100644 index 44c36f5f6..000000000 --- a/backend/src/ee/services/gateway/project-gateway-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TProjectGatewayDALFactory = ReturnType; - -export const projectGatewayDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.ProjectGateway); - return orm; -}; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index 7026899c7..612914bcc 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -41,7 +41,8 @@ export enum OrgPermissionGatewayActions { CreateGateways = "create-gateways", ListGateways = "list-gateways", EditGateways = "edit-gateways", - DeleteGateways = "delete-gateways" + DeleteGateways = "delete-gateways", + AttachGateways = "attach-gateways" } export enum OrgPermissionIdentityActions { @@ -337,6 +338,7 @@ const buildAdminPermission = () => { can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); @@ -378,6 +380,7 @@ const buildMemberPermission = () => { can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway); return rules; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 6367496f1..43720c40c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -32,7 +32,6 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; -import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal"; import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal"; import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { groupDALFactory } from "@app/ee/services/group/group-dal"; @@ -434,7 +433,6 @@ export const registerRoutes = async ( const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db); const gatewayDAL = gatewayDALFactory(db); - const projectGatewayDAL = projectGatewayDALFactory(db); const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db); const githubOrgSyncDAL = githubOrgSyncDALFactory(db); @@ -1408,8 +1406,7 @@ export const registerRoutes = async ( kmsService, licenseService, orgGatewayConfigDAL, - keyStore, - projectGatewayDAL + keyStore }); const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ @@ -1419,6 +1416,7 @@ export const registerRoutes = async ( permissionService, licenseService, gatewayService, + gatewayDAL, kmsService }); const identityGcpAuthService = identityGcpAuthServiceFactory({ @@ -1494,7 +1492,7 @@ export const registerRoutes = async ( permissionService, licenseService, kmsService, - projectGatewayDAL, + gatewayDAL, resourceMetadataDAL }); diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 2e7950f80..a1005cf1b 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -4,9 +4,14 @@ import https from "https"; import jwt from "jsonwebtoken"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + OrgPermissionGatewayActions, + OrgPermissionIdentityActions, + OrgPermissionSubjects +} from "@app/ee/services/permission/org-permission"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation @@ -46,6 +51,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = { licenseService: Pick; kmsService: Pick; gatewayService: TGatewayServiceFactory; + gatewayDAL: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; @@ -57,6 +63,7 @@ export const identityKubernetesAuthServiceFactory = ({ permissionService, licenseService, gatewayService, + gatewayDAL, kmsService }: TIdentityKubernetesAuthServiceFactoryDep) => { const $gatewayProxyWrapper = async ( @@ -343,6 +350,27 @@ export const identityKubernetesAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); + if (gatewayId) { + const [gateway] = await gatewayDAL.find({ id: gatewayId }); + if (!gateway) { + throw new NotFoundError({ + message: `Gateway with ID ${gatewayId} not found` + }); + } + + const { permission: orgPermission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(orgPermission).throwUnlessCan( + OrgPermissionGatewayActions.AttachGateways, + OrgPermissionSubjects.Gateway + ); + } + const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: identityMembershipOrg.orgId @@ -438,6 +466,27 @@ export const identityKubernetesAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); + if (gatewayId) { + const [gateway] = await gatewayDAL.find({ id: gatewayId }); + if (!gateway) { + throw new NotFoundError({ + message: `Gateway with ID ${gatewayId} not found` + }); + } + + const { permission: orgPermission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(orgPermission).throwUnlessCan( + OrgPermissionGatewayActions.AttachGateways, + OrgPermissionSubjects.Gateway + ); + } + const updateQuery: TIdentityKubernetesAuthsUpdate = { kubernetesHost, allowedNamespaces, diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 2dbfaacb7..a4bd202bf 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -12,7 +12,8 @@ export enum OrgGatewayPermissionActions { CreateGateways = "create-gateways", ListGateways = "list-gateways", EditGateways = "edit-gateways", - DeleteGateways = "delete-gateways" + DeleteGateways = "delete-gateways", + AttachGateways = "attach-gateways" } export enum OrgPermissionSubjects { diff --git a/frontend/src/hooks/api/gateways/mutation.tsx b/frontend/src/hooks/api/gateways/mutation.tsx index e93197fdd..ef292cb39 100644 --- a/frontend/src/hooks/api/gateways/mutation.tsx +++ b/frontend/src/hooks/api/gateways/mutation.tsx @@ -20,8 +20,8 @@ export const useDeleteGatewayById = () => { export const useUpdateGatewayById = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ id, name, projectIds }: TUpdateGatewayDTO) => { - return apiRequest.patch(`/api/v1/gateways/${id}`, { name, projectIds }); + mutationFn: ({ id, name }: TUpdateGatewayDTO) => { + return apiRequest.patch(`/api/v1/gateways/${id}`, { name }); }, onSuccess: () => { queryClient.invalidateQueries(gatewaysQueryKeys.list()); diff --git a/frontend/src/hooks/api/gateways/queries.tsx b/frontend/src/hooks/api/gateways/queries.tsx index 6ec374a6c..bb05b17a4 100644 --- a/frontend/src/hooks/api/gateways/queries.tsx +++ b/frontend/src/hooks/api/gateways/queries.tsx @@ -2,7 +2,7 @@ import { queryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TGateway, TListProjectGatewayDTO, TProjectGateway } from "./types"; +import { TGateway } from "./types"; export const gatewaysQueryKeys = { allKey: () => ["gateways"], @@ -14,20 +14,5 @@ export const gatewaysQueryKeys = { const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways"); return data.gateways; } - }), - listProjectGatewayKey: ({ projectId }: TListProjectGatewayDTO) => [ - ...gatewaysQueryKeys.allKey(), - "list", - { projectId } - ], - listProjectGateways: ({ projectId }: TListProjectGatewayDTO) => - queryOptions({ - queryKey: gatewaysQueryKeys.listProjectGatewayKey({ projectId }), - queryFn: async () => { - const { data } = await apiRequest.get<{ gateways: TProjectGateway[] }>( - `/api/v1/gateways/projects/${projectId}` - ); - return data.gateways; - } }) }; diff --git a/frontend/src/hooks/api/gateways/types.ts b/frontend/src/hooks/api/gateways/types.ts index a522b6c48..6a3f2d673 100644 --- a/frontend/src/hooks/api/gateways/types.ts +++ b/frontend/src/hooks/api/gateways/types.ts @@ -11,39 +11,13 @@ export type TGateway = { name: string; id: string; }; - projects: { - name: string; - id: string; - slug: string; - }[]; -}; - -export type TProjectGateway = { - id: string; - identityId: string; - name: string; - createdAt: string; - updatedAt: string; - issuedAt: string; - serialNumber: string; - heartbeat: string; - projectGatewayId: string; - identity: { - name: string; - id: string; - }; }; export type TUpdateGatewayDTO = { id: string; name?: string; - projectIds?: string[]; }; export type TDeleteGatewayDTO = { id: string; }; - -export type TListProjectGatewayDTO = { - projectId: string; -}; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index d7832fff8..12cd8a6cf 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -7,6 +7,7 @@ import { useQuery } from "@tanstack/react-query"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; import { Button, FormControl, @@ -18,9 +19,14 @@ import { TabList, TabPanel, Tabs, - TextArea + TextArea, + Tooltip } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; +import { + OrgGatewayPermissionActions, + OrgPermissionSubjects +} from "@app/context/OrgPermissionContext/types"; import { gatewaysQueryKeys, useAddIdentityKubernetesAuth, @@ -103,7 +109,7 @@ export const IdentityKubernetesAuthForm = ({ tokenReviewerJwt: "", allowedNames: "", allowedNamespaces: "", - gatewayId: null, + gatewayId: "", allowedAudience: "", caCert: "", accessTokenTTL: "2592000", @@ -294,38 +300,60 @@ export const IdentityKubernetesAuthForm = ({ )} /> - ( - - - + + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> )} - /> +
Name Cert Issued At - Projects Identity Health Check @@ -151,13 +149,6 @@ export const GatewayListPage = withPermission( {el.name} {format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")} - - {el.projects.map((projectDetails) => ( - - {projectDetails.name} - - ))} - {el.identity.name} {el.heartbeat diff --git a/frontend/src/pages/organization/Gateways/GatewayListPage/components/EditGatewayDetailsModal.tsx b/frontend/src/pages/organization/Gateways/GatewayListPage/components/EditGatewayDetailsModal.tsx index d41fd5489..1e07036ba 100644 --- a/frontend/src/pages/organization/Gateways/GatewayListPage/components/EditGatewayDetailsModal.tsx +++ b/frontend/src/pages/organization/Gateways/GatewayListPage/components/EditGatewayDetailsModal.tsx @@ -3,10 +3,9 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2"; -import { useGetUserWorkspaces, useUpdateGatewayById } from "@app/hooks/api"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { useUpdateGatewayById } from "@app/hooks/api"; import { TGateway } from "@app/hooks/api/gateways/types"; -import { ProjectType } from "@app/hooks/api/workspace/types"; type Props = { gatewayDetails: TGateway; @@ -14,13 +13,7 @@ type Props = { }; const schema = z.object({ - name: z.string(), - projects: z - .object({ - id: z.string(), - name: z.string() - }) - .array() + name: z.string() }); export type FormData = z.infer; @@ -38,20 +31,13 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => { }); const updateGatewayById = useUpdateGatewayById(); - // when gateway goes to other products switch to all - const { data: secretManagerWorkspaces, isLoading: isSecretManagerLoading } = useGetUserWorkspaces( - { - type: ProjectType.SecretManager - } - ); - const onFormSubmit = ({ name, projects }: FormData) => { + const onFormSubmit = ({ name }: FormData) => { if (isSubmitting) return; updateGatewayById.mutate( { id: gatewayDetails.id, - name, - projectIds: projects.map((el) => el.id) + name }, { onSuccess: () => { @@ -76,30 +62,6 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => { )} /> - ( - - option.id} - getOptionLabel={(option) => option.name} - /> - - )} - />
- ( - - - + +
+ +
+
+ + )} + /> )} - /> +
Service
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx index 7c15db140..3966111ee 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx @@ -6,6 +6,7 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; import { Accordion, AccordionContent, @@ -17,9 +18,11 @@ import { SecretInput, Select, SelectItem, - TextArea + TextArea, + Tooltip } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { OrgPermissionSubjects } from "@app/context"; +import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types"; import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api"; import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; @@ -60,7 +63,7 @@ const formSchema = z.object({ revocationStatement: z.string().min(1), renewStatement: z.string().optional(), ca: z.string().optional(), - projectGatewayId: z.string().optional().nullable() + gatewayId: z.string().optional().nullable() }) .partial(), defaultTTL: z.string().superRefine((val, ctx) => { @@ -147,15 +150,11 @@ export const EditDynamicSecretSqlProviderForm = ({ } }); - const { currentWorkspace } = useWorkspace(); - const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery( - gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id }) - ); + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const updateDynamicSecret = useUpdateDynamicSecret(); - const selectedProjectGatewayId = watch("inputs.projectGatewayId"); - const isGatewayInActive = - projectGateways?.findIndex((el) => el.projectGatewayId === selectedProjectGatewayId) === -1; + const selectedGatewayId = watch("inputs.gatewayId"); + const isGatewayInActive = gateways?.findIndex((el) => el.id === selectedGatewayId) === -1; const handleUpdateDynamicSecret = async ({ inputs, @@ -177,7 +176,7 @@ export const EditDynamicSecretSqlProviderForm = ({ defaultTTL, inputs: { ...inputs, - projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId + gatewayId: isGatewayInActive ? null : inputs.gatewayId }, newName: newName === dynamicSecret.name ? undefined : newName, metadata @@ -250,45 +249,60 @@ export const EditDynamicSecretSqlProviderForm = ({
Configuration
- ( - - - + +
+ +
+
+ + )} + /> )} - /> +
Date: Tue, 13 May 2025 15:04:21 +0400 Subject: [PATCH 3/9] Update organization-permissions.mdx --- docs/internals/permissions/organization-permissions.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/internals/permissions/organization-permissions.mdx b/docs/internals/permissions/organization-permissions.mdx index c68d845e2..6de3bd6fe 100644 --- a/docs/internals/permissions/organization-permissions.mdx +++ b/docs/internals/permissions/organization-permissions.mdx @@ -218,3 +218,4 @@ Supports conditions and permission inversion | `create-gateways` | Add new gateways to organization | | `edit-gateways` | Modify existing gateway settings | | `delete-gateways` | Remove gateways from organization | +| `attach-gateways` | Attach gateways to resources | From a12522db552ed60ab85eb100f3b9dae2030210c5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 13 May 2025 15:18:23 +0400 Subject: [PATCH 4/9] requested changes --- .../migrations/20250513081738_remove-gateway-project-link.ts | 3 +++ .../identity-kubernetes-auth-service.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts index 86936eca0..3308614ab 100644 --- a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts +++ b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts @@ -9,6 +9,8 @@ export async function up(knex: Knex): Promise { await knex.schema.alterTable(TableName.DynamicSecret, (table) => { table.uuid("gatewayId").nullable(); table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL"); + + table.index("gatewayId"); }); } @@ -16,5 +18,6 @@ export async function down(knex: Knex): Promise { await knex.schema.alterTable(TableName.DynamicSecret, (table) => { table.dropForeign("gatewayId"); table.dropColumn("gatewayId"); + table.dropIndex("gatewayId"); }); } diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index a1005cf1b..32855d3cb 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -165,8 +165,7 @@ export const identityKubernetesAuthServiceFactory = ({ // if ca cert, rejectUnauthorized: true httpsAgent: new https.Agent({ ca: caCert, - // rejectUnauthorized: !!caCert, - rejectUnauthorized: false + rejectUnauthorized: !!caCert }) } ) From 8adf4787b9d8e081da91a948b447b76f63aea706 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 13 May 2025 15:31:13 +0400 Subject: [PATCH 5/9] Update 20250513081738_remove-gateway-project-link.ts --- .../db/migrations/20250513081738_remove-gateway-project-link.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts index 3308614ab..6b7cf331f 100644 --- a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts +++ b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts @@ -18,6 +18,5 @@ export async function down(knex: Knex): Promise { await knex.schema.alterTable(TableName.DynamicSecret, (table) => { table.dropForeign("gatewayId"); table.dropColumn("gatewayId"); - table.dropIndex("gatewayId"); }); } From 63c71fabcd150f3859147afe366d623a06639927 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 14 May 2025 16:00:27 +0400 Subject: [PATCH 6/9] fix: migrate project gateway --- backend/src/@types/knex.d.ts | 8 ++ ...50513081738_remove-gateway-project-link.ts | 108 ++++++++++++++++-- backend/src/db/schemas/index.ts | 1 + backend/src/db/schemas/models.ts | 1 + 4 files changed, 108 insertions(+), 10 deletions(-) diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index ed6762ef0..4f136bef0 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -218,6 +218,9 @@ import { TProjectEnvironments, TProjectEnvironmentsInsert, TProjectEnvironmentsUpdate, + TProjectGateways, + TProjectGatewaysInsert, + TProjectGatewaysUpdate, TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate, @@ -1023,6 +1026,11 @@ declare module "knex/types/tables" { TKmipClientCertificatesUpdate >; [TableName.Gateway]: KnexOriginal.CompositeTableType; + [TableName.ProjectGateway]: KnexOriginal.CompositeTableType< + TProjectGateways, + TProjectGatewaysInsert, + TProjectGatewaysUpdate + >; [TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType< TOrgGatewayConfig, TOrgGatewayConfigInsert, diff --git a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts index 6b7cf331f..3b3c5322e 100644 --- a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts +++ b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts @@ -1,22 +1,110 @@ import { Knex } from "knex"; +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + import { TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { getMigrationEncryptionServices } from "./utils/services"; // Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed. // In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely. -export async function up(knex: Knex): Promise { - await knex.schema.alterTable(TableName.DynamicSecret, (table) => { - table.uuid("gatewayId").nullable(); - table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL"); +const BATCH_SIZE = 500; - table.index("gatewayId"); - }); +export async function up(knex: Knex): Promise { + // eslint-disable-next-line no-param-reassign + knex.replicaNode = () => { + return knex; + }; + + if (!(await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId"))) { + await knex.schema.alterTable(TableName.DynamicSecret, (table) => { + table.uuid("gatewayId").nullable(); + table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL"); + + table.index("gatewayId"); + }); + + const existingDynamicSecretsWithProjectGatewayId = await knex(TableName.DynamicSecret) + .select(selectAllTableCols(TableName.DynamicSecret)) + .whereNotNull(`${TableName.DynamicSecret}.projectGatewayId`) + .join(TableName.ProjectGateway, `${TableName.ProjectGateway}.id`, `${TableName.DynamicSecret}.projectGatewayId`) + .whereNotNull(`${TableName.ProjectGateway}.gatewayId`) + .select( + knex.ref("projectId").withSchema(TableName.ProjectGateway).as("projectId"), + knex.ref("gatewayId").withSchema(TableName.ProjectGateway).as("projectGatewayGatewayId") + ); + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + + const updatedDynamicSecrets = await Promise.all( + existingDynamicSecretsWithProjectGatewayId.map(async (existingDynamicSecret) => { + if (!existingDynamicSecret.projectGatewayGatewayId) { + const result = { + ...existingDynamicSecret, + gatewayId: null + }; + + const { projectId, projectGatewayGatewayId, ...rest } = result; + return rest; + } + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: existingDynamicSecret.projectId + }); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: existingDynamicSecret.projectId + }); + + let decryptedStoredInput = JSON.parse( + secretManagerDecryptor({ cipherTextBlob: Buffer.from(existingDynamicSecret.encryptedInput) }).toString() + ) as object; + + // We're not removing the existing projectGatewayId from the input so we can easily rollback without having to re-encrypt the input + decryptedStoredInput = { + ...decryptedStoredInput, + gatewayId: existingDynamicSecret.projectGatewayGatewayId + }; + + const encryptedInput = secretManagerEncryptor({ + plainText: Buffer.from(JSON.stringify(decryptedStoredInput)) + }).cipherTextBlob; + + const result = { + ...existingDynamicSecret, + encryptedInput, + gatewayId: existingDynamicSecret.projectGatewayGatewayId + }; + + const { projectId, projectGatewayGatewayId, ...rest } = result; + return rest; + }) + ); + + for (let i = 0; i < updatedDynamicSecrets.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.DynamicSecret) + .insert(updatedDynamicSecrets.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + } } export async function down(knex: Knex): Promise { - await knex.schema.alterTable(TableName.DynamicSecret, (table) => { - table.dropForeign("gatewayId"); - table.dropColumn("gatewayId"); - }); + // no re-encryption needed as we keep the old projectGatewayId in the input + if (await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId")) { + await knex.schema.alterTable(TableName.DynamicSecret, (table) => { + table.dropForeign("gatewayId"); + table.dropColumn("gatewayId"); + }); + } } diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 1db759887..ebbe417c4 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -72,6 +72,7 @@ export * from "./pki-collections"; export * from "./pki-subscribers"; export * from "./project-bots"; export * from "./project-environments"; +export * from "./project-gateways"; export * from "./project-keys"; export * from "./project-memberships"; export * from "./project-roles"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 6c2d457ba..912c8ac46 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -124,6 +124,7 @@ export enum TableName { // Gateway OrgGatewayConfig = "org_gateway_config", Gateway = "gateways", + ProjectGateway = "project_gateways", // junction tables with tags SecretV2JnTag = "secret_v2_tag_junction", JnSecretTag = "secret_tag_junction", From cd028ae133fdd0a5ec4b706c3222c1c34bf040dd Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 14 May 2025 16:01:07 +0400 Subject: [PATCH 7/9] Update 20250212191958_create-gateway.ts --- .../db/migrations/20250212191958_create-gateway.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/db/migrations/20250212191958_create-gateway.ts b/backend/src/db/migrations/20250212191958_create-gateway.ts index 73f4c5fd7..14c498ca9 100644 --- a/backend/src/db/migrations/20250212191958_create-gateway.ts +++ b/backend/src/db/migrations/20250212191958_create-gateway.ts @@ -68,8 +68,8 @@ export async function up(knex: Knex): Promise { await createOnUpdateTrigger(knex, TableName.Gateway); } - if (!(await knex.schema.hasTable("project_gateways"))) { - await knex.schema.createTable("project_gateways", (t) => { + if (!(await knex.schema.hasTable(TableName.ProjectGateway))) { + await knex.schema.createTable(TableName.ProjectGateway, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.string("projectId").notNullable(); @@ -81,7 +81,7 @@ export async function up(knex: Knex): Promise { t.timestamps(true, true, true); }); - await createOnUpdateTrigger(knex, "project_gateways"); + await createOnUpdateTrigger(knex, TableName.ProjectGateway); } if (await knex.schema.hasTable(TableName.DynamicSecret)) { @@ -90,7 +90,7 @@ export async function up(knex: Knex): Promise { // not setting a foreign constraint so that cascade effects are not triggered if (!doesGatewayColExist) { t.uuid("projectGatewayId"); - t.foreign("projectGatewayId").references("id").inTable("project_gateways"); + t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway); } }); } @@ -104,8 +104,8 @@ export async function down(knex: Knex): Promise { }); } - await knex.schema.dropTableIfExists("project_gateways"); - await dropOnUpdateTrigger(knex, "project_gateways"); + await knex.schema.dropTableIfExists(TableName.ProjectGateway); + await dropOnUpdateTrigger(knex, TableName.ProjectGateway); await knex.schema.dropTableIfExists(TableName.Gateway); await dropOnUpdateTrigger(knex, TableName.Gateway); From aaeb6e73fe104a60cea5483a49acb8fe34eff437 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 15 May 2025 16:06:20 +0400 Subject: [PATCH 8/9] requested changes --- .../v1/identity-kubernetes-auth-router.ts | 43 ++++++++++++++++++- .../platform/gateways/overview.mdx | 10 ----- .../IdentityKubernetesAuthForm.tsx | 2 +- .../components/EditGatewayDetailsModal.tsx | 11 +++++ .../ViewIdentityKubernetesAuthContent.tsx | 11 ++++- .../SqlDatabaseInputForm.tsx | 2 +- .../EditDynamicSecretSqlProviderForm.tsx | 2 +- 7 files changed, 65 insertions(+), 16 deletions(-) diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index fd01b9957..de7927573 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { IdentityKubernetesAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -101,7 +102,24 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }), body: z .object({ - kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost), + kubernetesHost: z + .string() + .trim() + .min(1) + .describe(KUBERNETES_AUTH.ATTACH.kubernetesHost) + .refine( + (val) => + characterValidator([ + CharacterType.Alphabets, + CharacterType.Numbers, + CharacterType.Colon, + CharacterType.Period, + CharacterType.ForwardSlash + ])(val), + { + message: "Kubernetes host must only contain alphabets, numbers, colons, periods, and forward slashes." + } + ), caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation @@ -201,7 +219,28 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }), body: z .object({ - kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost), + kubernetesHost: z + .string() + .trim() + .min(1) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.kubernetesHost) + .refine( + (val) => { + if (!val) return true; + + return characterValidator([ + CharacterType.Alphabets, + CharacterType.Numbers, + CharacterType.Colon, + CharacterType.Period, + CharacterType.ForwardSlash + ])(val); + }, + { + message: "Kubernetes host must only contain alphabets, numbers, colons, periods, and forward slashes." + } + ), caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index 7ccc098cd..ae4a3c7ad 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -158,14 +158,4 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t To confirm your Gateway is working, check the deployment status by looking for the message **"Gateway started successfully"** in the Gateway logs. This indicates the Gateway is running properly. Next, verify its registration by opening your Infisical dashboard, navigating to **Organization Access Control**, and selecting the **Gateways** tab. Your newly deployed Gateway should appear in the list. ![Gateway List](../../../images/platform/gateways/gateway-list.png) - - - To enable Infisical features like dynamic secrets or secret rotation to access private resources through the Gateway, you need to link the Gateway to the relevant projects. - - Start by accessing the **Gateway settings** then locate the Gateway in the list, click the options menu (**:**), and select **Edit Details**. - ![Edit Gateway Option](../../../images/platform/gateways/edit-gateway.png) - In the edit modal that appears, choose the projects you want the Gateway to access and click **Save** to confirm your selections. - ![Project Assignment Modal](../../../images/platform/gateways/assign-project.png) - Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project. - diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index 12cd8a6cf..369977a04 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -332,7 +332,7 @@ export const IdentityKubernetesAuthForm = ({ className="w-full border border-mineshaft-500" dropdownContainerClassName="max-w-none" isLoading={isGatewayLoading} - placeholder="Select Gateway" + placeholder="Default: Internet Gateway" position="popper" > { return (
+ +

+ Since the 15th May 2025, all gateways are automatically available for use in all projects + and you no longer need to link them. +
+ Organization members with the "Attach Gateways" permission can use gateways + anywhere within the organization. +

+
+ { + const { data: gateways } = useQuery(gatewaysQueryKeys.list()); + const { data, isPending } = useGetIdentityKubernetesAuth(identityId); + const selectedGateway = useMemo(() => { + return gateways?.find((gateway) => gateway.id === data?.gatewayId) || null; + }, [gateways, data?.gatewayId]); + if (isPending) { return (
@@ -69,6 +77,7 @@ export const ViewIdentityKubernetesAuthContent = ({ > {data.kubernetesHost} + {selectedGateway?.name} {data.tokenReviewerJwt ? ( Date: Thu, 15 May 2025 16:55:36 +0400 Subject: [PATCH 9/9] requested changes --- .../services/dynamic-secret/dynamic-secret-service.ts | 4 ++-- backend/src/ee/services/gateway/gateway-dal.ts | 11 +++++++++-- backend/src/lib/knex/index.ts | 4 ++-- .../identity-kubernetes-auth-service.ts | 4 ++-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 5a74236a9..c39f07b5c 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -121,7 +121,7 @@ export const dynamicSecretServiceFactory = ({ if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) { const gatewayId = inputs.gatewayId as string; - const [gateway] = await gatewayDAL.find({ id: gatewayId }); + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId }); if (!gateway) { throw new NotFoundError({ @@ -275,7 +275,7 @@ export const dynamicSecretServiceFactory = ({ if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) { const gatewayId = updatedInput.gatewayId as string; - const [gateway] = await gatewayDAL.find({ id: gatewayId }); + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId }); if (!gateway) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` diff --git a/backend/src/ee/services/gateway/gateway-dal.ts b/backend/src/ee/services/gateway/gateway-dal.ts index b51c781ee..31b4b727b 100644 --- a/backend/src/ee/services/gateway/gateway-dal.ts +++ b/backend/src/ee/services/gateway/gateway-dal.ts @@ -8,11 +8,14 @@ export type TGatewayDALFactory = ReturnType; export const gatewayDALFactory = (db: TDbClient) => { const orm = ormify(db, TableName.Gateway); - const find = async (filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {}) => { + const find = async ( + filter: TFindFilter & { orgId?: string }, + { offset, limit, sort, tx }: TFindOpt = {} + ) => { try { const query = (tx || db)(TableName.Gateway) // eslint-disable-next-line @typescript-eslint/no-misused-promises - .where(buildFindFilter(filter, TableName.Gateway)) + .where(buildFindFilter(filter, TableName.Gateway, ["orgId"])) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) .join( TableName.IdentityOrgMembership, @@ -22,6 +25,10 @@ export const gatewayDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.Gateway)) .select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId")) .select(db.ref("name").withSchema(TableName.Identity).as("identityName")); + + if (filter.orgId) { + void query.where(`${TableName.IdentityOrgMembership}.orgId`, filter.orgId); + } if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index b1e011709..2e17bff20 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -32,13 +32,13 @@ export const buildFindFilter = ( { $in, $notNull, $search, $complex, ...filter }: TFindFilter, tableName?: TableName, - excludeKeys?: Array + excludeKeys?: string[] ) => (bd: Knex.QueryBuilder) => { const processedFilter = tableName ? Object.fromEntries( Object.entries(filter) - .filter(([key]) => !excludeKeys || !excludeKeys.includes(key as keyof R)) + .filter(([key]) => !excludeKeys || !excludeKeys.includes(key)) .map(([key, value]) => [`${tableName}.${key}`, value]) ) : filter; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 32855d3cb..2da7c3881 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -350,7 +350,7 @@ export const identityKubernetesAuthServiceFactory = ({ }); if (gatewayId) { - const [gateway] = await gatewayDAL.find({ id: gatewayId }); + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); if (!gateway) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` @@ -466,7 +466,7 @@ export const identityKubernetesAuthServiceFactory = ({ }); if (gatewayId) { - const [gateway] = await gatewayDAL.find({ id: gatewayId }); + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); if (!gateway) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found`