diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 27f14fdb2..344d1e02e 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -119,6 +119,10 @@ declare module "@fastify/request-context" { oidc?: { claims: Record; }; + kubernetes?: { + namespace: string; + name: string; + }; }; identityPermissionMetadata?: Record; // filled by permission service assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string }; diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts index bf5cce7d5..b916bab67 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts @@ -23,7 +23,10 @@ const validateUsernameTemplateCharacters = characterValidator([ CharacterType.CloseBrace, CharacterType.CloseBracket, CharacterType.OpenBracket, - CharacterType.Fullstop + CharacterType.Fullstop, + CharacterType.SingleQuote, + CharacterType.Spaces, + CharacterType.Pipe ]); const userTemplateSchema = z @@ -33,7 +36,7 @@ const userTemplateSchema = z .refine((el) => validateUsernameTemplateCharacters(el)) .refine((el) => isValidHandleBarTemplate(el, { - allowedExpressions: (val) => ["randomUsername", "unixTimestamp"].includes(val) + allowedExpressions: (val) => ["randomUsername", "unixTimestamp", "identity.name"].includes(val) }) ); diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index fa1a80ac3..c38a8f146 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -99,7 +99,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; - await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); + await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { + projectId: folder.projectId + }); await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id); return; } @@ -133,7 +135,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); await Promise.all( dynamicSecretLeases.map(({ externalEntityId }) => - selectedProvider.revoke(decryptedStoredInput, externalEntityId) + selectedProvider.revoke(decryptedStoredInput, externalEntityId, { + projectId: folder.projectId + }) ) ); } diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index f3f3f3acd..561b2170c 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError, subject } from "@casl/ability"; +import RE2 from "re2"; import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -11,10 +12,13 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { ms } from "@app/lib/ms"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; +import { TUserDALFactory } from "@app/services/user/user-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models"; @@ -39,6 +43,8 @@ type TDynamicSecretLeaseServiceFactoryDep = { permissionService: Pick; projectDAL: Pick; kmsService: Pick; + userDAL: Pick; + identityDAL: TIdentityDALFactory; }; export type TDynamicSecretLeaseServiceFactory = ReturnType; @@ -52,8 +58,16 @@ export const dynamicSecretLeaseServiceFactory = ({ dynamicSecretQueueService, projectDAL, licenseService, - kmsService + kmsService, + userDAL, + identityDAL }: TDynamicSecretLeaseServiceFactoryDep) => { + const extractEmailUsername = (email: string) => { + const regex = new RE2(/^([^@]+)/); + const match = email.match(regex); + return match ? match[1] : email; + }; + const create = async ({ environmentSlug, path, @@ -132,10 +146,24 @@ export const dynamicSecretLeaseServiceFactory = ({ let result; try { + const identity: { name: string } = { name: "" }; + if (actor === ActorType.USER) { + const user = await userDAL.findById(actorId); + if (user) { + identity.name = extractEmailUsername(user.username); + } + } else if (actor === ActorType.Machine) { + const machineIdentity = await identityDAL.findById(actorId); + if (machineIdentity) { + identity.name = machineIdentity.name; + } + } result = await selectedProvider.create({ inputs: decryptedStoredInput, expireAt: expireAt.getTime(), - usernameTemplate: dynamicSecretCfg.usernameTemplate + usernameTemplate: dynamicSecretCfg.usernameTemplate, + identity, + metadata: { projectId } }); } catch (error: unknown) { if (error && typeof error === "object" && error !== null && "sqlMessage" in error) { @@ -237,7 +265,8 @@ export const dynamicSecretLeaseServiceFactory = ({ const { entityId } = await selectedProvider.renew( decryptedStoredInput, dynamicSecretLease.externalEntityId, - expireAt.getTime() + expireAt.getTime(), + { projectId } ); await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); @@ -313,7 +342,7 @@ export const dynamicSecretLeaseServiceFactory = ({ ) as object; const revokeResponse = await selectedProvider - .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId) + .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { projectId }) .catch(async (err) => { // only propogate this error if forced is false if (!isForced) return { error: err as Error }; 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 16ac10716..b502bf9f3 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -116,7 +116,7 @@ export const dynamicSecretServiceFactory = ({ throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" }); const selectedProvider = dynamicSecretProviders[provider.type]; - const inputs = await selectedProvider.validateProviderInputs(provider.inputs); + const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId }); let selectedGatewayId: string | null = null; if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) { @@ -146,7 +146,7 @@ export const dynamicSecretServiceFactory = ({ selectedGatewayId = gateway.id; } - const isConnected = await selectedProvider.validateConnection(provider.inputs); + const isConnected = await selectedProvider.validateConnection(provider.inputs, { projectId }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ @@ -272,7 +272,7 @@ export const dynamicSecretServiceFactory = ({ secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; - const updatedInput = await selectedProvider.validateProviderInputs(newInput); + const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId }); let selectedGatewayId: string | null = null; if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) { @@ -301,7 +301,7 @@ export const dynamicSecretServiceFactory = ({ selectedGatewayId = gateway.id; } - const isConnected = await selectedProvider.validateConnection(newInput); + const isConnected = await selectedProvider.validateConnection(newInput, { projectId }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => { @@ -472,7 +472,9 @@ export const dynamicSecretServiceFactory = ({ secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; + const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput, { + projectId + })) as object; return { ...dynamicSecretCfg, inputs: providerInputs }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts index 56fa110d1..89371f1bd 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts @@ -16,6 +16,7 @@ import { BadRequestError } from "@app/lib/errors"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const CreateElastiCacheUserSchema = z.object({ UserId: z.string().trim().min(1), @@ -132,14 +133,14 @@ const generatePassword = () => { return customAlphabet(charset, 64)(); }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-"; const randomUsername = `inf-${customAlphabet(charset, 32)()}`; if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -174,14 +175,21 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => { return true; }; - const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { - const { inputs, expireAt, usernameTemplate } = data; + const create = async (data: { + inputs: unknown; + expireAt: number; + usernameTemplate?: string | null; + identity?: { + name: string; + }; + }) => { + const { inputs, expireAt, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); if (!(await validateConnection(providerInputs))) { throw new BadRequestError({ message: "Failed to establish connection" }); } - const leaseUsername = generateUsername(usernameTemplate); + const leaseUsername = generateUsername(usernameTemplate, identity); const leasePassword = generatePassword(); const leaseExpiration = new Date(expireAt).toISOString(); diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts index 9d8e10f60..f7383d4ac 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -16,21 +16,25 @@ import { PutUserPolicyCommand, RemoveUserFromGroupCommand } from "@aws-sdk/client-iam"; -import handlebars from "handlebars"; +import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; +import { randomUUID } from "crypto"; import { z } from "zod"; +import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; +import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); if (!usernameTemplate) return randomUsername; - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -40,7 +44,43 @@ export const AwsIamProvider = (): TDynamicProviderFns => { return providerInputs; }; - const $getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer, projectId: string) => { + const appCfg = getConfig(); + if (providerInputs.method === AwsIamAuthType.AssumeRole) { + const stsClient = new STSClient({ + region: providerInputs.region, + credentials: + appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY + ? { + accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID, + secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY + } + : undefined // if hosting on AWS + }); + + const command = new AssumeRoleCommand({ + RoleArn: providerInputs.roleArn, + RoleSessionName: `infisical-dynamic-secret-${randomUUID()}`, + DurationSeconds: 900, // 15 mins + ExternalId: projectId + }); + + const assumeRes = await stsClient.send(command); + + if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) { + throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" }); + } + const client = new IAMClient({ + region: providerInputs.region, + credentials: { + accessKeyId: assumeRes.Credentials?.AccessKeyId, + secretAccessKey: assumeRes.Credentials?.SecretAccessKey, + sessionToken: assumeRes.Credentials?.SessionToken + } + }); + return client; + } + const client = new IAMClient({ region: providerInputs.region, credentials: { @@ -52,21 +92,41 @@ export const AwsIamProvider = (): TDynamicProviderFns => { return client; }; - const validateConnection = async (inputs: unknown) => { + const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => { const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs); - - const isConnected = await client.send(new GetUserCommand({})).then(() => true); + const client = await $getClient(providerInputs, projectId); + const isConnected = await client + .send(new GetUserCommand({})) + .then(() => true) + .catch((err) => { + const message = (err as Error)?.message; + if ( + providerInputs.method === AwsIamAuthType.AssumeRole && + // assume role will throw an error asking to provider username, but if so this has access in aws correctly + message.includes("Must specify userName when calling with non-User credentials") + ) { + return true; + } + throw err; + }); return isConnected; }; - const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { - const { inputs, usernameTemplate } = data; + const create = async (data: { + inputs: unknown; + expireAt: number; + usernameTemplate?: string | null; + identity?: { + name: string; + }; + metadata: { projectId: string }; + }) => { + const { inputs, usernameTemplate, metadata, identity } = data; const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs); + const client = await $getClient(providerInputs, metadata.projectId); - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; const createUserRes = await client.send( new CreateUserCommand({ @@ -76,6 +136,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { UserName: username }) ); + if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" }); if (userGroups) { await Promise.all( @@ -125,9 +186,9 @@ export const AwsIamProvider = (): TDynamicProviderFns => { }; }; - const revoke = async (inputs: unknown, entityId: string) => { + const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => { const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs); + const client = await $getClient(providerInputs, metadata.projectId); const username = entityId; diff --git a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts index fce23b56f..b939dcad6 100644 --- a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts +++ b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts @@ -8,19 +8,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const generatePassword = (size = 48) => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 48)(size); }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-" if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -75,12 +76,17 @@ export const CassandraProvider = (): TDynamicProviderFns => { return isConnected; }; - const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { - const { inputs, expireAt, usernameTemplate } = data; + const create = async (data: { + inputs: unknown; + expireAt: number; + usernameTemplate?: string | null; + identity?: { name: string }; + }) => { + const { inputs, expireAt, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); const client = await $getClient(providerInputs); - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); const { keyspace } = providerInputs; const expiration = new Date(expireAt).toISOString(); diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index 066822827..32d21ee76 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -1,5 +1,4 @@ import { Client as ElasticSearchClient } from "@elastic/elasticsearch"; -import handlebars from "handlebars"; import { customAlphabet } from "nanoid"; import { z } from "zod"; @@ -7,19 +6,20 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const generatePassword = () => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 64)(); }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-" if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -71,12 +71,12 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { return infoResponse; }; - const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => { - const { inputs, usernameTemplate } = data; + const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => { + const { inputs, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); const connection = await $getClient(providerInputs); - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); await connection.security.putUser({ diff --git a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts index 130e0fa92..cf8f2b3e0 100644 --- a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts +++ b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts @@ -1,13 +1,21 @@ import axios from "axios"; +import handlebars from "handlebars"; import https from "https"; import { InternalServerError } from "@app/lib/errors"; -import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types"; import { TGatewayServiceFactory } from "../../gateway/gateway-service"; -import { DynamicSecretKubernetesSchema, TDynamicProviderFns } from "./models"; +import { + DynamicSecretKubernetesSchema, + KubernetesAuthMethod, + KubernetesCredentialType, + KubernetesRoleType, + TDynamicProviderFns +} from "./models"; const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; @@ -15,6 +23,16 @@ type TKubernetesProviderDTO = { gatewayService: Pick; }; +const generateUsername = (usernameTemplate?: string | null) => { + const randomUsername = `dynamic-secret-sa-${alphaNumericNanoId(10).toLowerCase()}`; + if (!usernameTemplate) return randomUsername; + + return handlebars.compile(usernameTemplate)({ + randomUsername, + unixTimestamp: Math.floor(Date.now() / 100) + }); +}; + export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs); @@ -30,20 +48,27 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): gatewayId: string; targetHost: string; targetPort: number; + caCert?: string; + reviewTokenThroughGateway: boolean; + enableSsl: boolean; }, - gatewayCallback: (host: string, port: number) => Promise + gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise ): Promise => { const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); const callbackResult = await withGatewayProxy( - async (port) => { + async (port, httpsAgent) => { // Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server" - const res = await gatewayCallback("https://localhost", port); + const res = await gatewayCallback( + inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost", + port, + httpsAgent + ); return res; }, { - protocol: GatewayProxyProtocol.Tcp, + protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp, targetHost: inputs.targetHost, targetPort: inputs.targetPort, relayHost, @@ -54,7 +79,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): ca: relayDetails.certChain, cert: relayDetails.certificate, key: relayDetails.privateKey.toString() - } + }, + // we always pass this, because its needed for both tcp and http protocol + httpsAgent: new https.Agent({ + ca: inputs.caCert, + rejectUnauthorized: inputs.enableSsl + }) } ); @@ -64,7 +94,151 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const serviceAccountGetCallback = async (host: string, port: number) => { + const serviceAccountDynamicCallback = async (host: string, port: number, httpsAgent?: https.Agent) => { + if (providerInputs.credentialType !== KubernetesCredentialType.Dynamic) { + throw new Error("invalid callback"); + } + + const baseUrl = port ? `${host}:${port}` : host; + const serviceAccountName = generateUsername(); + const roleBindingName = `${serviceAccountName}-role-binding`; + + // 1. Create a test service account + await axios.post( + `${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts`, + { + metadata: { + name: serviceAccountName, + namespace: providerInputs.namespace + } + }, + { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + } + ); + + // 2. Create a test role binding + const roleBindingUrl = + providerInputs.roleType === KubernetesRoleType.ClusterRole + ? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings` + : `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`; + + const roleBindingMetadata = { + name: roleBindingName, + ...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace: providerInputs.namespace }) + }; + + await axios.post( + roleBindingUrl, + { + metadata: roleBindingMetadata, + roleRef: { + kind: providerInputs.roleType === KubernetesRoleType.ClusterRole ? "ClusterRole" : "Role", + name: providerInputs.role, + apiGroup: "rbac.authorization.k8s.io" + }, + subjects: [ + { + kind: "ServiceAccount", + name: serviceAccountName, + namespace: providerInputs.namespace + } + ] + }, + { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + } + ); + + // 3. Request a token for the test service account + await axios.post( + `${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`, + { + spec: { + expirationSeconds: 600, // 10 minutes + ...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {}) + } + }, + { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + } + ); + + // 4. Cleanup: delete role binding and service account + if (providerInputs.roleType === KubernetesRoleType.Role) { + await axios.delete( + `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${roleBindingName}`, + { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + } + ); + } else { + await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + }); + } + + await axios.delete( + `${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}`, + { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + } + ); + }; + + const serviceAccountStaticCallback = async (host: string, port: number, httpsAgent?: https.Agent) => { + if (providerInputs.credentialType !== KubernetesCredentialType.Static) { + throw new Error("invalid callback"); + } + const baseUrl = port ? `${host}:${port}` : host; await axios.get( @@ -72,36 +246,57 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): { headers: { "Content-Type": "application/json", - Authorization: `Bearer ${providerInputs.clusterToken}` + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) }, signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), timeout: EXTERNAL_REQUEST_TIMEOUT, - httpsAgent: new https.Agent({ - ca: providerInputs.ca, - rejectUnauthorized: providerInputs.sslEnabled - }) + httpsAgent } ); }; const url = new URL(providerInputs.url); + const k8sGatewayHost = url.hostname; const k8sPort = url.port ? Number(url.port) : 443; + const k8sHost = `${url.protocol}//${url.hostname}`; try { if (providerInputs.gatewayId) { - const k8sHost = url.hostname; - - await $gatewayProxyWrapper( - { - gatewayId: providerInputs.gatewayId, - targetHost: k8sHost, - targetPort: k8sPort - }, - serviceAccountGetCallback - ); + if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { + await $gatewayProxyWrapper( + { + gatewayId: providerInputs.gatewayId, + targetHost: k8sHost, + targetPort: k8sPort, + enableSsl: providerInputs.sslEnabled, + caCert: providerInputs.ca, + reviewTokenThroughGateway: true + }, + providerInputs.credentialType === KubernetesCredentialType.Static + ? serviceAccountStaticCallback + : serviceAccountDynamicCallback + ); + } else { + await $gatewayProxyWrapper( + { + gatewayId: providerInputs.gatewayId, + targetHost: k8sGatewayHost, + targetPort: k8sPort, + enableSsl: providerInputs.sslEnabled, + caCert: providerInputs.ca, + reviewTokenThroughGateway: false + }, + providerInputs.credentialType === KubernetesCredentialType.Static + ? serviceAccountStaticCallback + : serviceAccountDynamicCallback + ); + } + } else if (providerInputs.credentialType === KubernetesCredentialType.Static) { + await serviceAccountStaticCallback(k8sHost, k8sPort); } else { - const k8sHost = `${url.protocol}//${url.hostname}`; - await serviceAccountGetCallback(k8sHost, k8sPort); + await serviceAccountDynamicCallback(k8sHost, k8sPort); } return true; @@ -117,10 +312,119 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): } }; - const create = async ({ inputs, expireAt }: { inputs: unknown; expireAt: number }) => { + const create = async ({ + inputs, + expireAt, + usernameTemplate + }: { + inputs: unknown; + expireAt: number; + usernameTemplate?: string | null; + }) => { const providerInputs = await validateProviderInputs(inputs); - const tokenRequestCallback = async (host: string, port: number) => { + const serviceAccountDynamicCallback = async (host: string, port: number, httpsAgent?: https.Agent) => { + if (providerInputs.credentialType !== KubernetesCredentialType.Dynamic) { + throw new Error("invalid callback"); + } + + const baseUrl = port ? `${host}:${port}` : host; + const serviceAccountName = generateUsername(usernameTemplate); + const roleBindingName = `${serviceAccountName}-role-binding`; + + // 1. Create the service account + await axios.post( + `${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts`, + { + metadata: { + name: serviceAccountName, + namespace: providerInputs.namespace + } + }, + { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + } + ); + + // 2. Create the role binding + const roleBindingUrl = + providerInputs.roleType === KubernetesRoleType.ClusterRole + ? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings` + : `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`; + + const roleBindingMetadata = { + name: roleBindingName, + ...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace: providerInputs.namespace }) + }; + + await axios.post( + roleBindingUrl, + { + metadata: roleBindingMetadata, + roleRef: { + kind: providerInputs.roleType === KubernetesRoleType.ClusterRole ? "ClusterRole" : "Role", + name: providerInputs.role, + apiGroup: "rbac.authorization.k8s.io" + }, + subjects: [ + { + kind: "ServiceAccount", + name: serviceAccountName, + namespace: providerInputs.namespace + } + ] + }, + { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + } + ); + + // 3. Request a token for the service account + const res = await axios.post( + `${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`, + { + spec: { + expirationSeconds: Math.floor((expireAt - Date.now()) / 1000), + ...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {}) + } + }, + { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + } + ); + + return { ...res.data, serviceAccountName }; + }; + + const tokenRequestStaticCallback = async (host: string, port: number, httpsAgent?: https.Agent) => { + if (providerInputs.credentialType !== KubernetesCredentialType.Static) { + throw new Error("invalid callback"); + } + const baseUrl = port ? `${host}:${port}` : host; const res = await axios.post( @@ -134,18 +438,17 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): { headers: { "Content-Type": "application/json", - Authorization: `Bearer ${providerInputs.clusterToken}` + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) }, signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), timeout: EXTERNAL_REQUEST_TIMEOUT, - httpsAgent: new https.Agent({ - ca: providerInputs.ca, - rejectUnauthorized: providerInputs.sslEnabled - }) + httpsAgent } ); - return res.data; + return { ...res.data, serviceAccountName: providerInputs.serviceAccountName }; }; const url = new URL(providerInputs.url); @@ -154,19 +457,46 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): const k8sPort = url.port ? Number(url.port) : 443; try { - const tokenData = providerInputs.gatewayId - ? await $gatewayProxyWrapper( + let tokenData; + if (providerInputs.gatewayId) { + if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { + tokenData = await $gatewayProxyWrapper( + { + gatewayId: providerInputs.gatewayId, + targetHost: k8sHost, + targetPort: k8sPort, + enableSsl: providerInputs.sslEnabled, + caCert: providerInputs.ca, + reviewTokenThroughGateway: true + }, + providerInputs.credentialType === KubernetesCredentialType.Static + ? tokenRequestStaticCallback + : serviceAccountDynamicCallback + ); + } else { + tokenData = await $gatewayProxyWrapper( { gatewayId: providerInputs.gatewayId, targetHost: k8sGatewayHost, - targetPort: k8sPort + targetPort: k8sPort, + enableSsl: providerInputs.sslEnabled, + caCert: providerInputs.ca, + reviewTokenThroughGateway: false }, - tokenRequestCallback - ) - : await tokenRequestCallback(k8sHost, k8sPort); + providerInputs.credentialType === KubernetesCredentialType.Static + ? tokenRequestStaticCallback + : serviceAccountDynamicCallback + ); + } + } else { + tokenData = + providerInputs.credentialType === KubernetesCredentialType.Static + ? await tokenRequestStaticCallback(k8sHost, k8sPort) + : await serviceAccountDynamicCallback(k8sHost, k8sPort); + } return { - entityId: providerInputs.serviceAccountName, + entityId: tokenData.serviceAccountName, data: { TOKEN: tokenData.status.token } }; } catch (error) { @@ -181,7 +511,97 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): } }; - const revoke = async (_inputs: unknown, entityId: string) => { + const revoke = async (inputs: unknown, entityId: string) => { + const providerInputs = await validateProviderInputs(inputs); + + const serviceAccountDynamicCallback = async (host: string, port: number, httpsAgent?: https.Agent) => { + if (providerInputs.credentialType !== KubernetesCredentialType.Dynamic) { + throw new Error("invalid callback"); + } + + const baseUrl = port ? `${host}:${port}` : host; + const roleBindingName = `${entityId}-role-binding`; + + if (providerInputs.roleType === KubernetesRoleType.Role) { + await axios.delete( + `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${roleBindingName}`, + { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + } + ); + } else { + await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + }); + } + + // Delete the service account + await axios.delete(`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${entityId}`, { + headers: { + "Content-Type": "application/json", + ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } + : { Authorization: `Bearer ${providerInputs.clusterToken}` }) + }, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), + timeout: EXTERNAL_REQUEST_TIMEOUT, + httpsAgent + }); + }; + + if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) { + const url = new URL(providerInputs.url); + const k8sGatewayHost = url.hostname; + const k8sPort = url.port ? Number(url.port) : 443; + const k8sHost = `${url.protocol}//${url.hostname}`; + + if (providerInputs.gatewayId) { + if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { + await $gatewayProxyWrapper( + { + gatewayId: providerInputs.gatewayId, + targetHost: k8sHost, + targetPort: k8sPort, + enableSsl: providerInputs.sslEnabled, + caCert: providerInputs.ca, + reviewTokenThroughGateway: true + }, + serviceAccountDynamicCallback + ); + } else { + await $gatewayProxyWrapper( + { + gatewayId: providerInputs.gatewayId, + targetHost: k8sGatewayHost, + targetPort: k8sPort, + enableSsl: providerInputs.sslEnabled, + caCert: providerInputs.ca, + reviewTokenThroughGateway: false + }, + serviceAccountDynamicCallback + ); + } + } else { + await serviceAccountDynamicCallback(k8sHost, k8sPort); + } + } + return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/ldap.ts b/backend/src/ee/services/dynamic-secret/providers/ldap.ts index d0e3fbe66..1de8aa1e6 100644 --- a/backend/src/ee/services/dynamic-secret/providers/ldap.ts +++ b/backend/src/ee/services/dynamic-secret/providers/ldap.ts @@ -9,6 +9,7 @@ import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const generatePassword = () => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; @@ -22,13 +23,13 @@ const encodePassword = (password?: string) => { return base64Password; }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-" if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -196,8 +197,8 @@ export const LdapProvider = (): TDynamicProviderFns => { return dnArray; }; - const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => { - const { inputs, usernameTemplate } = data; + const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => { + const { inputs, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); const client = await $getClient(providerInputs); @@ -224,7 +225,7 @@ export const LdapProvider = (): TDynamicProviderFns => { }); } } else { - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 91d26da32..b2496eebd 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -20,6 +20,11 @@ export enum SqlProviders { Vertica = "vertica" } +export enum AwsIamAuthType { + AssumeRole = "assume-role", + AccessKey = "access-key" +} + export enum ElasticSearchAuthTypes { User = "user", ApiKey = "api-key" @@ -31,7 +36,18 @@ export enum LdapCredentialType { } export enum KubernetesCredentialType { - Static = "static" + Static = "static", + Dynamic = "dynamic" +} + +export enum KubernetesRoleType { + ClusterRole = "cluster-role", + Role = "role" +} + +export enum KubernetesAuthMethod { + Gateway = "gateway", + Api = "api" } export enum TotpConfigType { @@ -168,16 +184,38 @@ export const DynamicSecretSapAseSchema = z.object({ revocationStatement: z.string().trim() }); -export const DynamicSecretAwsIamSchema = z.object({ - accessKey: z.string().trim().min(1), - secretAccessKey: z.string().trim().min(1), - region: z.string().trim().min(1), - awsPath: z.string().trim().optional(), - permissionBoundaryPolicyArn: z.string().trim().optional(), - policyDocument: z.string().trim().optional(), - userGroups: z.string().trim().optional(), - policyArns: z.string().trim().optional() -}); +export const DynamicSecretAwsIamSchema = z.preprocess( + (val) => { + if (typeof val === "object" && val !== null && !Object.hasOwn(val, "method")) { + // eslint-disable-next-line no-param-reassign + (val as { method: string }).method = AwsIamAuthType.AccessKey; + } + return val; + }, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(AwsIamAuthType.AccessKey), + accessKey: z.string().trim().min(1), + secretAccessKey: z.string().trim().min(1), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional() + }), + z.object({ + method: z.literal(AwsIamAuthType.AssumeRole), + roleArn: z.string().trim().min(1, "Role ARN required"), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional() + }) + ]) +); export const DynamicSecretMongoAtlasSchema = z.object({ adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"), @@ -282,17 +320,50 @@ export const LdapSchema = z.union([ }) ]); -export const DynamicSecretKubernetesSchema = z.object({ - url: z.string().url().trim().min(1), - gatewayId: z.string().nullable().optional(), - sslEnabled: z.boolean().default(true), - clusterToken: z.string().trim().min(1), - ca: z.string().optional(), - serviceAccountName: z.string().trim().min(1), - credentialType: z.literal(KubernetesCredentialType.Static), - namespace: z.string().trim().min(1), - audiences: z.array(z.string().trim().min(1)) -}); +export const DynamicSecretKubernetesSchema = z + .discriminatedUnion("credentialType", [ + z.object({ + url: z.string().url().trim().min(1), + clusterToken: z.string().trim().optional(), + ca: z.string().optional(), + sslEnabled: z.boolean().default(false), + credentialType: z.literal(KubernetesCredentialType.Static), + serviceAccountName: z.string().trim().min(1), + namespace: z.string().trim().min(1), + gatewayId: z.string().optional(), + audiences: z.array(z.string().trim().min(1)), + authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api) + }), + z.object({ + url: z.string().url().trim().min(1), + clusterToken: z.string().trim().optional(), + ca: z.string().optional(), + sslEnabled: z.boolean().default(false), + credentialType: z.literal(KubernetesCredentialType.Dynamic), + namespace: z.string().trim().min(1), + gatewayId: z.string().optional(), + audiences: z.array(z.string().trim().min(1)), + roleType: z.nativeEnum(KubernetesRoleType), + role: z.string().trim().min(1), + authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api) + }) + ]) + .superRefine((data, ctx) => { + if (data.authMethod === KubernetesAuthMethod.Gateway && !data.gatewayId) { + ctx.addIssue({ + path: ["gatewayId"], + code: z.ZodIssueCode.custom, + message: "When auth method is set to Gateway, a gateway must be selected" + }); + } + if ((data.authMethod === KubernetesAuthMethod.Api || !data.authMethod) && !data.clusterToken) { + ctx.addIssue({ + path: ["clusterToken"], + code: z.ZodIssueCode.custom, + message: "When auth method is set to Manual Token, a cluster token must be provided" + }); + } + }); export const DynamicSecretVerticaSchema = z.object({ host: z.string().trim().toLowerCase(), @@ -400,9 +471,18 @@ export type TDynamicProviderFns = { inputs: unknown; expireAt: number; usernameTemplate?: string | null; + identity?: { + name: string; + }; + metadata: { projectId: string }; }) => Promise<{ entityId: string; data: unknown }>; - validateConnection: (inputs: unknown) => Promise; - validateProviderInputs: (inputs: object) => Promise; - revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>; - renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>; + validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise; + validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise; + revoke: (inputs: unknown, entityId: string, metadata: { projectId: string }) => Promise<{ entityId: string }>; + renew: ( + inputs: unknown, + entityId: string, + expireAt: number, + metadata: { projectId: string } + ) => Promise<{ entityId: string }>; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts index 8f8bf9430..6da8b4b4e 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts @@ -1,5 +1,4 @@ import axios, { AxiosError } from "axios"; -import handlebars from "handlebars"; import { customAlphabet } from "nanoid"; import { z } from "zod"; @@ -7,19 +6,20 @@ import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const generatePassword = (size = 48) => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 48)(size); }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -64,12 +64,17 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { return isConnected; }; - const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { - const { inputs, expireAt, usernameTemplate } = data; + const create = async (data: { + inputs: unknown; + expireAt: number; + usernameTemplate?: string | null; + identity?: { name: string }; + }) => { + const { inputs, expireAt, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); const client = await $getClient(providerInputs); - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); const expiration = new Date(expireAt).toISOString(); await client({ diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts index 0a15209e0..331a355a7 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts @@ -1,4 +1,3 @@ -import handlebars from "handlebars"; import { MongoClient } from "mongodb"; import { customAlphabet } from "nanoid"; import { z } from "zod"; @@ -7,19 +6,20 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const generatePassword = (size = 48) => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 48)(size); }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -60,12 +60,12 @@ export const MongoDBProvider = (): TDynamicProviderFns => { return isConnected; }; - const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => { - const { inputs, usernameTemplate } = data; + const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => { + const { inputs, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); const client = await $getClient(providerInputs); - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); const db = client.db(providerInputs.database); diff --git a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts index e7d90d272..76081c86c 100644 --- a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -1,5 +1,4 @@ import axios, { Axios } from "axios"; -import handlebars from "handlebars"; import https from "https"; import { customAlphabet } from "nanoid"; import { z } from "zod"; @@ -9,19 +8,20 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const generatePassword = () => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 64)(); }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-" if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -117,12 +117,12 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { return infoResponse; }; - const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => { - const { inputs, usernameTemplate } = data; + const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => { + const { inputs, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); const connection = await $getClient(providerInputs); - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); await createRabbitMqUser({ diff --git a/backend/src/ee/services/dynamic-secret/providers/redis.ts b/backend/src/ee/services/dynamic-secret/providers/redis.ts index 855af2e29..989ed96dc 100644 --- a/backend/src/ee/services/dynamic-secret/providers/redis.ts +++ b/backend/src/ee/services/dynamic-secret/providers/redis.ts @@ -9,19 +9,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const generatePassword = () => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 64)(); }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-" if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -121,12 +122,17 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { return pingResponse; }; - const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { - const { inputs, expireAt, usernameTemplate } = data; + const create = async (data: { + inputs: unknown; + expireAt: number; + usernameTemplate?: string | null; + identity?: { name: string }; + }) => { + const { inputs, expireAt, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); const connection = await $getClient(providerInputs); - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); const expiration = new Date(expireAt).toISOString(); diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts index af2431058..9c13d3efc 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts @@ -9,19 +9,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const generatePassword = (size = 48) => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; return customAlphabet(charset, 48)(size); }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = `inf_${alphaNumericNanoId(25)}`; // Username must start with an ascii letter, so we prepend the username with "inf-" if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -87,11 +88,11 @@ export const SapAseProvider = (): TDynamicProviderFns => { return true; }; - const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => { - const { inputs, usernameTemplate } = data; + const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => { + const { inputs, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); const client = await $getClient(providerInputs); diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts index 654e2d144..5c8a75555 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts @@ -15,19 +15,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const generatePassword = (size = 48) => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; return customAlphabet(charset, 48)(size); }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-" if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -97,11 +98,16 @@ export const SapHanaProvider = (): TDynamicProviderFns => { return testResult; }; - const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { - const { inputs, expireAt, usernameTemplate } = data; + const create = async (data: { + inputs: unknown; + expireAt: number; + usernameTemplate?: string | null; + identity?: { name: string }; + }) => { + const { inputs, expireAt, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); const expiration = new Date(expireAt).toISOString(); diff --git a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts index 571d488c9..9e97ecd30 100644 --- a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts +++ b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts @@ -8,6 +8,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; // destroy client requires callback... const noop = () => {}; @@ -17,13 +18,13 @@ const generatePassword = (size = 48) => { return customAlphabet(charset, 48)(size); }; -const generateUsername = (usernameTemplate?: string | null) => { +const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = `infisical_${alphaNumericNanoId(32)}`; // Username must start with an ascii letter, so we prepend the username with "inf-" if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity }); }; @@ -88,13 +89,18 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { return isValidConnection; }; - const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { - const { inputs, expireAt, usernameTemplate } = data; + const create = async (data: { + inputs: unknown; + expireAt: number; + usernameTemplate?: string | null; + identity?: { name: string }; + }) => { + const { inputs, expireAt, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); const client = await $getClient(providerInputs); - const username = generateUsername(usernameTemplate); + const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); try { 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 39f8dd6de..d3217be37 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -10,6 +10,7 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars import { TGatewayServiceFactory } from "../../gateway/gateway-service"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models"; +import { compileUsernameTemplate } from "./templateUtils"; const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; @@ -104,9 +105,8 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire } }; -const generateUsername = (provider: SqlProviders, usernameTemplate?: string | null) => { +const generateUsername = (provider: SqlProviders, usernameTemplate?: string | null, identity?: { name: string }) => { let randomUsername = ""; - // For oracle, the client assumes everything is upper case when not using quotes around the password if (provider === SqlProviders.Oracle) { randomUsername = alphaNumericNanoId(32).toUpperCase(); @@ -114,10 +114,13 @@ const generateUsername = (provider: SqlProviders, usernameTemplate?: string | nu randomUsername = alphaNumericNanoId(32); } if (!usernameTemplate) return randomUsername; - - return handlebars.compile(usernameTemplate)({ + return compileUsernameTemplate({ + usernameTemplate, randomUsername, - unixTimestamp: Math.floor(Date.now() / 100) + identity, + options: { + toUpperCase: provider === SqlProviders.Oracle + } }); }; @@ -221,11 +224,16 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) return isConnected; }; - const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { - const { inputs, expireAt, usernameTemplate } = data; + const create = async (data: { + inputs: unknown; + expireAt: number; + usernameTemplate?: string | null; + identity?: { name: string }; + }) => { + const { inputs, expireAt, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); - const username = generateUsername(providerInputs.client, usernameTemplate); + const username = generateUsername(providerInputs.client, usernameTemplate, identity); const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements); const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/templateUtils.ts b/backend/src/ee/services/dynamic-secret/providers/templateUtils.ts new file mode 100644 index 000000000..70a083dbf --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/templateUtils.ts @@ -0,0 +1,80 @@ +/* eslint-disable func-names */ +import handlebars from "handlebars"; +import RE2 from "re2"; + +import { logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +export const compileUsernameTemplate = ({ + usernameTemplate, + randomUsername, + identity, + unixTimestamp, + options +}: { + usernameTemplate: string; + randomUsername: string; + identity?: { name: string }; + unixTimestamp?: number; + options?: { + toUpperCase?: boolean; + }; +}): string => { + // Create isolated handlebars instance + const hbs = handlebars.create(); + + // Register random helper on local instance + hbs.registerHelper("random", function (length: number) { + if (typeof length !== "number" || length <= 0 || length > 100) { + return ""; + } + return alphaNumericNanoId(length); + }); + + // Register replace helper on local instance + hbs.registerHelper("replace", function (text: string, searchValue: string, replaceValue: string) { + // Convert to string if it's not already + const textStr = String(text || ""); + if (!textStr) { + return textStr; + } + + try { + const re2Pattern = new RE2(searchValue, "g"); + // Replace all occurrences + return re2Pattern.replace(textStr, replaceValue); + } catch (error) { + logger.error(error, "RE2 pattern failed, using original template"); + return textStr; + } + }); + + // Register truncate helper on local instance + hbs.registerHelper("truncate", function (text: string, length: number) { + // Convert to string if it's not already + const textStr = String(text || ""); + if (!textStr) { + return textStr; + } + + if (typeof length !== "number" || length <= 0) return textStr; + return textStr.substring(0, length); + }); + + // Compile template with context using local instance + const context = { + randomUsername, + unixTimestamp: unixTimestamp || Math.floor(Date.now() / 100), + identity: { + name: identity?.name + } + }; + + const result = hbs.compile(usernameTemplate)(context); + + if (options?.toUpperCase) { + return result.toUpperCase(); + } + + return result; +}; diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index 9424075ca..1d7c5fc71 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -42,6 +42,10 @@ export type TListGroupUsersDTO = { filter?: EFilterReturnedUsers; } & TGenericPermission; +export type TListProjectGroupUsersDTO = TListGroupUsersDTO & { + projectId: string; +}; + export type TAddUserToGroupDTO = { id: string; username: string; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3734cbf21..a10fcc67e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -89,6 +89,7 @@ export const GROUPS = { limit: "The number of users to return.", username: "The username to search for.", search: "The text string that user email or name will be filtered by.", + projectId: "The ID of the project the group belongs to.", filterUsers: "Whether to filter the list of returned users. 'existingMembers' will only return existing users in the group, 'nonMembers' will only return users not in the group, undefined will return all users in the organization." }, @@ -2276,7 +2277,8 @@ export const SecretSyncs = { }, GCP: { scope: "The Google project scope that secrets should be synced to.", - projectId: "The ID of the Google project secrets should be synced to." + projectId: "The ID of the Google project secrets should be synced to.", + locationId: 'The ID of the Google project location secrets should be synced to (ie "us-west4").' }, DATABRICKS: { scope: "The Databricks secret scope that secrets should be synced to." diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index cb53a71a6..e2fc73d8a 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -213,6 +213,12 @@ const envSchema = z GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()), DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"), + DYNAMIC_SECRET_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()).default( + process.env.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID + ), + DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()).default( + process.env.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY + ), /* ----------------------------------------------------------------------------- */ /* App Connections ----------------------------------------------------------------------------- */ diff --git a/backend/src/lib/template/validate-handlebars.ts b/backend/src/lib/template/validate-handlebars.ts index 08343e962..4aa0d1f63 100644 --- a/backend/src/lib/template/validate-handlebars.ts +++ b/backend/src/lib/template/validate-handlebars.ts @@ -7,13 +7,24 @@ type SanitizationArg = { allowedExpressions?: (arg: string) => boolean; }; +const isValidExpression = (expression: string, dto: SanitizationArg): boolean => { + // Allow helper functions (replace, truncate) + const allowedHelpers = ["replace", "truncate", "random"]; + if (allowedHelpers.includes(expression)) { + return true; + } + + // Check regular allowed expressions + return dto?.allowedExpressions?.(expression) || false; +}; + export const validateHandlebarTemplate = (templateName: string, template: string, dto: SanitizationArg) => { const parsedAst = handlebars.parse(template); parsedAst.body.forEach((el) => { if (el.type === "ContentStatement") return; if (el.type === "MustacheStatement" && "path" in el) { const { path } = el as { type: "MustacheStatement"; path: { type: "PathExpression"; original: string } }; - if (path.type === "PathExpression" && dto?.allowedExpressions?.(path.original)) return; + if (path.type === "PathExpression" && isValidExpression(path.original, dto)) return; } logger.error(el, "Template sanitization failed"); throw new BadRequestError({ message: `Template sanitization failed: ${templateName}` }); @@ -26,7 +37,7 @@ export const isValidHandleBarTemplate = (template: string, dto: SanitizationArg) if (el.type === "ContentStatement") return true; if (el.type === "MustacheStatement" && "path" in el) { const { path } = el as { type: "MustacheStatement"; path: { type: "PathExpression"; original: string } }; - if (path.type === "PathExpression" && dto?.allowedExpressions?.(path.original)) return true; + if (path.type === "PathExpression" && isValidExpression(path.original, dto)) return true; } return false; }); diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index afea5c9f9..f065bfbed 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -155,6 +155,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { oidc: token?.identityAuth?.oidc }); } + if (token?.identityAuth?.kubernetes) { + requestContext.set("identityAuthInfo", { + identityId: identity.identityId, + kubernetes: token?.identityAuth?.kubernetes + }); + } break; } case AuthMode.SERVICE_TOKEN: { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 509a2ef1f..aa38bb0e8 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1516,7 +1516,9 @@ export const registerRoutes = async ( dynamicSecretProviders, folderDAL, licenseService, - kmsService + kmsService, + userDAL, + identityDAL }); const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, diff --git a/backend/src/server/routes/v1/app-connection-routers/gcp-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/gcp-connection-router.ts index f92d5e668..c88308c64 100644 --- a/backend/src/server/routes/v1/app-connection-routers/gcp-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/gcp-connection-router.ts @@ -45,4 +45,37 @@ export const registerGcpConnectionRouter = async (server: FastifyZodProvider) => return projects; } }); + + server.route({ + method: "GET", + url: `/:connectionId/secret-manager-project-locations`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + projectId: z.string() + }), + response: { + 200: z.object({ displayName: z.string(), locationId: z.string() }).array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { + params: { connectionId }, + query: { projectId } + } = req; + + const locations = await server.services.appConnection.gcp.listSecretManagerProjectLocations( + { connectionId, projectId }, + req.permission + ); + + return locations; + } + }); }; diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts index 5a081a3d9..d07e3bd8b 100644 --- a/backend/src/server/routes/v2/group-project-router.ts +++ b/backend/src/server/routes/v2/group-project-router.ts @@ -4,9 +4,11 @@ import { GroupProjectMembershipsSchema, GroupsSchema, ProjectMembershipRole, - ProjectUserMembershipRolesSchema + ProjectUserMembershipRolesSchema, + UsersSchema } from "@app/db/schemas"; -import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; +import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; +import { ApiDocsTags, GROUPS, PROJECTS } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -301,4 +303,61 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => return { groupMembership }; } }); + + server.route({ + method: "GET", + url: "/:projectId/groups/:groupId/users", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], + description: "Return project group users", + params: z.object({ + projectId: z.string().trim().describe(GROUPS.LIST_USERS.projectId), + groupId: z.string().trim().describe(GROUPS.LIST_USERS.id) + }), + querystring: z.object({ + offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset), + limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit), + username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username), + search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search), + filter: z.nativeEnum(EFilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers) + }), + response: { + 200: z.object({ + users: UsersSchema.pick({ + email: true, + username: true, + firstName: true, + lastName: true, + id: true + }) + .merge( + z.object({ + isPartOfGroup: z.boolean(), + joinedGroupAt: z.date().nullable() + }) + ) + .array(), + totalCount: z.number() + }) + } + }, + handler: async (req) => { + const { users, totalCount } = await server.services.groupProject.listProjectGroupUsers({ + id: req.params.groupId, + projectId: req.params.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + return { users, totalCount }; + } + }); }; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts index 8bde74062..d1533b696 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts @@ -11,8 +11,10 @@ import { AppConnection } from "../app-connection-enums"; import { GcpConnectionMethod } from "./gcp-connection-enums"; import { GCPApp, + GCPGetProjectLocationsRes, GCPGetProjectsRes, GCPGetServiceRes, + GCPLocation, TGcpConnection, TGcpConnectionConfig } from "./gcp-connection-types"; @@ -145,6 +147,45 @@ export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) return projects; }; +export const getGcpSecretManagerProjectLocations = async (projectId: string, appConnection: TGcpConnection) => { + const accessToken = await getGcpConnectionAuthToken(appConnection); + + let gcpLocations: GCPLocation[] = []; + + const pageSize = 100; + let pageToken: string | undefined; + let hasMorePages = true; + + while (hasMorePages) { + const params = new URLSearchParams({ + pageSize: String(pageSize), + ...(pageToken ? { pageToken } : {}) + }); + + // eslint-disable-next-line no-await-in-loop + const { data } = await request.get( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${projectId}/locations`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + gcpLocations = gcpLocations.concat(data.locations); + + if (!data.nextPageToken) { + hasMorePages = false; + } + + pageToken = data.nextPageToken; + } + + return gcpLocations.sort((a, b) => a.displayName.localeCompare(b.displayName)); +}; + export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => { // Check if provided service account email suffix matches organization ID. // We do this to mitigate confused deputy attacks in multi-tenant instances diff --git a/backend/src/services/app-connection/gcp/gcp-connection-service.ts b/backend/src/services/app-connection/gcp/gcp-connection-service.ts index 96b795a8f..74f2ab2c4 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-service.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-service.ts @@ -1,8 +1,8 @@ import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; -import { getGcpSecretManagerProjects } from "./gcp-connection-fns"; -import { TGcpConnection } from "./gcp-connection-types"; +import { getGcpSecretManagerProjectLocations, getGcpSecretManagerProjects } from "./gcp-connection-fns"; +import { TGcpConnection, TGetGCPProjectLocationsDTO } from "./gcp-connection-types"; type TGetAppConnectionFunc = ( app: AppConnection, @@ -23,7 +23,23 @@ export const gcpConnectionService = (getAppConnection: TGetAppConnectionFunc) => } }; + const listSecretManagerProjectLocations = async ( + { connectionId, projectId }: TGetGCPProjectLocationsDTO, + actor: OrgServiceActor + ) => { + const appConnection = await getAppConnection(AppConnection.GCP, connectionId, actor); + + try { + const locations = await getGcpSecretManagerProjectLocations(projectId, appConnection); + + return locations; + } catch (error) { + return []; + } + }; + return { - listSecretManagerProjects + listSecretManagerProjects, + listSecretManagerProjectLocations }; }; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-types.ts b/backend/src/services/app-connection/gcp/gcp-connection-types.ts index 2bb518820..4dc4bd131 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-types.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-types.ts @@ -38,6 +38,22 @@ export type GCPGetProjectsRes = { nextPageToken?: string; }; +export type GCPLocation = { + name: string; + locationId: string; + displayName: string; +}; + +export type GCPGetProjectLocationsRes = { + locations: GCPLocation[]; + nextPageToken?: string; +}; + +export type TGetGCPProjectLocationsDTO = { + projectId: string; + connectionId: string; +}; + export type GCPGetServiceRes = { name: string; parent: string; diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index bee85b14c..64ba573d5 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -397,7 +397,7 @@ export const authLoginServiceFactory = ({ // Check if the user actually has access to the specified organization. const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); - const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId); + const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId && org.userStatus !== "invited"); const selectedOrg = await orgDAL.findById(organizationId); if (!hasOrganizationMembership) { diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 1ff2c78d2..a793ecfab 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; +import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation @@ -42,7 +43,7 @@ type TGroupProjectServiceFactoryDep = { projectKeyDAL: Pick; projectRoleDAL: Pick; projectBotDAL: TProjectBotDALFactory; - groupDAL: Pick; + groupDAL: Pick; permissionService: Pick; }; @@ -471,11 +472,54 @@ export const groupProjectServiceFactory = ({ return groupMembership; }; + const listProjectGroupUsers = async ({ + id, + projectId, + offset, + limit, + username, + actor, + actorId, + actorAuthMethod, + actorOrgId, + search, + filter + }: TListProjectGroupUsersDTO) => { + const project = await projectDAL.findById(projectId); + + if (!project) { + throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); + + const { members, totalCount } = await groupDAL.findAllGroupPossibleMembers({ + orgId: project.orgId, + groupId: id, + offset, + limit, + username, + search, + filter + }); + + return { users: members, totalCount }; + }; + return { addGroupToProject, updateGroupInProject, removeGroupFromProject, listGroupsInProject, - getGroupInProject + getGroupInProject, + listProjectGroupUsers }; }; diff --git a/backend/src/services/identity-access-token/identity-access-token-types.ts b/backend/src/services/identity-access-token/identity-access-token-types.ts index c97d2f40a..87adfa5dc 100644 --- a/backend/src/services/identity-access-token/identity-access-token-types.ts +++ b/backend/src/services/identity-access-token/identity-access-token-types.ts @@ -11,5 +11,9 @@ export type TIdentityAccessTokenJwtPayload = { oidc?: { claims: Record; }; + kubernetes?: { + namespace: string; + name: string; + }; }; }; 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 b15a2ed4f..a1231c353 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 @@ -416,7 +416,13 @@ export const identityKubernetesAuthServiceFactory = ({ { identityId: identityKubernetesAuth.identityId, identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN, + identityAuth: { + kubernetes: { + namespace: targetNamespace, + name: targetName + } + } } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index c4f0856a1..5730c0d92 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -212,7 +212,7 @@ export const orgDALFactory = (db: TDbClient) => { // special query const findAllOrgsByUserId = async ( userId: string - ): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string })[]> => { + ): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string; userStatus: string })[]> => { try { const org = (await db .replicaNode()(TableName.OrgMembership) @@ -234,6 +234,7 @@ export const orgDALFactory = (db: TDbClient) => { }) .select(selectAllTableCols(TableName.Organization)) .select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole")) + .select(db.ref("status").withSchema(TableName.OrgMembership).as("userStatus")) .select( db.raw(` CASE @@ -242,7 +243,7 @@ export const orgDALFactory = (db: TDbClient) => { ELSE '' END as "orgAuthMethod" `) - )) as (TOrganizations & { orgAuthMethod: string; userRole: string })[]; + )) as (TOrganizations & { orgAuthMethod: string; userRole: string; userStatus: string })[]; return org; } catch (error) { diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 4a72f12e8..63cb8935d 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -183,7 +183,9 @@ export const orgServiceFactory = ({ * */ const findAllOrganizationOfUser = async (userId: string) => { const orgs = await orgDAL.findAllOrgsByUserId(userId); - return orgs; + + // Filter out orgs where the membership object is an invitation + return orgs.filter((org) => org.userStatus !== "invited"); }; /* * Get all workspace members diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts b/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts index 348d2bfa5..389070a9a 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts @@ -1,3 +1,63 @@ export enum GcpSyncScope { - Global = "global" + Global = "global", + Region = "region" +} + +export enum GCPSecretManagerLocation { + // Asia Pacific + ASIA_SOUTHEAST3 = "asia-southeast3", // Bangkok + ASIA_SOUTH2 = "asia-south2", // Delhi + ASIA_EAST2 = "asia-east2", // Hong Kong + ASIA_SOUTHEAST2 = "asia-southeast2", // Jakarta + AUSTRALIA_SOUTHEAST2 = "australia-southeast2", // Melbourne + ASIA_SOUTH1 = "asia-south1", // Mumbai + ASIA_NORTHEAST2 = "asia-northeast2", // Osaka + ASIA_NORTHEAST3 = "asia-northeast3", // Seoul + ASIA_SOUTHEAST1 = "asia-southeast1", // Singapore + AUSTRALIA_SOUTHEAST1 = "australia-southeast1", // Sydney + ASIA_EAST1 = "asia-east1", // Taiwan + ASIA_NORTHEAST1 = "asia-northeast1", // Tokyo + + // Europe + EUROPE_WEST1 = "europe-west1", // Belgium + EUROPE_WEST10 = "europe-west10", // Berlin + EUROPE_NORTH1 = "europe-north1", // Finland + EUROPE_NORTH2 = "europe-north2", // Stockholm + EUROPE_WEST3 = "europe-west3", // Frankfurt + EUROPE_WEST2 = "europe-west2", // London + EUROPE_SOUTHWEST1 = "europe-southwest1", // Madrid + EUROPE_WEST8 = "europe-west8", // Milan + EUROPE_WEST4 = "europe-west4", // Netherlands + EUROPE_WEST12 = "europe-west12", // Turin + EUROPE_WEST9 = "europe-west9", // Paris + EUROPE_CENTRAL2 = "europe-central2", // Warsaw + EUROPE_WEST6 = "europe-west6", // Zurich + + // North America + US_CENTRAL1 = "us-central1", // Iowa + US_WEST4 = "us-west4", // Las Vegas + US_WEST2 = "us-west2", // Los Angeles + NORTHAMERICA_SOUTH1 = "northamerica-south1", // Mexico + NORTHAMERICA_NORTHEAST1 = "northamerica-northeast1", // Montréal + US_EAST4 = "us-east4", // Northern Virginia + US_CENTRAL2 = "us-central2", // Oklahoma + US_WEST1 = "us-west1", // Oregon + US_WEST3 = "us-west3", // Salt Lake City + US_EAST1 = "us-east1", // South Carolina + NORTHAMERICA_NORTHEAST2 = "northamerica-northeast2", // Toronto + US_EAST5 = "us-east5", // Columbus + US_SOUTH1 = "us-south1", // Dallas + US_WEST8 = "us-west8", // Phoenix + + // South America + SOUTHAMERICA_EAST1 = "southamerica-east1", // São Paulo + SOUTHAMERICA_WEST1 = "southamerica-west1", // Santiago + + // Middle East + ME_CENTRAL2 = "me-central2", // Dammam + ME_CENTRAL1 = "me-central1", // Doha + ME_WEST1 = "me-west1", // Tel Aviv + + // Africa + AFRICA_SOUTH1 = "africa-south1" // Johannesburg } diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts index 6a45aab31..d51383fef 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts @@ -4,6 +4,7 @@ import { request } from "@app/lib/config/request"; import { logger } from "@app/lib/logger"; import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { GcpSyncScope } from "@app/services/secret-sync/gcp/gcp-sync-enums"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { SecretSyncError } from "../secret-sync-errors"; @@ -15,9 +16,17 @@ import { TGcpSyncWithCredentials } from "./gcp-sync-types"; -const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => { +const getProjectUrl = (secretSync: TGcpSyncWithCredentials) => { const { destinationConfig } = secretSync; + if (destinationConfig.scope === GcpSyncScope.Global) { + return `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}`; + } + + return `https://secretmanager.${destinationConfig.locationId}.rep.googleapis.com/v1/projects/${destinationConfig.projectId}/locations/${destinationConfig.locationId}`; +}; + +const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => { let gcpSecrets: GCPSecret[] = []; const pageSize = 100; @@ -31,16 +40,13 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden }); // eslint-disable-next-line no-await-in-loop - const { data: secretsRes } = await request.get( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${secretSync.destinationConfig.projectId}/secrets`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" - } + const { data: secretsRes } = await request.get(`${getProjectUrl(secretSync)}/secrets`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); if (secretsRes.secrets) { gcpSecrets = gcpSecrets.concat(secretsRes.secrets); @@ -61,7 +67,7 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden try { const { data: secretLatest } = await request.get( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`, + `${getProjectUrl(secretSync)}/secrets/${key}/versions/latest:access`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -113,11 +119,14 @@ export const GcpSyncFns = { if (!(key in gcpSecrets)) { // case: create secret await request.post( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`, + `${getProjectUrl(secretSync)}/secrets`, { - replication: { - automatic: {} - } + replication: + destinationConfig.scope === GcpSyncScope.Global + ? { + automatic: {} + } + : undefined }, { params: { @@ -131,7 +140,7 @@ export const GcpSyncFns = { ); await request.post( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`, + `${getProjectUrl(secretSync)}/secrets/${key}:addVersion`, { payload: { data: Buffer.from(secretMap[key].value).toString("base64") @@ -163,15 +172,12 @@ export const GcpSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) continue; // case: delete secret - await request.delete( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" - } + await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); } else if (secretMap[key].value !== gcpSecrets[key]) { if (!secretMap[key].value) { logger.warn( @@ -180,7 +186,7 @@ export const GcpSyncFns = { } await request.post( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`, + `${getProjectUrl(secretSync)}/secrets/${key}:addVersion`, { payload: { data: Buffer.from(secretMap[key].value).toString("base64") @@ -212,21 +218,18 @@ export const GcpSyncFns = { }, removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => { - const { destinationConfig, connection } = secretSync; + const { connection } = secretSync; const accessToken = await getGcpConnectionAuthToken(connection); const gcpSecrets = await getGcpSecrets(accessToken, secretSync); for await (const [key] of Object.entries(gcpSecrets)) { if (key in secretMap) { - await request.delete( - `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" - } + await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); } } } diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts index 0643c431a..875ceaf70 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts @@ -10,14 +10,33 @@ import { import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; import { SecretSync } from "../secret-sync-enums"; -import { GcpSyncScope } from "./gcp-sync-enums"; +import { GCPSecretManagerLocation, GcpSyncScope } from "./gcp-sync-enums"; const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; -const GcpSyncDestinationConfigSchema = z.object({ - scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope), - projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId) -}); +const GcpSyncDestinationConfigSchema = z.discriminatedUnion("scope", [ + z + .object({ + scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope), + projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId) + }) + .describe( + JSON.stringify({ + title: "Global" + }) + ), + z + .object({ + scope: z.literal(GcpSyncScope.Region).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope), + projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId), + locationId: z.nativeEnum(GCPSecretManagerLocation).describe(SecretSyncs.DESTINATION_CONFIG.GCP.locationId) + }) + .describe( + JSON.stringify({ + title: "Region" + }) + ) +]); export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({ destination: z.literal(SecretSync.GCPSecretManager), diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go index 90710154e..abc4d6949 100644 --- a/cli/packages/cmd/gateway.go +++ b/cli/packages/cmd/gateway.go @@ -43,7 +43,8 @@ func getInfisicalSdkInstance(cmd *cobra.Command) (infisicalSdk.InfisicalClientIn } // if the --token param is not set, we use the auth-method flag to determine the authentication method, and perform the appropriate login flow based on that - authMethod, err := cmd.Flags().GetString("auth-method") + authMethod, err := util.GetCmdFlagOrEnv(cmd, "auth-method", []string{util.INFISICAL_AUTH_METHOD_NAME}) + if err != nil { cancel() return nil, nil, err diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index ef549aabe..fd3ce1569 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -243,6 +243,7 @@ var loginCmd = &cobra.Command{ util.AuthStrategy.GCP_IAM_AUTH: sdkAuthenticator.HandleGcpIamAuthLogin, util.AuthStrategy.AWS_IAM_AUTH: sdkAuthenticator.HandleAwsIamAuthLogin, util.AuthStrategy.OIDC_AUTH: sdkAuthenticator.HandleOidcAuthLogin, + util.AuthStrategy.JWT_AUTH: sdkAuthenticator.HandleJwtAuthLogin, } credential, err := authStrategies[strategy]() diff --git a/cli/packages/gateway/connection.go b/cli/packages/gateway/connection.go index 46d194c96..3f4ffdf03 100644 --- a/cli/packages/gateway/connection.go +++ b/cli/packages/gateway/connection.go @@ -26,9 +26,13 @@ func handleConnection(ctx context.Context, quicConn quic.Connection) { log.Info().Msgf("New connection from: %s", quicConn.RemoteAddr().String()) // Use WaitGroup to track all streams var wg sync.WaitGroup + + contextWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + for { // Accept the first stream, which we'll use for commands - stream, err := quicConn.AcceptStream(ctx) + stream, err := quicConn.AcceptStream(contextWithTimeout) if err != nil { log.Printf("Failed to accept QUIC stream: %v", err) break @@ -52,7 +56,12 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) { // Use buffered reader for better handling of fragmented data reader := bufio.NewReader(stream) - defer stream.Close() + defer func() { + log.Info().Msgf("Closing stream %d", streamID) + if stream != nil { + stream.Close() + } + }() for { msg, err := reader.ReadBytes('\n') @@ -166,7 +175,6 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string, } } - // set certificate verification based on what the gateway client sent if verifyParam != "" { tlsConfig.InsecureSkipVerify = verifyParam == "false" log.Info().Msgf("TLS verification set to: %s", verifyParam) @@ -175,82 +183,94 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string, transport.TLSClientConfig = tlsConfig } - // read and parse the http request from the stream - req, err := http.ReadRequest(reader) - if err != nil { - return fmt.Errorf("failed to read HTTP request: %v", err) - } - - actionHeader := req.Header.Get("x-infisical-action") - if actionHeader != "" { - - if actionHeader == "inject-k8s-sa-auth-token" { - token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") - - if err != nil { - stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token"))) - return fmt.Errorf("failed to read k8s sa auth token: %v", err) - } - - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token))) - log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL) - } - - req.Header.Del("x-infisical-action") - } - - var targetFullURL string - if strings.HasPrefix(targetURL, "http://") || strings.HasPrefix(targetURL, "https://") { - baseURL := strings.TrimSuffix(targetURL, "/") - targetFullURL = baseURL + req.URL.Path - if req.URL.RawQuery != "" { - targetFullURL += "?" + req.URL.RawQuery - } - } else { - baseURL := strings.TrimSuffix("http://"+targetURL, "/") - targetFullURL = baseURL + req.URL.Path - if req.URL.RawQuery != "" { - targetFullURL += "?" + req.URL.RawQuery - } - } - - // create the request to the target - proxyReq, err := http.NewRequest(req.Method, targetFullURL, req.Body) - proxyReq.Header = req.Header.Clone() - if err != nil { - return fmt.Errorf("failed to create proxy request: %v", err) - } - - log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL) - client := &http.Client{ Transport: transport, Timeout: 30 * time.Second, } - // make the request to the target - resp, err := client.Do(proxyReq) - if err != nil { - stream.Write([]byte(buildHttpInternalServerError(fmt.Sprintf("failed to reach target due to networking error: %s", err.Error())))) - return fmt.Errorf("failed to reach target due to networking error: %v", err) + // Loop to handle multiple HTTP requests on the same stream + for { + req, err := http.ReadRequest(reader) + + if err != nil { + if errors.Is(err, io.EOF) { + log.Info().Msg("Client closed HTTP connection") + return nil + } + return fmt.Errorf("failed to read HTTP request: %v", err) + } + log.Info().Msgf("Received HTTP request: %s", req.URL.Path) + + actionHeader := req.Header.Get("x-infisical-action") + if actionHeader != "" { + if actionHeader == "inject-k8s-sa-auth-token" { + token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") + if err != nil { + stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token"))) + continue // Continue to next request instead of returning + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token))) + log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL) + } + req.Header.Del("x-infisical-action") + } + + // Build full target URL + var targetFullURL string + if strings.HasPrefix(targetURL, "http://") || strings.HasPrefix(targetURL, "https://") { + baseURL := strings.TrimSuffix(targetURL, "/") + targetFullURL = baseURL + req.URL.Path + if req.URL.RawQuery != "" { + targetFullURL += "?" + req.URL.RawQuery + } + } else { + baseURL := strings.TrimSuffix("http://"+targetURL, "/") + targetFullURL = baseURL + req.URL.Path + if req.URL.RawQuery != "" { + targetFullURL += "?" + req.URL.RawQuery + } + } + + // create the request to the target + proxyReq, err := http.NewRequest(req.Method, targetFullURL, req.Body) + if err != nil { + log.Error().Msgf("Failed to create proxy request: %v", err) + stream.Write([]byte(buildHttpInternalServerError("failed to create proxy request"))) + continue // Continue to next request + } + proxyReq.Header = req.Header.Clone() + + log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL) + + resp, err := client.Do(proxyReq) + if err != nil { + log.Error().Msgf("Failed to reach target: %v", err) + stream.Write([]byte(buildHttpInternalServerError(fmt.Sprintf("failed to reach target due to networking error: %s", err.Error())))) + continue // Continue to next request + } + + // Write the entire response (status line, headers, body) to the stream + // http.Response.Write handles this for "Connection: close" correctly. + // For other connection tokens, manual removal might be needed if they cause issues with QUIC. + // For a simple proxy, this is generally sufficient. + resp.Header.Del("Connection") // Good practice for proxies + + log.Info().Msgf("Writing response to stream: %s", resp.Status) + + if err := resp.Write(stream); err != nil { + log.Error().Err(err).Msg("Failed to write response to stream") + resp.Body.Close() + return fmt.Errorf("failed to write response to stream: %w", err) + } + + resp.Body.Close() + + // Check if client wants to close connection + if req.Header.Get("Connection") == "close" { + log.Info().Msg("Client requested connection close") + return nil + } } - defer resp.Body.Close() - - // Write the entire response (status line, headers, body) to the stream - // http.Response.Write handles this for "Connection: close" correctly. - // For other connection tokens, manual removal might be needed if they cause issues with QUIC. - // For a simple proxy, this is generally sufficient. - resp.Header.Del("Connection") // Good practice for proxies - - log.Info().Msgf("Writing response to stream: %s", resp.Status) - if err := resp.Write(stream); err != nil { - // If writing the response fails, the connection to the client might be broken. - // Logging the error is important. The original error will be returned. - log.Error().Err(err).Msg("Failed to write response to stream") - return fmt.Errorf("failed to write response to stream: %w", err) - } - - return nil } func buildHttpInternalServerError(message string) string { diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go index 68fda6d50..126e5a5d0 100644 --- a/cli/packages/util/constants.go +++ b/cli/packages/util/constants.go @@ -13,6 +13,8 @@ const ( VAULT_BACKEND_AUTO_MODE = "auto" VAULT_BACKEND_FILE_MODE = "file" + INFISICAL_AUTH_METHOD_NAME = "INFISICAL_AUTH_METHOD" + // Universal Auth INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID" INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET" @@ -29,6 +31,8 @@ const ( // JWT AUTH INFISICAL_JWT_NAME = "INFISICAL_JWT" + INFISICAL_GATEWAY_TOKEN_NAME_LEGACY = "TOKEN" // backwards compatibility with gateway helm chart, where token was the only supported auth method + // Generic env variable used for auth methods that require a machine identity ID INFISICAL_MACHINE_IDENTITY_ID_NAME = "INFISICAL_MACHINE_IDENTITY_ID" diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index fc3f994a7..abd9768aa 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -96,6 +96,11 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) source = fmt.Sprintf("%s environment variable", INFISICAL_TOKEN_NAME) } + + if infisicalToken == "" { // if its still empty, check for the `TOKEN` environment variable (for gateway helm) + infisicalToken = os.Getenv(INFISICAL_GATEWAY_TOKEN_NAME_LEGACY) + source = fmt.Sprintf("%s environment variable", INFISICAL_GATEWAY_TOKEN_NAME_LEGACY) + } } if infisicalToken == "" { // If it's empty, we return nothing at all. diff --git a/docs/cli/commands/gateway.mdx b/docs/cli/commands/gateway.mdx index fd035f1fd..a12493c58 100644 --- a/docs/cli/commands/gateway.mdx +++ b/docs/cli/commands/gateway.mdx @@ -26,28 +26,215 @@ Run the Infisical gateway in the foreground or manage its systemd service instal Run the Infisical gateway in the foreground. The gateway will connect to the relay service and maintain a persistent connection. ```bash - infisical gateway --token= --domain= + infisical gateway --domain= --auth-method= ``` - ### Flags + ### Authentication - - The machine identity access token to authenticate with Infisical. + The Infisical CLI supports multiple authentication methods. Below are the available authentication methods, with their respective flags. + + + + The Universal Auth method is a simple and secure way to authenticate with Infisical. It requires a client ID and a client secret to authenticate with Infisical. + + + + + Your machine identity client ID. + + + Your machine identity client secret. + + + The authentication method to use. Must be `universal-auth` when using Universal Auth. + + + ```bash - # Example - infisical gateway --token= + infisical gateway --auth-method=universal-auth --client-id= --client-secret= ``` - You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the gateway command. + + The Native Kubernetes method is used to authenticate with Infisical when running in a Kubernetes environment. It requires a service account token to authenticate with Infisical. + + + + + Your machine identity ID. + + + Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + + The authentication method to use. Must be `kubernetes` when using Native Kubernetes. + + + + + + + ```bash + infisical gateway --auth-method=kubernetes --machine-identity-id= + ``` + + + + The Native Azure method is used to authenticate with Infisical when running in an Azure environment. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `azure` when using Native Azure. + + + + + + + ```bash + infisical gateway --auth-method=azure --machine-identity-id= + ``` + + + + The Native GCP ID Token method is used to authenticate with Infisical when running in a GCP environment. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `gcp-id-token` when using Native GCP ID Token. + + + + + + + ```bash + infisical gateway --auth-method=gcp-id-token --machine-identity-id= + ``` + + + + + The GCP IAM method is used to authenticate with Infisical with a GCP service account key. + + + + + Your machine identity ID. + + + Path to your GCP service account key file _(Must be in JSON format!)_ + + + The authentication method to use. Must be `gcp-iam` when using GCP IAM. + + + + + ```bash + infisical gateway --auth-method=gcp-iam --machine-identity-id= --service-account-key-file-path= + ``` + + + + The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `aws-iam` when using Native AWS IAM. + + + + + ```bash + infisical gateway --auth-method=aws-iam --machine-identity-id= + ``` + + + + + The OIDC Auth method is used to authenticate with Infisical via identity tokens with OIDC. + + + + + Your machine identity ID. + + + The OIDC JWT from the identity provider. + + + The authentication method to use. Must be `oidc-auth` when using OIDC Auth. + + + + + ```bash + infisical gateway --auth-method=oidc-auth --machine-identity-id= --jwt= + ``` + + + + The JWT Auth method is used to authenticate with Infisical via a JWT token. + + + + + The JWT token to use for authentication. + + + Your machine identity ID. + + + The authentication method to use. Must be `jwt-auth` when using JWT Auth. + + + + + + ```bash + infisical gateway --auth-method=jwt-auth --jwt= --machine-identity-id= + ``` + + + You can use the `INFISICAL_TOKEN` environment variable to authenticate with Infisical with a raw machine identity access token. + + + + + The machine identity access token to use for authentication. + + + + + ```bash + infisical gateway --token= + ``` + + + + + ### Other Flags Domain of your self-hosted Infisical instance. ```bash # Example - sudo infisical gateway install --domain=https://app.your-domain.com + infisical gateway --domain=https://app.your-domain.com ``` diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index f493ff5d2..f93e3b4b2 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -190,7 +190,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa - + The OIDC Auth method is used to authenticate with Infisical via identity tokens with OIDC. @@ -198,7 +198,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa Your machine identity ID. - + The OIDC JWT from the identity provider. @@ -212,11 +212,35 @@ The Infisical CLI supports multiple authentication methods. Below are the availa Run the `login` command with the following flags to obtain an access token: ```bash - infisical login --method=oidc-auth --machine-identity-id= --oidc-jwt= + infisical login --method=oidc-auth --machine-identity-id= --jwt= ``` + + + The JWT Auth method is used to authenticate with Infisical via a JWT token. + + + + + The JWT token to use for authentication. + + + Your machine identity ID. + + + + + + + Run the `login` command with the following flags to obtain an access token: + + ```bash + infisical login --method=jwt-auth --jwt= --machine-identity-id= + ``` + + diff --git a/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx b/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx index 5e1cc7093..b953a80bf 100644 --- a/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx +++ b/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx @@ -1,5 +1,5 @@ --- -title: "Machine identities" +title: "Machine identities" description: "Learn how to set metadata and leverage authentication attributes for machine identities." --- @@ -25,7 +25,7 @@ Machine identities can have metadata set manually, just like users. In addition, #### Accessing Attributes From Machine Identity Login -When machine identities authenticate, they may receive additional payloads/attributes from the service provider. +When machine identities authenticate, they may receive additional payloads/attributes from the service provider. For methods like OIDC, these come as claims in the token and can be made available in your policies. @@ -50,17 +50,29 @@ For methods like OIDC, these come as claims in the token and can be made availab ``` You might map: - - - **department:** to `user.department` + + - **department:** to `user.department` - **role:** to `user.role` Once configured, these attributes become available in your policies using the following format: - + ``` {{ identity.auth.oidc.claims. }} ``` + + + + For identities authenticated using Kubernetes, the service account's namespace and name are available in their policy and can be accessed as follows: + + ``` + {{ identity.auth.kubernetes.namespace }} + {{ identity.auth.kubernetes.name }} + ``` + + + At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible. diff --git a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx index 66c4c706a..6c70612c8 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx @@ -95,12 +95,28 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa ![Modify ElastiCache Statements Modal](/images/platform/dynamic-secrets/modify-elasticache-statement.png) - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` If you want to provide specific privileges for the generated dynamic credentials, you can modify the ElastiCache statement to your needs. This is useful if you want to only give access to a specific resource. diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 56a10419c..aa10e4029 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -50,110 +50,304 @@ Replace **\** with your AWS account id and **\** w ## Set up Dynamic Secrets with AWS IAM - - - Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to. - - - ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) - - - ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png) - - - - Name by which you want the secret to be referenced - + + + Infisical will assume the provided role in your AWS account securely, without the need to share any credentials. + + To connect your self-hosted Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the configured AWS IAM Role. - - Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) - + If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2. - - Maximum time-to-live for a generated secret - + The following steps are for instances not deployed on AWS: + + + Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console. + + + Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles: + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowAssumeAnyRole", + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/*" + } + ] + } + ``` + + + Obtain the AWS access key ID and secret access key for your IAM User by navigating to **IAM > Users > [Your User] > Security credentials > Access keys**. - - The managing AWS IAM User Access Key - + ![Access Key Step 1](/images/integrations/aws/integrations-aws-access-key-1.png) + ![Access Key Step 2](/images/integrations/aws/integrations-aws-access-key-2.png) + ![Access Key Step 3](/images/integrations/aws/integrations-aws-access-key-3.png) + + + 1. Set the access key as **DYNAMIC_SECRET_AWS_ACCESS_KEY_ID**. + 2. Set the secret key as **DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY**. + + + - - The managing AWS IAM User Secret Key - + + + 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. + ![IAM Role Creation](/images/integrations/aws/integration-aws-iam-assume-role.png) - - [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. - + 2. Select **AWS Account** as the **Trusted Entity Type**. + 3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. + 4. (Recommended) Enable "Require external ID" and input your **Project ID** to strengthen security and mitigate the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html). + 5. Assign permission as shared in prerequisite. - - The AWS data center region. - + + When configuring an IAM Role that Infisical will assume, it’s highly recommended to enable the **"Require external ID"** option and specify your **Project ID**. - - The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role. - + This precaution helps protect your AWS account against the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html), a potential security vulnerability where Infisical could be tricked into performing actions on your behalf by an unauthorized actor. - - The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas - + Always enable "Require external ID" and use your Project ID when setting up the IAM Role. + + + + ![Copy IAM Role ARN](/images/integrations/aws/integration-aws-iam-assume-arn.png) + + + Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png) + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-assume-role.png) + + Name by which you want the secret to be referenced + - - The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas - + + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) + - - The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas - + + Maximum time-to-live for a generated secret + - -Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. -Allowed template variables are -- `{{randomUsername}}`: Random username string -- `{{unixTimestamp}}`: Current Unix timestamp - + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters - ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam.png) + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value - - - After submitting the form, you will see a dynamic secret created in the dashboard. + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` + + + Select *Assume Role* method. + - ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) - - - Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. - To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. - Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + The ARN of the AWS Role to assume. + - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. + - When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + The AWS data center region. + - ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role. + - - Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4. - + + The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas + + + The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas + - Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + The AWS IAM inline policy that should be attached to the created users. + Multiple values can be provided by separating them with commas + - ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) - - + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Allowed template variables are + + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4. + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) + + + + + + Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance. + + + Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png) + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-access-key.png) + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) + + + + Maximum time-to-live for a generated secret + + + + Select *Access Key* method. + + + + The managing AWS IAM User Access Key + + + + The managing AWS IAM User Secret Key + + + + [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. + + + + The AWS data center region. + + + + The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role. + + + + The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas + + + + The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas + + + + The AWS IAM inline policy that should be attached to the created users. + Multiple values can be provided by separating them with commas + + + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Allowed template variables are + + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + + + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4. + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) + + + + + ## Audit or Revoke Leases + Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases + To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) - Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic + secret diff --git a/docs/documentation/platform/dynamic-secrets/cassandra.mdx b/docs/documentation/platform/dynamic-secrets/cassandra.mdx index 628432bea..56b7ec336 100644 --- a/docs/documentation/platform/dynamic-secrets/cassandra.mdx +++ b/docs/documentation/platform/dynamic-secrets/cassandra.mdx @@ -80,11 +80,27 @@ The above configuration allows user creation and granting permissions. ![Modify CQL Statements Modal](../../../images/platform/dynamic-secrets/modify-cql-statements.png) - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` If you want to provide specific privileges for the generated dynamic credentials, you can modify the CQL statement to your needs. This is useful if you want to only give access to a specific key-space(s). diff --git a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx index 6c5028bb2..06d1102e2 100644 --- a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx +++ b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx @@ -87,13 +87,29 @@ The port that your Elasticsearch instance is running on. _(Example: 9200)_ A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service. - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp - + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-input-modal-elastic-search.png) diff --git a/docs/documentation/platform/dynamic-secrets/kubernetes.mdx b/docs/documentation/platform/dynamic-secrets/kubernetes.mdx index d6d051ff7..713aefae6 100644 --- a/docs/documentation/platform/dynamic-secrets/kubernetes.mdx +++ b/docs/documentation/platform/dynamic-secrets/kubernetes.mdx @@ -33,125 +33,6 @@ This feature is ideal for scenarios where you need to: - Maintain a secure audit trail of cluster access - Manage access to multiple Kubernetes clusters -## Prerequisites - -- A Kubernetes cluster with a service account -- Cluster access token with permissions to create service account tokens -- (Optional) [Gateway](/documentation/platform/gateways/overview) for private cluster access - -## RBAC Configuration - -Before you can start generating dynamic service account tokens, you'll need to configure the appropriate permissions in your Kubernetes cluster. This involves setting up Role-Based Access Control (RBAC) to allow the creation and management of service account tokens. - -The RBAC configuration serves a crucial security purpose: it creates a dedicated service account with minimal permissions that can only create and manage service account tokens. This follows the principle of least privilege, ensuring that the token generation process is secure and controlled. - -The following RBAC configuration creates the necessary permissions for generating service account tokens: - -```yaml rbac.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: tokenrequest -rules: - - apiGroups: [""] - resources: - - "serviceaccounts/token" - - "serviceaccounts" - verbs: - - "create" - - "get" ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: tokenrequest -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: tokenrequest -subjects: - - kind: ServiceAccount - name: infisical-token-requester - namespace: default -``` - -```bash -kubectl apply -f rbac.yaml -``` - -This configuration: - -1. Creates a `ClusterRole` named `tokenrequest` that allows: - - Creating and getting service account tokens - - Getting service account information -2. Creates a `ClusterRoleBinding` that binds the role to a service account named `infisical-token-requester` in the `default` namespace - -You can customize the service account name and namespace according to your needs. - -## Obtaining the Cluster Token - -After setting up the RBAC configuration, you need to obtain a token for the service account that will be used to create dynamic secrets. Here's how to get the token: - -1. Create a service account in your Kubernetes cluster that will be used to create service account tokens: - -```yaml infisical-service-account.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: infisical-token-requester - namespace: default -``` - -```bash -kubectl apply -f infisical-service-account.yaml -``` - -2. Create a long-lived service account token using this configuration file: - -```yaml service-account-token.yaml -apiVersion: v1 -kind: Secret -type: kubernetes.io/service-account-token -metadata: - name: infisical-token-requester-token - annotations: - kubernetes.io/service-account.name: "infisical-token-requester" -``` - -```bash -kubectl apply -f service-account-token.yaml -``` - -3. Link the secret to the service account: - -```bash -kubectl patch serviceaccount infisical-token-requester -p '{"secrets": [{"name": "infisical-token-requester-token"}]}' -n default -``` - -4. Retrieve the token: - -```bash -kubectl get secret infisical-token-requester-token -n default -o=jsonpath='{.data.token}' | base64 --decode -``` - -This token will be used as the "Cluster Token" in the dynamic secret configuration. - -## Obtaining the Cluster URL - -The cluster URL is the address of your Kubernetes API server. The simplest way to find it is to use the `kubectl cluster-info` command: - -```bash -kubectl cluster-info -``` - -This command works for all Kubernetes environments (managed services like GKE, EKS, AKS, or self-hosted clusters) and will show you the Kubernetes control plane address, which is your cluster URL. - - - Make sure the cluster URL is accessible from where you're running Infisical. - If you're using a private cluster, you'll need to configure a [Gateway](/documentation/platform/gateways/overview) to - access it. - - ## Set up Dynamic Secrets with Kubernetes @@ -164,6 +45,348 @@ This command works for all Kubernetes environments (managed services like GKE, E ![Dynamic Secret Modal](/images/platform/dynamic-secrets/dynamic-secret-modal-kubernetes.png) + + Before proceeding with the setup, you'll need to make two key decisions: + + 1. **Credential Type**: How you want to manage service accounts + - **Static**: Use an existing service account with predefined permissions + - **Dynamic**: Create temporary service accounts with specific role assignments + + 2. **Authentication Method**: How you want to authenticate with the cluster + - **Token (API)**: Use a service account token for direct API access + - **Gateway**: Use an Infisical Gateway deployed in your cluster + + + + Static credentials generate service account tokens for a predefined service account. This is useful when you want to: + - Generate tokens for an existing service account + - Maintain consistent permissions across token generations + - Use a service account that already has the necessary RBAC permissions + + ### Prerequisites + + - A Kubernetes cluster with a service account + - Cluster access token with permissions to create service account tokens + - (Optional) [Gateway](/documentation/platform/gateways/overview) for private cluster access + + ### Authentication Setup + + Choose your authentication method: + + + + This method uses a service account token to authenticate with the Kubernetes cluster. It's suitable when: + - You want to use a specific service account token that you've created + - You're working with a public cluster or have network access to the cluster's API server + - You want to explicitly control which service account is used for operations + + + With Token (API) authentication, Infisical uses the provided service account token + to make API calls to your Kubernetes cluster. This token must have the necessary + permissions to generate tokens for the target service account. + + + 1. Create a service account: + ```yaml infisical-service-account.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: infisical-token-requester + namespace: default + ``` + + ```bash + kubectl apply -f infisical-service-account.yaml + ``` + + 2. Set up RBAC permissions: + ```yaml rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: tokenrequest + rules: + - apiGroups: [""] + resources: + - "serviceaccounts/token" + - "serviceaccounts" + verbs: + - "create" + - "get" + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: tokenrequest + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tokenrequest + subjects: + - kind: ServiceAccount + name: infisical-token-requester + namespace: default + ``` + + ```bash + kubectl apply -f rbac.yaml + ``` + + 3. Create and obtain the token: + ```yaml service-account-token.yaml + apiVersion: v1 + kind: Secret + type: kubernetes.io/service-account-token + metadata: + name: infisical-token-requester-token + annotations: + kubernetes.io/service-account.name: "infisical-token-requester" + ``` + + ```bash + kubectl apply -f service-account-token.yaml + kubectl patch serviceaccount infisical-token-requester -p '{"secrets": [{"name": "infisical-token-requester-token"}]}' -n default + kubectl get secret infisical-token-requester-token -n default -o=jsonpath='{.data.token}' | base64 --decode + ``` + + + This method uses an Infisical Gateway deployed in your Kubernetes cluster. It's ideal when: + - You want to avoid storing static service account tokens + - You prefer to use the Gateway's pre-configured service account + - You want centralized management of cluster operations + + + With Gateway authentication, Infisical communicates with the Gateway, which then + uses its own service account to make API calls to the Kubernetes API server. + The Gateway's service account must have the necessary permissions to generate + tokens for the target service account. + + + 1. Deploy the Infisical Gateway in your cluster + 2. Set up RBAC permissions for the Gateway's service account: + ```yaml rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: tokenrequest + rules: + - apiGroups: [""] + resources: + - "serviceaccounts/token" + - "serviceaccounts" + verbs: + - "create" + - "get" + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: tokenrequest + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tokenrequest + subjects: + - kind: ServiceAccount + name: infisical-gateway + namespace: infisical + ``` + + ```bash + kubectl apply -f rbac.yaml + ``` + + + + + + + Dynamic credentials create a temporary service account, assign it to a defined role/cluster-role, and generate a service account token. This is useful when you want to: + - Create temporary service accounts with specific permissions + - Automatically clean up service accounts after token expiration + - Assign different roles to different users or applications + - Maintain strict control over service account permissions + + ### Prerequisites + + - A Kubernetes cluster with a service account + - Cluster access token with permissions to create service accounts and manage RBAC + - (Optional) [Gateway](/documentation/platform/gateways/overview) for private cluster access + + ### Authentication Setup + + Choose your authentication method: + + + + This method uses a service account token to authenticate with the Kubernetes cluster. It's suitable when: + - You want to use a specific service account token that you've created + - You're working with a public cluster or have network access to the cluster's API server + - You want to explicitly control which service account is used for operations + + + With Token (API) authentication, Infisical uses the provided service account token + to make API calls to your Kubernetes cluster. This token must have the necessary + permissions to create and manage service accounts, their tokens, and RBAC resources. + + + 1. Create a service account: + ```yaml service-account.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: infisical-token-requester + namespace: default + --- + apiVersion: v1 + kind: Secret + type: kubernetes.io/service-account-token + metadata: + name: infisical-token-requester-token + annotations: + kubernetes.io/service-account.name: "infisical-token-requester" + ``` + + ```bash + kubectl apply -f service-account.yaml + ``` + + 2. Set up RBAC permissions: + ```yaml rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: tokenrequest + rules: + - apiGroups: [""] + resources: + - "serviceaccounts/token" + - "serviceaccounts" + verbs: + - "create" + - "get" + - "delete" + - apiGroups: ["rbac.authorization.k8s.io"] + resources: + - "rolebindings" + - "clusterrolebindings" + verbs: + - "create" + - "delete" + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: tokenrequest + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tokenrequest + subjects: + - kind: ServiceAccount + name: infisical-token-requester + namespace: default + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: infisical-dynamic-role-binding-sa + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: infisical-dynamic-role + subjects: + - kind: ServiceAccount + name: infisical-token-requester + namespace: default + ``` + + ```bash + kubectl apply -f rbac.yaml + ``` + + + This method uses an Infisical Gateway deployed in your Kubernetes cluster. It's ideal when: + - You want to avoid storing static service account tokens + - You prefer to use the Gateway's pre-configured service account + - You want centralized management of cluster operations + + + With Gateway authentication, Infisical communicates with the Gateway, which then + uses its own service account to make API calls to the Kubernetes API server. + The Gateway's service account must have the necessary permissions to create and + manage service accounts, their tokens, and RBAC resources. + + + 1. Deploy the Infisical Gateway in your cluster + 2. Set up RBAC permissions for the Gateway's service account: + ```yaml rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: tokenrequest + rules: + - apiGroups: [""] + resources: + - "serviceaccounts/token" + - "serviceaccounts" + verbs: + - "create" + - "get" + - "delete" + - apiGroups: ["rbac.authorization.k8s.io"] + resources: + - "rolebindings" + - "clusterrolebindings" + verbs: + - "create" + - "delete" + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: tokenrequest + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tokenrequest + subjects: + - kind: ServiceAccount + name: infisical-gateway + namespace: infisical + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: infisical-dynamic-role-binding-sa + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: infisical-dynamic-role + subjects: + - kind: ServiceAccount + name: infisical-gateway + namespace: infisical + ``` + + ```bash + kubectl apply -f rbac.yaml + ``` + + + + + In Kubernetes RBAC, a service account can only create role bindings for resources that it has access to. + This means that if you want to create dynamic service accounts with access to certain resources, + the service account creating these bindings (either the token requester or Gateway service account) + must also have access to those same resources. For example, if you want to create dynamic service + accounts that can access secrets, the token requester service account must also have access to secrets. + + + + + + Name by which you want the secret to be referenced @@ -186,48 +409,62 @@ This command works for all Kubernetes environments (managed services like GKE, E Custom CA certificate for the Kubernetes API server. Leave blank to use the system/public CA. + + Choose between Token (API) or Gateway authentication. If using Gateway, the Gateway must be deployed in your Kubernetes cluster. + - Token with permissions to create service account tokens + Token with permissions to create service accounts and manage RBAC (required when using Token authentication) + + + Choose between Static (predefined service account) or Dynamic (temporary service accounts with role assignments) - Name of the service account to generate tokens for + Name of the service account to generate tokens for (required for Static credentials) - Kubernetes namespace where the service account exists + Kubernetes namespace where the service account exists or will be created + + + Type of role to assign (ClusterRole or Role) (required for Dynamic credentials) + + + Name of the role to assign to the temporary service account (required for Dynamic credentials) Optional list of audiences to include in the generated token - ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes.png) + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes-1.png) + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes-2.png) After submitting the form, you will see a dynamic secret created in the dashboard. - - Once you've successfully configured the dynamic secret, you're ready to generate on-demand service account tokens. - To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. - Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. - - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) - - When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. - - ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) - - - Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. - - - Once you click the `Submit` button, a new secret lease will be generated and the service account token will be shown to you. - - ![Provision Lease](/images/platform/dynamic-secrets/kubernetes-lease-value.png) - - +## Generate and Manage Tokens + +Once you've successfully configured the dynamic secret, you're ready to generate on-demand service account tokens. +To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. +Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + +![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) +![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + +When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + +![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when + configuring the dynamic secret. + + +Once you click the `Submit` button, a new secret lease will be generated and the service account token will be shown to you. + +![Provision Lease](/images/platform/dynamic-secrets/kubernetes-lease-value.png) + ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. diff --git a/docs/documentation/platform/dynamic-secrets/ldap.mdx b/docs/documentation/platform/dynamic-secrets/ldap.mdx index 1a3eca404..ddaaf9101 100644 --- a/docs/documentation/platform/dynamic-secrets/ldap.mdx +++ b/docs/documentation/platform/dynamic-secrets/ldap.mdx @@ -123,13 +123,29 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem changetype: delete ``` - + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. Allowed template variables are - `{{randomUsername}}`: Random username string - `{{unixTimestamp}}`: Current Unix timestamp - + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` + diff --git a/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx b/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx index 5d27d16e2..f167a0641 100644 --- a/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx +++ b/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx @@ -63,13 +63,29 @@ Create a project scoped API Key with the required permission in your Mongo Atlas ![Modify Scope Modal](../../../images/platform/dynamic-secrets/advanced-option-atlas.png) - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp - + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` + List that contains clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances that this database user can access. If omitted, MongoDB Cloud grants the database user access to all the clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances in the project. diff --git a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx index f71922473..7c4ebb568 100644 --- a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx +++ b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx @@ -66,12 +66,28 @@ Create a user with the required permission in your MongoDB instance. This user w A CA may be required if your DB requires it for incoming connections. - + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. Allowed template variables are - `{{randomUsername}}`: Random username string - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-mongodb.png) diff --git a/docs/documentation/platform/dynamic-secrets/mssql.mdx b/docs/documentation/platform/dynamic-secrets/mssql.mdx index 0a73bf129..6b6cef982 100644 --- a/docs/documentation/platform/dynamic-secrets/mssql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mssql.mdx @@ -9,7 +9,6 @@ The Infisical MS SQL dynamic secret allows you to generate Microsoft SQL server Create a user with the required permission in your SQL instance. This user will be used to create new accounts on-demand. - ## Set up Dynamic Secrets with MS SQL @@ -27,104 +26,123 @@ Create a user with the required permission in your SQL instance. This user will Name by which you want the secret to be referenced - - Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) - + + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) + - - Maximum time-to-live for a generated secret - + + Maximum time-to-live for a generated secret + - - List of key/value metadata pairs - + + List of key/value metadata pairs + - - Choose the service you want to generate dynamic secrets for. This must be selected as **MS SQL**. - + + Choose the service you want to generate dynamic secrets for. This must be selected as **MS SQL**. + - - Database host - + + Database host + - - Database port - + + Database port + - - Username that will be used to create dynamic secrets - + + Username that will be used to create dynamic secrets + - - Password that will be used to create dynamic secrets - + + Password that will be used to create dynamic secrets + - - Name of the database for which you want to create dynamic secrets - + + Name of the database for which you want to create dynamic secrets + - - A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions). - + + A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions). + - ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-mssql.png) + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-mssql.png) ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statements-mssql.png) - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` + - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp - - - If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s). - After submitting the form, you will see a dynamic secret created in the dashboard. - - If this step fails, you may have to add the CA certificate. - + + If this step fails, you may have to add the CA certificate. + + + ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) - ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) - When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. - ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) - - Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. - + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. + - Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) - ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) ## Audit or Revoke Leases + Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. This will allow you to see the expiration time of the lease or delete the lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases + To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) - Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic + secret diff --git a/docs/documentation/platform/dynamic-secrets/mysql.mdx b/docs/documentation/platform/dynamic-secrets/mysql.mdx index 6f708ebba..33d11a4fc 100644 --- a/docs/documentation/platform/dynamic-secrets/mysql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mysql.mdx @@ -69,15 +69,28 @@ Create a user with the required permission in your SQL instance. This user will ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statement-mysql.png) - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp - - - If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s). + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` diff --git a/docs/documentation/platform/dynamic-secrets/oracle.mdx b/docs/documentation/platform/dynamic-secrets/oracle.mdx index 02b379b98..3c83f3359 100644 --- a/docs/documentation/platform/dynamic-secrets/oracle.mdx +++ b/docs/documentation/platform/dynamic-secrets/oracle.mdx @@ -71,15 +71,28 @@ Create a user with the required permission in your SQL instance. This user will ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statement-oracle.png) - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp - - - If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s). + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` diff --git a/docs/documentation/platform/dynamic-secrets/postgresql.mdx b/docs/documentation/platform/dynamic-secrets/postgresql.mdx index f13c9c762..974066e14 100644 --- a/docs/documentation/platform/dynamic-secrets/postgresql.mdx +++ b/docs/documentation/platform/dynamic-secrets/postgresql.mdx @@ -72,12 +72,28 @@ Create a user with the required permission in your SQL instance. This user will ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statements.png) - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s). diff --git a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx index 09c04e61b..be41901b7 100644 --- a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx +++ b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx @@ -66,12 +66,28 @@ The port that the RabbitMQ management plugin is listening on. This is `15672` by -Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. -Allowed template variables are -- `{{randomUsername}}`: Random username string -- `{{unixTimestamp}}`: Current Unix timestamp - + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` + A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service. diff --git a/docs/documentation/platform/dynamic-secrets/redis.mdx b/docs/documentation/platform/dynamic-secrets/redis.mdx index b3e585204..8e28c3cb2 100644 --- a/docs/documentation/platform/dynamic-secrets/redis.mdx +++ b/docs/documentation/platform/dynamic-secrets/redis.mdx @@ -57,12 +57,28 @@ Create a user with the required permission in your Redis instance. This user wil ![Modify Redis Statements Modal](/images/platform/dynamic-secrets/modify-redis-statement.png) - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` If you want to provide specific privileges for the generated dynamic credentials, you can modify the Redis statement to your needs. This is useful if you want to only give access to a specific table(s). diff --git a/docs/documentation/platform/dynamic-secrets/sap-ase.mdx b/docs/documentation/platform/dynamic-secrets/sap-ase.mdx index 2737ab084..6da572f35 100644 --- a/docs/documentation/platform/dynamic-secrets/sap-ase.mdx +++ b/docs/documentation/platform/dynamic-secrets/sap-ase.mdx @@ -64,13 +64,29 @@ The Infisical SAP ASE dynamic secret allows you to generate SAP ASE database cre ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png) - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp - + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` + If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. diff --git a/docs/documentation/platform/dynamic-secrets/sap-hana.mdx b/docs/documentation/platform/dynamic-secrets/sap-hana.mdx index 597d69803..8ccd842f2 100644 --- a/docs/documentation/platform/dynamic-secrets/sap-hana.mdx +++ b/docs/documentation/platform/dynamic-secrets/sap-hana.mdx @@ -64,12 +64,28 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sap-hana-sql-statements.png) - - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` diff --git a/docs/documentation/platform/dynamic-secrets/snowflake.mdx b/docs/documentation/platform/dynamic-secrets/snowflake.mdx index 86378bbf6..f0bbaa08c 100644 --- a/docs/documentation/platform/dynamic-secrets/snowflake.mdx +++ b/docs/documentation/platform/dynamic-secrets/snowflake.mdx @@ -78,12 +78,28 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede ![Modify SQL Statements Modal](/images/platform/dynamic-secrets/snowflake/dynamic-secret-snowflake-sql-statements.png) - Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. - Allowed template variables are - - `{{randomUsername}}`: Random username string - - `{{unixTimestamp}}`: Current Unix timestamp - + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` + If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index e5f9623f5..127e544b7 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -89,18 +89,208 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t helm repo update ``` - ### Create a Kubernetes Secret with the gateway token + ### Create a Kubernetes Secret containing gateway environment variables - Create a new Kubernetes secret containing the gateway token as the `TOKEN` key. You can optionally also set the `INFISICAL_API_URL` key to your Infisical instance URL. By default, `INFISICAL_API_URL` is set to `https://app.infisical.com`. + The gateway supports all identity authentication methods through the use of environment variables. + The environment variables must be set in the `infisical-gateway-environment` Kubernetes secret. - ```bash - kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN= - ``` - - - The secret name is `infisical-gateway-environment` by default. The `TOKEN` key is required, and the `INFISICAL_API_URL` key is optional. - + #### Supported authentication methods + + + + The Universal Auth method is a simple and secure way to authenticate with Infisical. It requires a client ID and a client secret to authenticate with Infisical. + + + + + Your machine identity client ID. + + + Your machine identity client secret. + + + The authentication method to use. Must be `universal-auth` when using Universal Auth. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=universal-auth --from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_ID= --from-literal=INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET= + ``` + + + + The Native Kubernetes method is used to authenticate with Infisical when running in a Kubernetes environment. It requires a service account token to authenticate with Infisical. + + + + + Your machine identity ID. + + + Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + + The authentication method to use. Must be `kubernetes` when using Native Kubernetes. + + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=kubernetes --from-literal=INFISICAL_MACHINE_IDENTITY_ID= + ``` + + + + The Native Azure method is used to authenticate with Infisical when running in an Azure environment. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `azure` when using Native Azure. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=azure --from-literal=INFISICAL_MACHINE_IDENTITY_ID= + ``` + + + The Native GCP ID Token method is used to authenticate with Infisical when running in a GCP environment. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `gcp-id-token` when using Native GCP ID Token. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=gcp-id-token --from-literal=INFISICAL_MACHINE_IDENTITY_ID= + ``` + + + + The GCP IAM method is used to authenticate with Infisical with a GCP service account key. + + + + + Your machine identity ID. + + + Path to your GCP service account key file _(Must be in JSON format!)_ + + + The authentication method to use. Must be `gcp-iam` when using GCP IAM. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=gcp-iam --from-literal=INFISICAL_MACHINE_IDENTITY_ID= --from-literal=INFISICAL_GCP_SERVICE_ACCOUNT_KEY_FILE_PATH= + ``` + + + + + The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc. + + + + + Your machine identity ID. + + + The authentication method to use. Must be `aws-iam` when using Native AWS IAM. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=aws-iam --from-literal=INFISICAL_MACHINE_IDENTITY_ID= + ``` + + + + The OIDC Auth method is used to authenticate with Infisical via identity tokens with OIDC. + + + + + Your machine identity ID. + + + The OIDC JWT from the identity provider. + + + The authentication method to use. Must be `oidc-auth` when using OIDC Auth. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=oidc-auth --from-literal=INFISICAL_MACHINE_IDENTITY_ID= --from-literal=INFISICAL_JWT= + ``` + + + + The JWT Auth method is used to authenticate with Infisical via a JWT token. + + + + + The JWT token to use for authentication. + + + Your machine identity ID. + + + The authentication method to use. Must be `jwt-auth` when using JWT Auth. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_AUTH_METHOD=jwt-auth --from-literal=INFISICAL_JWT= --from-literal=INFISICAL_MACHINE_IDENTITY_ID= + ``` + + + You can use the `INFISICAL_TOKEN` environment variable to authenticate with Infisical with a raw machine identity access token. + + + + + The machine identity access token to use for authentication. + + + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=INFISICAL_TOKEN= + ``` + + + + + #### Other environment variables + + + + The API URL to use for the gateway. By default, `INFISICAL_API_URL` is set to `https://app.infisical.com`. + + + ### Install the Infisical Gateway Helm Chart ```bash diff --git a/docs/documentation/platform/identities/oidc-auth/azure.mdx b/docs/documentation/platform/identities/oidc-auth/azure.mdx new file mode 100644 index 000000000..a9f244794 --- /dev/null +++ b/docs/documentation/platform/identities/oidc-auth/azure.mdx @@ -0,0 +1,157 @@ +--- +title: Azure +description: "Learn how to authenticate Azure pipelines with Infisical using OpenID Connect (OIDC)." +--- + +**OIDC Auth** is a platform-agnostic JWT-based authentication method that can be used to authenticate from any platform or environment using an identity provider with OpenID Connect. + +## Diagram + +The following sequence diagram illustrates the OIDC Auth workflow for authenticating Azure pipelines with Infisical. + +```mermaid +sequenceDiagram + participant Client as Azure Pipeline + participant Idp as Identity Provider + participant Infis as Infisical + + Client->>Idp: Step 1: Request identity token + Idp-->>Client: Return JWT with verifiable claims + + Note over Client,Infis: Step 2: Login Operation + Client->>Infis: Send signed JWT to /api/v1/auth/oidc-auth/login + + Note over Infis,Idp: Step 3: Query verification + Infis->>Idp: Request JWT public key using OIDC Discovery + Idp-->>Infis: Return public key + + Note over Infis: Step 4: JWT validation + Infis->>Client: Return short-lived access token + + Note over Client,Infis: Step 5: Access Infisical API with Token + Client->>Infis: Make authenticated requests using the short-lived access token +``` + +## Concept + +At a high-level, Infisical authenticates a client by verifying the JWT and checking that it meets specific requirements (e.g. it is issued by a trusted identity provider) at the `/api/v1/auth/oidc-auth/login` endpoint. If successful, +then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. + +To be more specific: + +1. The Azure pipeline requests an identity token from Azure's identity provider. +2. The fetched identity token is sent to Infisical at the `/api/v1/auth/oidc-auth/login` endpoint. +3. Infisical fetches the public key that was used to sign the identity token from Azure's identity provider using OIDC Discovery. +4. Infisical validates the JWT using the public key provided by the identity provider and checks that the subject, audience, and claims of the token matches with the set criteria. +5. If all is well, Infisical returns a short-lived access token that the Azure pipeline can use to make authenticated requests to the Infisical API. + + + Infisical needs network-level access to Azure's identity provider endpoints. + + +## Guide + +In the following steps, we explore how to create and use identities to access the Infisical API using the OIDC Auth authentication method. + + + + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. + + ![identities organization](/images/platform/identities/identities-org.png) + + When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. + + ![identities organization create](/images/platform/identities/identities-org-create.png) + + Now input a few details for your new identity. Here's some guidance for each field: + + - Name (required): A friendly name for the identity. + - Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. + + Once you've created an identity, you'll be redirected to a page where you can manage the identity. + + ![identities page](/images/platform/identities/identities-page.png) + + Since the identity has been configured with Universal Auth by default, you should re-configure it to use OIDC Auth instead. To do this, press to edit the **Authentication** section, + remove the existing Universal Auth configuration, and add a new OIDC Auth configuration onto the identity. + + ![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png) + + ![identities create oidc auth method](/images/platform/identities/identities-org-create-oidc-auth-method.png) + + Restrict access by configuring the Subject, Audiences, and Claims fields + + Here's some more guidance on each field: + -
**OIDC Discovery URL**: The URL used to retrieve the OpenID Connect configuration from the identity provider. This is used to fetch the public keys needed to verify the JWT. For Azure, set this to `https://login.microsoftonline.com/{tenant-id}/v2.0` (replace `{tenant-id}` with your Azure AD tenant ID).
+ -
**Issuer**: The value of the `iss` claim that the token must match. For Azure, this should be `https://login.microsoftonline.com/{tenant-id}/v2.0`.
+ - **Subject**: This must match the `sub` claim in the JWT. + - **Audiences**: Values that must match the `aud` claim. + - **Claims**: Additional claims that must be present. Refer to [Azure DevOps docs](https://learn.microsoft.com/en-us/azure/devops/pipelines/library/connect-to-azure?view=azure-devops#workload-identity-federation) for available claims. + - **Access Token TTL**: Lifetime of the issued token (in seconds), e.g., `2592000` (30 days) + - **Access Token Max TTL**: Maximum allowed lifetime of the token + - **Access Token Max Number of Uses**: Max times the token can be used (`0` = unlimited) + - **Access Token Trusted IPs**: List of allowed IP ranges (defaults to `0.0.0.0/0`) + + If you are unsure about what to configure for the subject, audience, and claims fields, you can inspect the JWT token from your Azure DevOps pipeline by adding a debug step that outputs the token claims. + The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible. +
+ + To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. + + To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. + + Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. + + ![identities project](/images/platform/identities/identities-project.png) + + ![identities project create](/images/platform/identities/identities-project-create.png) + + + In Azure DevOps, to authenticate with Infisical using OIDC, you must configure a service connection that enables workload identity federation. + + Once set up, the OIDC token can be fetched automatically within the pipeline job context. Here's an example: + + ```yaml + trigger: + - main + + pool: + vmImage: ubuntu-latest + + steps: + - task: AzureCLI@2 + displayName: 'Retrieve secrets from Infisical using OIDC' + inputs: + azureSubscription: 'your-azure-service-connection-name' + scriptType: 'bash' + scriptLocation: 'inlineScript' + addSpnToEnvironment: true + inlineScript: | + # Get OIDC access token + OIDC_TOKEN=$(az account get-access-token --resource "api://AzureADTokenExchange" --query accessToken -o tsv) + + [ -z "$OIDC_TOKEN" ] && { echo "Failed to get access token"; exit 1; } + + # Exchange for Infisical access token + ACCESS_TOKEN=$(curl -s -X POST "/api/v1/auth/oidc-auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"identityId\":\"{your-identity-id}\",\"jwt\":\"$OIDC_TOKEN\"}" \ + | jq -r '.accessToken') + + # Fetch secrets + curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \ + "/api/v3/secrets/raw?environment={your-environment-slug}&workspaceSlug={your-workspace-slug}" + ``` + + Make sure the service connection is properly configured for workload identity federation and linked to your Azure AD app registration with appropriate claims. + + + Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation; + the default TTL is `7200` seconds which can be adjusted. + + If an identity access token expires, it can no longer authenticate with the Infisical API. In this case, + a new access token should be obtained by performing another login operation. + + + +
diff --git a/docs/images/platform/access-controls/abac-policy-k8s-format.png b/docs/images/platform/access-controls/abac-policy-k8s-format.png new file mode 100644 index 000000000..0aff7830a Binary files /dev/null and b/docs/images/platform/access-controls/abac-policy-k8s-format.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-access-key.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-access-key.png new file mode 100644 index 000000000..439208d83 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-access-key.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-assume-role.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-assume-role.png new file mode 100644 index 000000000..e3be4b5f1 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-assume-role.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam.png deleted file mode 100644 index 0ba6aa172..000000000 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes-1.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes-1.png new file mode 100644 index 000000000..dffbedad3 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes-1.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes-2.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes-2.png new file mode 100644 index 000000000..cc5e56001 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes-2.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes.png deleted file mode 100644 index 011dfadc7..000000000 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-kubernetes.png and /dev/null differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png index c50b6232a..1841b4b6d 100644 Binary files a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png differ diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx index df490c6b5..0be08a9a9 100644 --- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -34,6 +34,9 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical." - **GCP Connection**: The GCP Connection to authenticate with. - **Project**: The GCP project to sync with. + - **Scope**: The GCP project scope that secrets should be synced to: + - **Global**: Secrets will be synced globally; available to all project regions. + - **Region**: Secrets will be synced to the specified region. 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. ![Configure Options](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png) diff --git a/docs/mint.json b/docs/mint.json index 43fd81045..92bc2cc29 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -342,6 +342,7 @@ "group": "OIDC Auth", "pages": [ "documentation/platform/identities/oidc-auth/general", + "documentation/platform/identities/oidc-auth/azure", "documentation/platform/identities/oidc-auth/github", "documentation/platform/identities/oidc-auth/circleci", "documentation/platform/identities/oidc-auth/gitlab", diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx index 2af03b203..3361b8384 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/GcpSyncFields.tsx @@ -5,27 +5,56 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; -import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; -import { useGcpConnectionListProjects } from "@app/hooks/api/appConnections/gcp/queries"; -import { TGitHubConnectionEnvironment } from "@app/hooks/api/appConnections/github"; +import { + Badge, + FilterableSelect, + FormControl, + Select, + SelectItem, + Tooltip +} from "@app/components/v2"; +import { GCP_SYNC_SCOPES } from "@app/helpers/secretSyncs"; +import { + useGcpConnectionListProjectLocations, + useGcpConnectionListProjects +} from "@app/hooks/api/appConnections/gcp/queries"; +import { TGcpLocation, TGcpProject } from "@app/hooks/api/appConnections/gcp/types"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { TSecretSyncForm } from "../schemas"; +const formatOptionLabel = ({ displayName, locationId }: TGcpLocation) => ( +
+ {displayName}{" "} + + {locationId} + +
+); + export const GcpSyncFields = () => { const { control, setValue } = useFormContext< TSecretSyncForm & { destination: SecretSync.GCPSecretManager } >(); const connectionId = useWatch({ name: "connection.id", control }); + const projectId = useWatch({ name: "destinationConfig.projectId", control }); + const selectedScope = useWatch({ name: "destinationConfig.scope", control }); const { data: projects, isPending } = useGcpConnectionListProjects(connectionId, { enabled: Boolean(connectionId) }); + const { data: locations, isPending: areLocationsPending } = useGcpConnectionListProjectLocations( + { connectionId, projectId }, + { + enabled: Boolean(connectionId) && Boolean(projectId) + } + ); + useEffect(() => { - setValue("destinationConfig.scope", GcpSyncScope.Global); + if (!selectedScope) setValue("destinationConfig.scope", GcpSyncScope.Global); }, []); return ( @@ -33,6 +62,7 @@ export const GcpSyncFields = () => { { setValue("destinationConfig.projectId", ""); + setValue("destinationConfig.locationId", ""); }} /> { isLoading={isPending && Boolean(connectionId)} isDisabled={!connectionId} value={projects?.find((project) => project.id === value) ?? null} - onChange={(option) => - onChange((option as SingleValue)?.id ?? null) - } + onChange={(option) => { + setValue("destinationConfig.locationId", ""); + onChange((option as SingleValue)?.id ?? null); + }} options={projects} placeholder="Select a GCP project..." getOptionLabel={(option) => option.name} @@ -71,6 +102,76 @@ export const GcpSyncFields = () => { )} /> + ( + +

+ Specify how Infisical should sync secrets to GCP. The following options are + available: +

+
    + {Object.values(GCP_SYNC_SCOPES).map(({ name, description }) => { + return ( +
  • +

    + {name}: {description} +

    +
  • + ); + })} +
+ + } + tooltipClassName="max-w-lg" + label="Scope" + > + +
+ )} + /> + {selectedScope === GcpSyncScope.Region && ( + ( + + option.locationId === value) ?? null} + onChange={(option) => + onChange((option as SingleValue)?.locationId ?? null) + } + options={locations} + placeholder="Select a region..." + getOptionValue={(option) => option.locationId} + formatOptionLabel={formatOptionLabel} + /> + + )} + /> + )} ); }; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx index 000478f5e..bebb1ffe4 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/GcpSyncReviewFields.tsx @@ -3,12 +3,23 @@ import { useFormContext } from "react-hook-form"; import { GenericFieldLabel } from "@app/components/secret-syncs"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; export const GcpSyncReviewFields = () => { const { watch } = useFormContext< TSecretSyncForm & { destination: SecretSync.GCPSecretManager } >(); - const projectId = watch("destinationConfig.projectId"); + const destinationConfig = watch("destinationConfig"); - return {projectId}; + return ( + <> + {destinationConfig.projectId} + + {destinationConfig.scope} + + {destinationConfig.scope === GcpSyncScope.Region && ( + {destinationConfig.locationId} + )} + + ); }; diff --git a/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts index 4225c6619..fffae153e 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/gcp-sync-destination-schema.ts @@ -7,9 +7,16 @@ import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge( z.object({ destination: z.literal(SecretSync.GCPSecretManager), - destinationConfig: z.object({ - scope: z.literal(GcpSyncScope.Global), - projectId: z.string().min(1, "Project ID required") - }) + destinationConfig: z.discriminatedUnion("scope", [ + z.object({ + scope: z.literal(GcpSyncScope.Global), + projectId: z.string().min(1, "Project ID required") + }), + z.object({ + scope: z.literal(GcpSyncScope.Region), + projectId: z.string().min(1, "Project ID required"), + locationId: z.string().min(1, "Region required") + }) + ]) }) ); diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 88a0f7517..fbfc001e9 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -4,6 +4,7 @@ import { SecretSyncImportBehavior, SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; +import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; export const SECRET_SYNC_MAP: Record = { @@ -124,3 +125,14 @@ export const HUMANITEC_SYNC_SCOPES: Record< "Infisical will sync secrets as environment level shared values to the specified Humanitec application environment." } }; + +export const GCP_SYNC_SCOPES: Record = { + [GcpSyncScope.Global]: { + name: "Global", + description: "Secrets will be synced globally; being available in all project regions." + }, + [GcpSyncScope.Region]: { + name: "Region", + description: "Secrets will be synced to the specified region." + } +}; diff --git a/frontend/src/hooks/api/appConnections/gcp/queries.tsx b/frontend/src/hooks/api/appConnections/gcp/queries.tsx index a88860006..c8535aae1 100644 --- a/frontend/src/hooks/api/appConnections/gcp/queries.tsx +++ b/frontend/src/hooks/api/appConnections/gcp/queries.tsx @@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { appConnectionKeys } from "../queries"; -import { TGcpProject } from "./types"; +import { TGcpLocation, TGcpProject, TListProjectLocations } from "./types"; const gcpConnectionKeys = { all: [...appConnectionKeys.all, "gcp"] as const, listProjects: (connectionId: string) => - [...gcpConnectionKeys.all, "projects", connectionId] as const + [...gcpConnectionKeys.all, "projects", connectionId] as const, + listProjectLocations: ({ projectId, connectionId }: TListProjectLocations) => + [...gcpConnectionKeys.all, "project-locations", connectionId, projectId] as const }; export const useGcpConnectionListProjects = ( @@ -35,3 +37,29 @@ export const useGcpConnectionListProjects = ( ...options }); }; + +export const useGcpConnectionListProjectLocations = ( + { connectionId, projectId }: TListProjectLocations, + options?: Omit< + UseQueryOptions< + TGcpLocation[], + unknown, + TGcpLocation[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: gcpConnectionKeys.listProjectLocations({ connectionId, projectId }), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/gcp/${connectionId}/secret-manager-project-locations`, + { params: { projectId } } + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/gcp/types.ts b/frontend/src/hooks/api/appConnections/gcp/types.ts index 2af4eee9d..c5b594446 100644 --- a/frontend/src/hooks/api/appConnections/gcp/types.ts +++ b/frontend/src/hooks/api/appConnections/gcp/types.ts @@ -2,3 +2,13 @@ export type TGcpProject = { id: string; name: string; }; + +export type TListProjectLocations = { + connectionId: string; + projectId: string; +}; + +export type TGcpLocation = { + displayName: string; + locationId: string; +}; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index f9aa6d4d0..440c534f0 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -37,6 +37,11 @@ export enum DynamicSecretProviders { Vertica = "vertica" } +export enum KubernetesDynamicSecretCredentialType { + Static = "static", + Dynamic = "dynamic" +} + export enum SqlProviders { Postgres = "postgres", MySql = "mysql2", @@ -44,6 +49,11 @@ export enum SqlProviders { MsSQL = "mssql" } +export enum DynamicSecretAwsIamAuth { + AssumeRole = "assume-role", + AccessKey = "access-key" +} + export type TDynamicSecretProvider = | { type: DynamicSecretProviders.SqlDatabase; @@ -78,15 +88,26 @@ export type TDynamicSecretProvider = } | { type: DynamicSecretProviders.AwsIam; - inputs: { - accessKey: string; - secretAccessKey: string; - region: string; - awsPath?: string; - policyDocument?: string; - userGroups?: string; - policyArns?: string; - }; + inputs: + | { + method: DynamicSecretAwsIamAuth.AccessKey; + accessKey: string; + secretAccessKey: string; + region: string; + awsPath?: string; + policyDocument?: string; + userGroups?: string; + policyArns?: string; + } + | { + method: DynamicSecretAwsIamAuth.AssumeRole; + roleArn: string; + region: string; + awsPath?: string; + policyDocument?: string; + userGroups?: string; + policyArns?: string; + }; } | { type: DynamicSecretProviders.Redis; @@ -267,17 +288,32 @@ export type TDynamicSecretProvider = } | { type: DynamicSecretProviders.Kubernetes; - inputs: { - url: string; - clusterToken: string; - ca?: string; - serviceAccountName: string; - credentialType: "dynamic" | "static"; - namespace: string; - gatewayId?: string; - sslEnabled: boolean; - audiences: string[]; - }; + inputs: + | { + url: string; + clusterToken?: string; + ca?: string; + serviceAccountName: string; + credentialType: KubernetesDynamicSecretCredentialType.Static; + namespace: string; + gatewayId?: string; + sslEnabled: boolean; + audiences: string[]; + authMethod: string; + } + | { + url: string; + clusterToken?: string; + ca?: string; + credentialType: KubernetesDynamicSecretCredentialType.Dynamic; + namespace: string; + gatewayId?: string; + sslEnabled: boolean; + audiences: string[]; + roleType: string; + role: string; + authMethod: string; + }; } | { type: DynamicSecretProviders.Vertica; diff --git a/frontend/src/hooks/api/groups/queries.tsx b/frontend/src/hooks/api/groups/queries.tsx index eb10085bc..6fbe0ec13 100644 --- a/frontend/src/hooks/api/groups/queries.tsx +++ b/frontend/src/hooks/api/groups/queries.tsx @@ -21,7 +21,27 @@ export const groupKeys = { limit: number; search: string; filter?: EFilterReturnedUsers; - }) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, search, filter }] as const + }) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, search, filter }] as const, + specificProjectGroupUserMemberships: ({ + projectId, + slug, + offset, + limit, + search, + filter + }: { + slug: string; + projectId: string; + offset: number; + limit: number; + search: string; + filter?: EFilterReturnedUsers; + }) => + [ + ...groupKeys.forGroupUserMemberships(slug), + projectId, + { offset, limit, search, filter } + ] as const }; export const useGetGroupById = (groupId: string) => { @@ -80,3 +100,51 @@ export const useListGroupUsers = ({ } }); }; + +export const useListProjectGroupUsers = ({ + id, + projectId, + groupSlug, + offset = 0, + limit = 10, + search, + filter +}: { + id: string; + groupSlug: string; + projectId: string; + offset: number; + limit: number; + search: string; + filter?: EFilterReturnedUsers; +}) => { + return useQuery({ + queryKey: groupKeys.specificProjectGroupUserMemberships({ + slug: groupSlug, + projectId, + offset, + limit, + search, + filter + }), + enabled: Boolean(groupSlug), + placeholderData: (previousData) => previousData, + queryFn: async () => { + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit), + search, + ...(filter && { filter }) + }); + + const { data } = await apiRequest.get<{ users: TGroupUser[]; totalCount: number }>( + `/api/v2/workspace/${projectId}/groups/${id}/users`, + { + params + } + ); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts b/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts index bda7da6be..b7b9c1db8 100644 --- a/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/gcp-sync.ts @@ -3,15 +3,22 @@ import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; export enum GcpSyncScope { - Global = "global" + Global = "global", + Region = "region" } export type TGcpSync = TRootSecretSync & { destination: SecretSync.GCPSecretManager; - destinationConfig: { - scope: GcpSyncScope.Global; - projectId: string; - }; + destinationConfig: + | { + scope: GcpSyncScope.Global; + projectId: string; + } + | { + scope: GcpSyncScope.Region; + projectId: string; + locationId: string; + }; connection: { app: AppConnection.GCP; name: string; diff --git a/frontend/src/hooks/api/workspace/mutations.tsx b/frontend/src/hooks/api/workspace/mutations.tsx index ea7376d3d..5cd7e3bbc 100644 --- a/frontend/src/hooks/api/workspace/mutations.tsx +++ b/frontend/src/hooks/api/workspace/mutations.tsx @@ -50,10 +50,13 @@ export const useUpdateGroupWorkspaceRole = () => { return groupMembership; }, - onSuccess: (_, { projectId }) => { + onSuccess: (_, { projectId, groupId }) => { queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId) }); + queryClient.invalidateQueries({ + queryKey: workspaceKeys.getWorkspaceGroupMembershipDetails(projectId, groupId) + }); } }); }; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index c040a1267..2ae2bc463 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -691,6 +691,21 @@ export const useGetWorkspaceIdentityMembershipDetails = (projectId: string, iden }); }; +export const useGetWorkspaceGroupMembershipDetails = (projectId: string, groupId: string) => { + return useQuery({ + enabled: Boolean(projectId && groupId), + queryKey: workspaceKeys.getWorkspaceGroupMembershipDetails(projectId, groupId), + queryFn: async () => { + const { + data: { groupMembership } + } = await apiRequest.get<{ groupMembership: TGroupMembership }>( + `/api/v2/workspace/${projectId}/groups/${groupId}` + ); + return groupMembership; + } + }); +}; + export const useListWorkspaceGroups = (projectId: string) => { return useQuery({ queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId), diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index c10616b63..15317d2fd 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -36,6 +36,8 @@ export const workspaceKeys = { searchWorkspace: (dto: TSearchProjectsDTO) => ["search-projects", dto] as const, getWorkspaceGroupMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-groups"] as const, + getWorkspaceGroupMembershipDetails: (workspaceId: string, groupId: string) => + [{ workspaceId, groupId }, "workspace-group-membership-details"] as const, getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) => [{ projectSlug }, "workspace-cas"] as const, specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) => diff --git a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx index a736b52a7..ac194cd05 100644 --- a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx @@ -3,7 +3,7 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Link, useNavigate } from "@tanstack/react-router"; +import { Link, useNavigate, useRouter } from "@tanstack/react-router"; import axios from "axios"; import { addSeconds, formatISO } from "date-fns"; import { jwtDecode } from "jwt-decode"; @@ -51,6 +51,7 @@ export const SelectOrganizationSection = () => { const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); + const router = useRouter(); const queryParams = new URLSearchParams(window.location.search); const orgId = queryParams.get("org_id"); const callbackPort = queryParams.get("callback_port"); @@ -118,6 +119,8 @@ export const SelectOrganizationSection = () => { }) .finally(() => setIsInitialOrgCheckLoading(false)); + await router.invalidate(); + if (isMfaEnabled) { SecurityClient.setMfaToken(token); if (mfaMethod) { diff --git a/frontend/src/pages/auth/SelectOrgPage/route.tsx b/frontend/src/pages/auth/SelectOrgPage/route.tsx index 445479b3f..27ad4bb94 100644 --- a/frontend/src/pages/auth/SelectOrgPage/route.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/route.tsx @@ -7,7 +7,8 @@ import { SelectOrganizationPage } from "./SelectOrgPage"; export const SelectOrganizationPageQueryParams = z.object({ org_id: z.string().optional().catch(""), callback_port: z.coerce.number().optional().catch(undefined), - is_admin_login: z.boolean().optional().catch(false) + is_admin_login: z.boolean().optional().catch(false), + force: z.boolean().optional() }); export const Route = createFileRoute("/_restrict-login-signup/login/select-organization")({ diff --git a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx index a01545537..9cb739736 100644 --- a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx +++ b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx @@ -28,8 +28,7 @@ import { import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { ProjectType } from "@app/hooks/api/workspace/types"; - -import { navigateUserToOrg } from "../LoginPage/Login.utils"; +import { isLoggedIn } from "@app/hooks/api/reactQuery"; // eslint-disable-next-line new-cap const client = new jsrp.client(); @@ -71,6 +70,8 @@ export const SignupInvitePage = () => { const { mutateAsync: selectOrganization } = useSelectOrganization(); + const loggedIn = isLoggedIn(); + // Verifies if the information that the users entered (name, workspace) is there, and if the password matched the criteria. const signupErrorCheck = async () => { setIsLoading(true); @@ -242,29 +243,10 @@ export const SignupInvitePage = () => { if (response?.token) { SecurityClient.setSignupToken(response.token); setStep(2); + } else if (loggedIn) { + navigate({ to: "/login/select-organization", search: { force: true } }); } else { - const redirectExistingUser = async () => { - try { - const { token: mfaToken, isMfaEnabled } = await selectOrganization({ - organizationId - }); - - if (isMfaEnabled) { - SecurityClient.setMfaToken(mfaToken); - toggleShowMfa.on(); - setMfaSuccessCallback(() => redirectExistingUser); - return; - } - - // user will be redirected to dashboard - // if not logged in gets kicked out to login - await navigateUserToOrg(navigate, organizationId); - } catch (err) { - navigate({ to: "/login" }); - } - }; - - await redirectExistingUser(); + navigate({ to: "/login" }); } } } catch (err) { diff --git a/frontend/src/pages/middlewares/restrict-login-signup.tsx b/frontend/src/pages/middlewares/restrict-login-signup.tsx index f85ea44f2..73964868a 100644 --- a/frontend/src/pages/middlewares/restrict-login-signup.tsx +++ b/frontend/src/pages/middlewares/restrict-login-signup.tsx @@ -15,7 +15,8 @@ import { setAuthToken } from "@app/hooks/api/reactQuery"; import { ProjectType } from "@app/hooks/api/workspace/types"; const QueryParamsSchema = z.object({ - callback_port: z.coerce.number().optional().catch(undefined) + callback_port: z.coerce.number().optional().catch(undefined), + force: z.boolean().optional() }); export const AuthConsentWrapper = () => { @@ -71,7 +72,7 @@ export const AuthConsentWrapper = () => { export const Route = createFileRoute("/_restrict-login-signup")({ validateSearch: zodValidator(QueryParamsSchema), search: { - middlewares: [stripSearchParams({ callback_port: undefined })] + middlewares: [stripSearchParams({ callback_port: undefined, force: undefined })] }, beforeLoad: async ({ context, location, search }) => { if (!context.serverConfig.initialized) { @@ -90,6 +91,12 @@ export const Route = createFileRoute("/_restrict-login-signup")({ if (!data) return; setAuthToken(data.token); + + if (location.pathname === "/signupinvite") return; + + // Avoid redirect if on select-organization page with force=true + if (location.pathname.endsWith("select-organization") && search?.force === true) return; + // to do cli login if (search?.callback_port) { if (location.pathname.endsWith("select-organization") || location.pathname.endsWith("login")) diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx index 6472c34c1..f56f852d4 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx @@ -152,7 +152,7 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props Email Added On - + diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx index 829ad2908..8ec279a9d 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx @@ -1,8 +1,17 @@ -import { faUserMinus } from "@fortawesome/free-solid-svg-icons"; +import { faEllipsisV, faUserMinus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OrgPermissionCan } from "@app/components/permissions"; -import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + IconButton, + Td, + Tooltip, + Tr +} from "@app/components/v2"; import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api"; import { TGroupUser } from "@app/hooks/api/groups/types"; @@ -38,30 +47,47 @@ export const GroupMembershipRow = ({

{new Date(joinedGroupAt).toLocaleDateString()}

- - - {(isAllowed) => { - return ( - + + + + - handlePopUpOpen("removeMemberFromGroup", { username })} - variant="plain" - colorSchema="danger" - > - - - - ); - }} - + + + + + + {(isAllowed) => { + return ( + +
+ } + onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })} + isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled} + > + Remove User From Group + +
+
+ ); + }} +
+
+ + ); diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx index 08a17148e..91b6d3186 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx @@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form"; import { faCheck, faClock, faEdit, faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { PopperContentProps } from "@radix-ui/react-popper"; import { twMerge } from "tailwind-merge"; import { z } from "zod"; @@ -28,6 +29,7 @@ import { formatProjectRoleName } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; import { useGetProjectRoles, useUpdateGroupWorkspaceRole } from "@app/hooks/api"; import { TGroupMembership } from "@app/hooks/api/groups/types"; +import { TProjectRole } from "@app/hooks/api/roles/types"; import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types"; import { groupBy } from "@app/lib/fn/array"; @@ -196,33 +198,38 @@ type TForm = z.infer; export type TMemberRolesProp = { disableEdit?: boolean; groupId: string; + className?: string; roles: TGroupMembership["roles"]; + popperContentProps?: PopperContentProps; }; const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; -export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMemberRolesProp) => { +type FormProps = { + projectRoles: Omit[] | undefined; + roles: TGroupMembership["roles"]; + groupId: string; + onClose: VoidFunction; +}; + +const GroupRolesForm = ({ projectRoles, roles, groupId, onClose }: FormProps) => { const { currentWorkspace } = useWorkspace(); - const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const); + const [searchRoles, setSearchRoles] = useState(""); + const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role); + + const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole(); + const { handleSubmit, control, - reset, setValue, formState: { isSubmitting, isDirty } } = useForm({ resolver: zodResolver(formSchema) }); - const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles( - currentWorkspace?.id ?? "" - ); - const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role); - - const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole(); - const handleRoleUpdate = async (data: TForm) => { const selectedRoles = Object.keys(data) .filter((el) => Boolean(data[el].isChecked)) @@ -253,7 +260,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember roles: selectedRoles }); createNotification({ text: "Successfully updated group role", type: "success" }); - handlePopUpToggle("editRole"); + onClose(); setSearchRoles(""); } catch { createNotification({ text: "Failed to update group role", type: "error" }); @@ -261,7 +268,120 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember }; return ( -
+
+
+ {projectRoles + ?.filter( + ({ name, slug }) => + name.toLowerCase().includes(searchRoles.toLowerCase()) || + slug.toLowerCase().includes(searchRoles.toLowerCase()) + ) + ?.map(({ id, name, slug }) => { + const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0]; + + return ( +
+
+ ( + { + field.onChange(isChecked); + setValue(`${slug}.temporaryAccess`, false); + }} + > + {name} + + )} + /> +
+
+ ( + { + setValue(`${slug}.isChecked`, true, { shouldDirty: true }); + field.onChange({ isTemporary: true, ...data }); + }} + onRemoveTemporary={() => { + setValue(`${slug}.isChecked`, false, { shouldDirty: true }); + field.onChange(false); + }} + /> + )} + /> +
+
+ ); + })} +
+
+
+ setSearchRoles(el.target.value)} + leftIcon={} + placeholder="Search roles.." + /> +
+
+ +
+
+
+ ); +}; + +export const GroupRoles = ({ + roles = [], + disableEdit = false, + groupId, + className, + popperContentProps +}: TMemberRolesProp) => { + const { currentWorkspace } = useWorkspace(); + const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const); + + const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles( + currentWorkspace?.id ?? "" + ); + + return ( +
{roles .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) .map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => { @@ -325,119 +445,32 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember open={popUp.editRole.isOpen} onOpenChange={(isOpen) => { handlePopUpToggle("editRole", isOpen); - reset(); }} > {!disableEdit && ( - + e.stopPropagation()}> )} - + e.stopPropagation()} + hideCloseBtn + className="pt-4" + > {isRolesLoading ? (
) : ( -
-
- {projectRoles - ?.filter( - ({ name, slug }) => - name.toLowerCase().includes(searchRoles.toLowerCase()) || - slug.toLowerCase().includes(searchRoles.toLowerCase()) - ) - ?.map(({ id, name, slug }) => { - const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0]; - - return ( -
-
- ( - { - field.onChange(isChecked); - setValue(`${slug}.temporaryAccess`, false); - }} - > - {name} - - )} - /> -
-
- ( - { - setValue(`${slug}.isChecked`, true, { shouldDirty: true }); - field.onChange({ isTemporary: true, ...data }); - }} - onRemoveTemporary={() => { - setValue(`${slug}.isChecked`, false, { shouldDirty: true }); - field.onChange(false); - }} - /> - )} - /> -
-
- ); - })} -
-
-
- setSearchRoles(el.target.value)} - leftIcon={} - placeholder="Search roles.." - /> -
-
- -
-
-
+ handlePopUpToggle("editRole")} + /> )}
diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx index 8f06576e2..ff2c53aca 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx @@ -8,6 +8,7 @@ import { faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; import { format } from "date-fns"; import { ProjectPermissionCan } from "@app/components/permissions"; @@ -55,6 +56,7 @@ enum GroupsOrderBy { export const GroupTable = ({ handlePopUpOpen }: Props) => { const { currentWorkspace } = useWorkspace(); + const navigate = useNavigate(); const { search, @@ -143,7 +145,32 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { .slice(offset, perPage * page) .map(({ group: { id, name }, roles, createdAt }) => { return ( - + { + if (evt.key === "Enter") { + navigate({ + to: `/${currentWorkspace.type}/$projectId/groups/$groupId` as const, + params: { + projectId: currentWorkspace.id, + groupId: id + } + }); + } + }} + onClick={() => + navigate({ + to: `/${currentWorkspace.type}/$projectId/groups/$groupId` as const, + params: { + projectId: currentWorkspace.id, + groupId: id + } + }) + } + > {name} {
{ + onClick={(e) => { + e.stopPropagation(); handlePopUpOpen("deleteGroup", { id, name diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx new file mode 100644 index 000000000..a3dfabcd0 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx @@ -0,0 +1,70 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; +import { useParams } from "@tanstack/react-router"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { EmptyState, PageHeader, Spinner } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useGetWorkspaceGroupMembershipDetails } from "@app/hooks/api/workspace/queries"; + +import { GroupDetailsSection } from "./components/GroupDetailsSection"; +import { GroupMembersSection } from "./components/GroupMembersSection"; + +const Page = () => { + const groupId = useParams({ + strict: false, + select: (el) => el.groupId as string + }); + + const { currentWorkspace } = useWorkspace(); + + const { data: groupMembership, isPending } = useGetWorkspaceGroupMembershipDetails( + currentWorkspace.id, + groupId + ); + + if (isPending) + return ( +
+ +
+ ); + + return ( +
+ {groupMembership ? ( +
+ +
+
+ +
+ +
+
+ ) : ( + + )} +
+ ); +}; + +export const GroupDetailsByIDPage = () => { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "Project Group" })} + + + + + + + ); +}; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx new file mode 100644 index 000000000..4f1dcb586 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx @@ -0,0 +1,152 @@ +import { faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; +import { format } from "date-fns"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + IconButton +} from "@app/components/v2"; +import { CopyButton } from "@app/components/v2/CopyButton"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useDeleteGroupFromWorkspace } from "@app/hooks/api"; +import { TGroupMembership } from "@app/hooks/api/groups/types"; +import { GroupRoles } from "@app/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles"; + +type Props = { + groupMembership: TGroupMembership; +}; + +export const GroupDetailsSection = ({ groupMembership }: Props) => { + const { handlePopUpToggle, popUp, handlePopUpClose, handlePopUpOpen } = usePopUp([ + "deleteGroup" + ] as const); + + const { mutateAsync: deleteMutateAsync } = useDeleteGroupFromWorkspace(); + const { currentWorkspace } = useWorkspace(); + const navigate = useNavigate(); + + const onRemoveGroupSubmit = async () => { + try { + await deleteMutateAsync({ + groupId: groupMembership.group.id, + projectId: currentWorkspace.id + }); + + createNotification({ + text: "Successfully removed group from project", + type: "success" + }); + + navigate({ + to: `/${currentWorkspace.type}/${currentWorkspace.id}/access-management?selectedTab=groups` + }); + + handlePopUpClose("deleteGroup"); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to remove group from project"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( +
+
+

Group Details

+ + + + + + + + + {(isAllowed) => { + return ( + } + onClick={() => handlePopUpOpen("deleteGroup")} + isDisabled={!isAllowed} + > + Remove Group From Project + + ); + }} + + + +
+
+
+

Group ID

+
+

{groupMembership.group.id}

+ +
+
+
+

Name

+

{groupMembership.group.name}

+
+
+

Slug

+
+

{groupMembership.group.slug}

+ +
+
+
+

Project Role

+ + {(isAllowed) => ( + + )} + +
+
+

Assigned to Project

+

+ {format(groupMembership.createdAt, "M/d/yyyy")} +

+
+
+ handlePopUpToggle("deleteGroup", isOpen)} + deleteKey="confirm" + buttonText="Remove" + onDeleteApproved={onRemoveGroupSubmit} + /> +
+ ); +}; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx new file mode 100644 index 000000000..0be3a9b01 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx @@ -0,0 +1,20 @@ +import { TGroupMembership } from "@app/hooks/api/groups/types"; + +import { GroupMembersTable } from "./GroupMembersTable"; + +type Props = { + groupMembership: TGroupMembership; +}; + +export const GroupMembersSection = ({ groupMembership }: Props) => { + return ( +
+
+

Group Members

+
+
+ +
+
+ ); +}; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx new file mode 100644 index 000000000..c0639260a --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx @@ -0,0 +1,235 @@ +import { useMemo } from "react"; +import { + faArrowDown, + faArrowUp, + faFolder, + faMagnifyingGlass, + faSearch +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { + ConfirmActionModal, + EmptyState, + IconButton, + Input, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; +import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; +import { useAssumeProjectPrivileges } from "@app/hooks/api"; +import { ActorType } from "@app/hooks/api/auditLogs/enums"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { useListProjectGroupUsers } from "@app/hooks/api/groups/queries"; +import { EFilterReturnedUsers, TGroupMembership } from "@app/hooks/api/groups/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; + +import { GroupMembershipRow } from "./GroupMembershipRow"; + +type Props = { + groupMembership: TGroupMembership; +}; + +enum GroupMembersOrderBy { + Name = "name" +} + +export const GroupMembersTable = ({ groupMembership }: Props) => { + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + toggleOrderDirection + } = usePagination(GroupMembersOrderBy.Name, { + initPerPage: getUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, 20) + }); + + const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["assumePrivileges"] as const); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, newPerPage); + }; + + const { currentWorkspace } = useWorkspace(); + + const { data: groupMemberships, isPending } = useListProjectGroupUsers({ + id: groupMembership.group.id, + groupSlug: groupMembership.group.slug, + projectId: currentWorkspace.id, + offset, + limit: perPage, + search, + filter: EFilterReturnedUsers.EXISTING_MEMBERS + }); + + const filteredGroupMemberships = useMemo(() => { + return groupMemberships && groupMemberships?.users + ? groupMemberships?.users + ?.filter((membership) => { + const userSearchString = `${membership.firstName && membership.firstName} ${ + membership.lastName && membership.lastName + } ${membership.email && membership.email} ${ + membership.username && membership.username + }`; + return userSearchString.toLowerCase().includes(search.trim().toLowerCase()); + }) + .sort((a, b) => { + const [membershipOne, membershipTwo] = + orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + const membershipOneComparisonString = membershipOne.firstName + ? membershipOne.firstName + : membershipOne.email; + + const membershipTwoComparisonString = membershipTwo.firstName + ? membershipTwo.firstName + : membershipTwo.email; + + const comparison = membershipOneComparisonString + .toLowerCase() + .localeCompare(membershipTwoComparisonString.toLowerCase()); + + return comparison; + }) + : []; + }, [groupMemberships, orderDirection, search]); + + useResetPageHelper({ + totalCount: filteredGroupMemberships?.length, + offset, + setPage + }); + + const assumePrivileges = useAssumeProjectPrivileges(); + + const handleAssumePrivileges = async () => { + const { userId } = popUp?.assumePrivileges?.data as { userId: string }; + assumePrivileges.mutate( + { + actorId: userId, + actorType: ActorType.USER, + projectId: currentWorkspace.id + }, + { + onSuccess: () => { + createNotification({ + type: "success", + text: "User privilege assumption has started" + }); + + let overviewPage: string; + + switch (currentWorkspace.type) { + case ProjectType.SecretScanning: + overviewPage = "data-sources"; + break; + case ProjectType.CertificateManager: + overviewPage = "subscribers"; + break; + default: + overviewPage = "overview"; + } + + window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/${overviewPage}`; + } + } + ); + }; + + return ( +
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search users..." + /> + + + + + + + + + + + {isPending && } + {!isPending && + filteredGroupMemberships.slice(offset, perPage * page).map((userGroupMembership) => { + return ( + handlePopUpOpen("assumePrivileges", { userId })} + /> + ); + })} + +
+
+ Name + + + +
+
EmailAdded On +
+ {Boolean(filteredGroupMemberships.length) && ( + + )} + {!isPending && !filteredGroupMemberships?.length && ( + + )} +
+ handlePopUpToggle("assumePrivileges", isOpen)} + onConfirmed={handleAssumePrivileges} + buttonText="Confirm" + /> +
+ ); +}; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx new file mode 100644 index 000000000..55f566477 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx @@ -0,0 +1,76 @@ +import { faEllipsisV, faUser } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + IconButton, + Td, + Tooltip, + Tr +} from "@app/components/v2"; +import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/context"; +import { TGroupUser } from "@app/hooks/api/groups/types"; + +type Props = { + user: TGroupUser; + onAssumePrivileges: (userId: string) => void; +}; + +export const GroupMembershipRow = ({ + user: { firstName, lastName, joinedGroupAt, email, id }, + onAssumePrivileges +}: Props) => { + return ( + + +

{`${firstName ?? "-"} ${lastName ?? ""}`}

+ + +

{email}

+ + + +

{new Date(joinedGroupAt).toLocaleDateString()}

+
+ + + + + + + + + + + + {(isAllowed) => { + return ( + } + onClick={() => onAssumePrivileges(id)} + isDisabled={!isAllowed} + > + Assume Privileges + + ); + }} + + + + + + + ); +}; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/index.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/index.tsx new file mode 100644 index 000000000..70c696609 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/index.tsx @@ -0,0 +1 @@ +export { GroupMembersSection } from "./GroupMembersSection"; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/index.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/index.tsx new file mode 100644 index 000000000..003c47910 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/index.tsx @@ -0,0 +1 @@ +export { GroupDetailsSection } from "./GroupDetailsSection"; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-cert-manager.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-cert-manager.tsx new file mode 100644 index 000000000..2663d75b1 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-cert-manager.tsx @@ -0,0 +1,28 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId" +)({ + component: GroupDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/cert-manager/$projectId/access-management", + params: { + projectId: params.projectId + } + }) + }, + { + label: "Groups" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-kms.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-kms.tsx new file mode 100644 index 000000000..6f4b5a8a0 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-kms.tsx @@ -0,0 +1,28 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId" +)({ + component: GroupDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/kms/$projectId/access-management", + params: { + projectId: params.projectId + } + }) + }, + { + label: "Groups" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx new file mode 100644 index 000000000..30fb82048 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx @@ -0,0 +1,28 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId" +)({ + component: GroupDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/secret-manager/$projectId/access-management", + params: { + projectId: params.projectId + } + }) + }, + { + label: "Groups" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-scanning.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-scanning.tsx new file mode 100644 index 000000000..0c3d7c998 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-scanning.tsx @@ -0,0 +1,28 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId" +)({ + component: GroupDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/secret-scanning/$projectId/access-management", + params: { + projectId: params.projectId + } + }) + }, + { + label: "Groups" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-ssh.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-ssh.tsx new file mode 100644 index 000000000..3aa9338fd --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-ssh.tsx @@ -0,0 +1,28 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId" +)({ + component: GroupDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Access Control", + link: linkOptions({ + to: "/ssh/$projectId/access-management", + params: { + projectId: params.projectId + } + }) + }, + { + label: "Groups" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx new file mode 100644 index 000000000..83ead0221 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx @@ -0,0 +1,27 @@ +import { faHome } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId" +)({ + component: GroupDetailsByIDPage, + context: () => ({ + breadcrumbs: [ + { + label: "Home", + icon: () => , + link: linkOptions({ to: "/organization/secret-manager/overview" }) + }, + { + label: "Access Control", + link: linkOptions({ to: "/organization/access-management" }) + }, + { + label: "groups" + } + ] + }) +}); diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 4943df471..0390f387a 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -1,5 +1,6 @@ import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; +import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { GitHubSyncScope, GitHubSyncVisibility @@ -47,7 +48,8 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { break; case SecretSync.GCPSecretManager: primaryText = destinationConfig.projectId; - secondaryText = "Global"; + secondaryText = + destinationConfig.scope === GcpSyncScope.Global ? "Global" : destinationConfig.locationId; break; case SecretSync.AzureKeyVault: primaryText = destinationConfig.vaultBaseUrl; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx index cd7b330e8..80c80ee61 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx @@ -5,22 +5,46 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2"; +import { + Button, + FilterableSelect, + FormControl, + Input, + Select, + SelectItem, + TextArea +} from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; -import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { + DynamicSecretAwsIamAuth, + DynamicSecretProviders +} from "@app/hooks/api/dynamicSecret/types"; import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ - provider: z.object({ - accessKey: z.string().trim().min(1), - secretAccessKey: z.string().trim().min(1), - region: z.string().trim().min(1), - awsPath: z.string().trim().optional(), - permissionBoundaryPolicyArn: z.string().trim().optional(), - policyDocument: z.string().trim().optional(), - userGroups: z.string().trim().optional(), - policyArns: z.string().trim().optional() - }), + provider: z.discriminatedUnion("method", [ + z.object({ + method: z.literal(DynamicSecretAwsIamAuth.AccessKey), + accessKey: z.string().trim().min(1), + secretAccessKey: z.string().trim().min(1), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional() + }), + z.object({ + method: z.literal(DynamicSecretAwsIamAuth.AssumeRole), + roleArn: z.string().trim().min(1), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional() + }) + ]), defaultTTL: z.string().superRefine((val, ctx) => { const valMs = ms(val); if (valMs < 60 * 1000) @@ -67,16 +91,21 @@ export const AwsIamInputForm = ({ const { control, formState: { isSubmitting }, - handleSubmit + handleSubmit, + watch } = useForm({ resolver: zodResolver(formSchema), defaultValues: { environment: isSingleEnvironmentMode ? environments[0] : undefined, - usernameTemplate: "{{randomUsername}}" + usernameTemplate: "{{randomUsername}}", + provider: { + method: DynamicSecretAwsIamAuth.AssumeRole + } } }); const createDynamicSecret = useCreateDynamicSecret(); + const isAccessKeyMethod = watch("provider.method") === DynamicSecretAwsIamAuth.AccessKey; const handleCreateDynamicSecret = async ({ name, @@ -127,7 +156,7 @@ export const AwsIamInputForm = ({ isError={Boolean(error)} errorText={error?.message} > - + )} /> @@ -170,38 +199,82 @@ export const AwsIamInputForm = ({ Configuration
-
- ( - ( + + - - )} - /> - ( - - - - )} - /> -
+ + Assume Role (Recommended) + + Access Key + + + )} + /> + {isAccessKeyMethod ? ( +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ ) : ( +
+ ( + + + + )} + /> +
+ )}
{ - const valMs = ms(val); - if (valMs < 60 * 1000) - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); - if (valMs > 24 * 60 * 60 * 1000) - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); - }), - maxTTL: z - .string() - .optional() - .superRefine((val, ctx) => { - if (!val) return; +const formSchema = z + .object({ + provider: z.discriminatedUnion("credentialType", [ + z.object({ + url: z.string().url().trim().min(1), + clusterToken: z.string().trim().optional(), + ca: z.string().optional(), + sslEnabled: z.boolean().default(false), + credentialType: z.literal(KubernetesDynamicSecretCredentialType.Static), + serviceAccountName: z.string().trim().min(1), + namespace: z.string().trim().min(1), + gatewayId: z.string().optional(), + audiences: z.array(z.string().trim().min(1)), + authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api) + }), + z.object({ + url: z.string().url().trim().min(1), + clusterToken: z.string().trim().optional(), + ca: z.string().optional(), + sslEnabled: z.boolean().default(false), + credentialType: z.literal(KubernetesDynamicSecretCredentialType.Dynamic), + namespace: z.string().trim().min(1), + gatewayId: z.string().optional(), + audiences: z.array(z.string().trim().min(1)), + roleType: z.nativeEnum(RoleType), + role: z.string().trim().min(1), + authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api) + }) + ]), + defaultTTL: z.string().superRefine((val, ctx) => { const valMs = ms(val); if (valMs < 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: slugSchema(), - environment: z.object({ name: z.string(), slug: z.string() }) -}); + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + name: slugSchema(), + environment: z.object({ name: z.string(), slug: z.string() }), + usernameTemplate: z.string().trim().optional() + }) + .superRefine((data, ctx) => { + if (data.provider.authMethod === AuthMethod.Gateway && !data.provider.gatewayId) { + ctx.addIssue({ + path: ["provider.gatewayId"], + code: z.ZodIssueCode.custom, + message: "When auth method is set to Gateway, a gateway must be selected" + }); + } + if (data.provider.authMethod === AuthMethod.Api && !data.provider.clusterToken) { + ctx.addIssue({ + path: ["provider.clusterToken"], + code: z.ZodIssueCode.custom, + message: "When auth method is set to Token, a cluster token must be provided" + }); + } + }); type TForm = z.infer & FieldValues; @@ -113,10 +159,11 @@ export const KubernetesInputForm = ({ sslEnabled: false, serviceAccountName: "", namespace: "", - credentialType: CredentialType.Static, + credentialType: KubernetesDynamicSecretCredentialType.Static, gatewayId: undefined, - audiences: [] - }, + audiences: [], + authMethod: AuthMethod.Api + } as const, environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -130,12 +177,16 @@ export const KubernetesInputForm = ({ const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const sslEnabled = watch("provider.sslEnabled"); + const credentialType = watch("provider.credentialType"); + const authMethod = watch("provider.authMethod"); const handleCreateDynamicSecret = async (formData: TForm) => { - const { provider, ...rest } = formData; + const { provider, usernameTemplate, ...rest } = formData; // wait till previous request is finished if (createDynamicSecret.isPending) return; + try { + const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}"; await createDynamicSecret.mutateAsync({ provider: { type: DynamicSecretProviders.Kubernetes, inputs: provider }, maxTTL: rest.maxTTL, @@ -143,7 +194,9 @@ export const KubernetesInputForm = ({ path: secretPath, defaultTTL: rest.defaultTTL, projectSlug, - environmentSlug: rest.environment.slug + environmentSlug: rest.environment.slug, + usernameTemplate: + !usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate }); onCompleted(); @@ -343,20 +396,45 @@ export const KubernetesInputForm = ({ )} /> - ( - + )} /> + {authMethod === AuthMethod.Api && ( + ( + + + + )} + /> + )} @@ -386,21 +462,45 @@ export const KubernetesInputForm = ({ )} />
-
- ( - - - - )} - /> -
+ {credentialType === KubernetesDynamicSecretCredentialType.Static && ( +
+ ( + + + + )} + /> +
+ )} + {credentialType === KubernetesDynamicSecretCredentialType.Dynamic && ( +
+ ( + + + + )} + /> +
+ )}
+ {credentialType === KubernetesDynamicSecretCredentialType.Dynamic && ( +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+ )}
{ const valMs = ms(val); if (valMs < 60 * 1000) @@ -66,6 +77,7 @@ export const EditDynamicSecretAwsIamForm = ({ }: Props) => { const { control, + watch, formState: { isSubmitting }, handleSubmit } = useForm({ @@ -80,6 +92,7 @@ export const EditDynamicSecretAwsIamForm = ({ } } }); + const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey; const updateDynamicSecret = useUpdateDynamicSecret(); @@ -173,38 +186,82 @@ export const EditDynamicSecretAwsIamForm = ({
Configuration
-
- ( - ( + + - - )} - /> - ( - - - - )} - /> -
+ + Assume Role (Recommended) + + Access Key + + + )} + /> + {isAccessKeyMethod ? ( +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ ) : ( +
+ ( + + + + )} + /> +
+ )}
{ - const valMs = ms(val); - if (valMs < 60 * 1000) - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); - if (valMs > 24 * 60 * 60 * 1000) - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); - }), - maxTTL: z - .string() - .optional() - .superRefine((val, ctx) => { - if (!val) return; +const formSchema = z + .object({ + inputs: z.discriminatedUnion("credentialType", [ + z.object({ + url: z.string().url().trim().min(1), + clusterToken: z.string().trim().optional(), + ca: z.string().optional(), + sslEnabled: z.boolean().default(false), + credentialType: z.literal(KubernetesDynamicSecretCredentialType.Static), + serviceAccountName: z.string().trim().min(1), + namespace: z.string().trim().min(1), + gatewayId: z.string().optional(), + audiences: z.array(z.string().trim().min(1)), + authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api) + }), + z.object({ + url: z.string().url().trim().min(1), + clusterToken: z.string().trim().optional(), + ca: z.string().optional(), + sslEnabled: z.boolean().default(false), + credentialType: z.literal(KubernetesDynamicSecretCredentialType.Dynamic), + namespace: z.string().trim().min(1), + gatewayId: z.string().optional(), + audiences: z.array(z.string().trim().min(1)), + roleType: z.nativeEnum(RoleType), + role: z.string().trim().min(1), + authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api) + }) + ]), + defaultTTL: z.string().superRefine((val, ctx) => { const valMs = ms(val); if (valMs < 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - newName: slugSchema().optional() -}); + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + newName: slugSchema().optional(), + usernameTemplate: z.string().trim().optional() + }) + .superRefine((data, ctx) => { + if (data.inputs.authMethod === AuthMethod.Gateway && !data.inputs.gatewayId) { + ctx.addIssue({ + path: ["inputs.gatewayId"], + code: z.ZodIssueCode.custom, + message: "When auth method is set to Gateway, a gateway must be selected" + }); + } + if (data.inputs.authMethod === AuthMethod.Api && !data.inputs.clusterToken) { + ctx.addIssue({ + path: ["inputs.clusterToken"], + code: z.ZodIssueCode.custom, + message: "When auth method is set to Token, a cluster token must be provided" + }); + } + }); type TForm = z.infer & FieldValues; @@ -103,6 +149,7 @@ export const EditDynamicSecretKubernetesForm = ({ values: { newName: dynamicSecret.name, defaultTTL: dynamicSecret.defaultTTL, + usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}", maxTTL: dynamicSecret.maxTTL, inputs: dynamicSecret.inputs as TForm["inputs"] } @@ -110,17 +157,20 @@ export const EditDynamicSecretKubernetesForm = ({ const { fields, append, remove } = useFieldArray({ control, - name: "inputs.audiences" as const + name: "inputs.audiences" }); const updateDynamicSecret = useUpdateDynamicSecret(); const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const sslEnabled = watch("inputs.sslEnabled"); + const credentialType = watch("inputs.credentialType"); + const authMethod = watch("inputs.authMethod"); const handleUpdateDynamicSecret = async (formData: TForm) => { // wait till previous request is finished if (updateDynamicSecret.isPending) return; + const isDefaultUsernameTemplate = formData.usernameTemplate === "{{randomUsername}}"; try { await updateDynamicSecret.mutateAsync({ name: dynamicSecret.name, @@ -131,9 +181,14 @@ export const EditDynamicSecretKubernetesForm = ({ inputs: formData.inputs, newName: formData.newName === dynamicSecret.name ? undefined : formData.newName, defaultTTL: formData.defaultTTL, - maxTTL: formData.maxTTL + maxTTL: formData.maxTTL, + usernameTemplate: + !formData.usernameTemplate || isDefaultUsernameTemplate + ? null + : formData.usernameTemplate } }); + onClose(); createNotification({ type: "success", @@ -339,17 +394,43 @@ export const EditDynamicSecretKubernetesForm = ({ ( - + )} /> + {authMethod === AuthMethod.Api && ( + ( + + + + )} + /> + )} @@ -379,21 +458,44 @@ export const EditDynamicSecretKubernetesForm = ({ )} />
-
- ( - - - - )} - /> -
+ {credentialType === KubernetesDynamicSecretCredentialType.Static && ( +
+ ( + + + + )} + /> +
+ )} + {credentialType === KubernetesDynamicSecretCredentialType.Dynamic && ( +
+ ( + + + + )} + /> +
+ )}
+ {credentialType === KubernetesDynamicSecretCredentialType.Dynamic && ( +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx index cffefcfd3..88e146c48 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GcpSyncDestinationSection.tsx @@ -1,14 +1,22 @@ import { GenericFieldLabel } from "@app/components/secret-syncs"; -import { TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync"; +import { GcpSyncScope, TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync"; type Props = { secretSync: TGcpSync; }; export const GcpSyncDestinationSection = ({ secretSync }: Props) => { - const { - destinationConfig: { projectId } - } = secretSync; + const { destinationConfig } = secretSync; - return {projectId}; + return ( + <> + {destinationConfig.projectId} + + {destinationConfig.scope} + + {destinationConfig.scope === GcpSyncScope.Region && ( + {destinationConfig.locationId} + )} + + ); }; diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 449ea28cb..9496230af 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -115,19 +115,24 @@ import { Route as certManagerAlertingPageRouteImport } from './pages/cert-manage import { Route as projectRoleDetailsBySlugPageRouteSshImport } from './pages/project/RoleDetailsBySlugPage/route-ssh' import { Route as projectMemberDetailsByIDPageRouteSshImport } from './pages/project/MemberDetailsByIDPage/route-ssh' import { Route as projectIdentityDetailsByIDPageRouteSshImport } from './pages/project/IdentityDetailsByIDPage/route-ssh' +import { Route as projectGroupDetailsByIDPageRouteSshImport } from './pages/project/GroupDetailsByIDPage/route-ssh' import { Route as projectRoleDetailsBySlugPageRouteSecretScanningImport } from './pages/project/RoleDetailsBySlugPage/route-secret-scanning' import { Route as projectMemberDetailsByIDPageRouteSecretScanningImport } from './pages/project/MemberDetailsByIDPage/route-secret-scanning' import { Route as projectIdentityDetailsByIDPageRouteSecretScanningImport } from './pages/project/IdentityDetailsByIDPage/route-secret-scanning' +import { Route as projectGroupDetailsByIDPageRouteSecretScanningImport } from './pages/project/GroupDetailsByIDPage/route-secret-scanning' import { Route as projectRoleDetailsBySlugPageRouteSecretManagerImport } from './pages/project/RoleDetailsBySlugPage/route-secret-manager' import { Route as projectMemberDetailsByIDPageRouteSecretManagerImport } from './pages/project/MemberDetailsByIDPage/route-secret-manager' import { Route as projectIdentityDetailsByIDPageRouteSecretManagerImport } from './pages/project/IdentityDetailsByIDPage/route-secret-manager' +import { Route as projectGroupDetailsByIDPageRouteSecretManagerImport } from './pages/project/GroupDetailsByIDPage/route-secret-manager' import { Route as projectRoleDetailsBySlugPageRouteKmsImport } from './pages/project/RoleDetailsBySlugPage/route-kms' import { Route as projectMemberDetailsByIDPageRouteKmsImport } from './pages/project/MemberDetailsByIDPage/route-kms' import { Route as projectIdentityDetailsByIDPageRouteKmsImport } from './pages/project/IdentityDetailsByIDPage/route-kms' +import { Route as projectGroupDetailsByIDPageRouteKmsImport } from './pages/project/GroupDetailsByIDPage/route-kms' import { Route as projectRoleDetailsBySlugPageRouteCertManagerImport } from './pages/project/RoleDetailsBySlugPage/route-cert-manager' import { Route as certManagerPkiCollectionDetailsByIDPageRoutesImport } from './pages/cert-manager/PkiCollectionDetailsByIDPage/routes' import { Route as projectMemberDetailsByIDPageRouteCertManagerImport } from './pages/project/MemberDetailsByIDPage/route-cert-manager' import { Route as projectIdentityDetailsByIDPageRouteCertManagerImport } from './pages/project/IdentityDetailsByIDPage/route-cert-manager' +import { Route as projectGroupDetailsByIDPageRouteCertManagerImport } from './pages/project/GroupDetailsByIDPage/route-cert-manager' import { Route as sshSshHostGroupDetailsByIDPageRouteImport } from './pages/ssh/SshHostGroupDetailsByIDPage/route' import { Route as sshSshCaByIDPageRouteImport } from './pages/ssh/SshCaByIDPage/route' import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route' @@ -1149,6 +1154,13 @@ const projectIdentityDetailsByIDPageRouteSshRoute = getParentRoute: () => sshLayoutRoute, } as any) +const projectGroupDetailsByIDPageRouteSshRoute = + projectGroupDetailsByIDPageRouteSshImport.update({ + id: '/groups/$groupId', + path: '/groups/$groupId', + getParentRoute: () => sshLayoutRoute, + } as any) + const projectRoleDetailsBySlugPageRouteSecretScanningRoute = projectRoleDetailsBySlugPageRouteSecretScanningImport.update({ id: '/roles/$roleSlug', @@ -1170,6 +1182,13 @@ const projectIdentityDetailsByIDPageRouteSecretScanningRoute = getParentRoute: () => secretScanningLayoutRoute, } as any) +const projectGroupDetailsByIDPageRouteSecretScanningRoute = + projectGroupDetailsByIDPageRouteSecretScanningImport.update({ + id: '/groups/$groupId', + path: '/groups/$groupId', + getParentRoute: () => secretScanningLayoutRoute, + } as any) + const projectRoleDetailsBySlugPageRouteSecretManagerRoute = projectRoleDetailsBySlugPageRouteSecretManagerImport.update({ id: '/roles/$roleSlug', @@ -1191,6 +1210,13 @@ const projectIdentityDetailsByIDPageRouteSecretManagerRoute = getParentRoute: () => secretManagerLayoutRoute, } as any) +const projectGroupDetailsByIDPageRouteSecretManagerRoute = + projectGroupDetailsByIDPageRouteSecretManagerImport.update({ + id: '/groups/$groupId', + path: '/groups/$groupId', + getParentRoute: () => secretManagerLayoutRoute, + } as any) + const projectRoleDetailsBySlugPageRouteKmsRoute = projectRoleDetailsBySlugPageRouteKmsImport.update({ id: '/roles/$roleSlug', @@ -1212,6 +1238,13 @@ const projectIdentityDetailsByIDPageRouteKmsRoute = getParentRoute: () => kmsLayoutRoute, } as any) +const projectGroupDetailsByIDPageRouteKmsRoute = + projectGroupDetailsByIDPageRouteKmsImport.update({ + id: '/groups/$groupId', + path: '/groups/$groupId', + getParentRoute: () => kmsLayoutRoute, + } as any) + const projectRoleDetailsBySlugPageRouteCertManagerRoute = projectRoleDetailsBySlugPageRouteCertManagerImport.update({ id: '/roles/$roleSlug', @@ -1240,6 +1273,13 @@ const projectIdentityDetailsByIDPageRouteCertManagerRoute = getParentRoute: () => certManagerLayoutRoute, } as any) +const projectGroupDetailsByIDPageRouteCertManagerRoute = + projectGroupDetailsByIDPageRouteCertManagerImport.update({ + id: '/groups/$groupId', + path: '/groups/$groupId', + getParentRoute: () => certManagerLayoutRoute, + } as any) + const sshSshHostGroupDetailsByIDPageRouteRoute = sshSshHostGroupDetailsByIDPageRouteImport.update({ id: '/ssh-host-groups/$sshHostGroupId', @@ -2861,6 +2901,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof sshSshHostGroupDetailsByIDPageRouteImport parentRoute: typeof sshLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId' + path: '/groups/$groupId' + fullPath: '/cert-manager/$projectId/groups/$groupId' + preLoaderRoute: typeof projectGroupDetailsByIDPageRouteCertManagerImport + parentRoute: typeof certManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId': { id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId' path: '/identities/$identityId' @@ -2889,6 +2936,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteCertManagerImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId': { + id: '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId' + path: '/groups/$groupId' + fullPath: '/kms/$projectId/groups/$groupId' + preLoaderRoute: typeof projectGroupDetailsByIDPageRouteKmsImport + parentRoute: typeof kmsLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId': { id: '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId' path: '/identities/$identityId' @@ -2910,6 +2964,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteKmsImport parentRoute: typeof kmsLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId': { + id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId' + path: '/groups/$groupId' + fullPath: '/secret-manager/$projectId/groups/$groupId' + preLoaderRoute: typeof projectGroupDetailsByIDPageRouteSecretManagerImport + parentRoute: typeof secretManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId': { id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId' path: '/identities/$identityId' @@ -2931,6 +2992,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteSecretManagerImport parentRoute: typeof secretManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId': { + id: '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId' + path: '/groups/$groupId' + fullPath: '/secret-scanning/$projectId/groups/$groupId' + preLoaderRoute: typeof projectGroupDetailsByIDPageRouteSecretScanningImport + parentRoute: typeof secretScanningLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId': { id: '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId' path: '/identities/$identityId' @@ -2952,6 +3020,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteSecretScanningImport parentRoute: typeof secretScanningLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId': { + id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId' + path: '/groups/$groupId' + fullPath: '/ssh/$projectId/groups/$groupId' + preLoaderRoute: typeof projectGroupDetailsByIDPageRouteSshImport + parentRoute: typeof sshLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId': { id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId' path: '/identities/$identityId' @@ -3735,6 +3810,7 @@ interface certManagerLayoutRouteChildren { AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute + projectGroupDetailsByIDPageRouteCertManagerRoute: typeof projectGroupDetailsByIDPageRouteCertManagerRoute projectIdentityDetailsByIDPageRouteCertManagerRoute: typeof projectIdentityDetailsByIDPageRouteCertManagerRoute projectMemberDetailsByIDPageRouteCertManagerRoute: typeof projectMemberDetailsByIDPageRouteCertManagerRoute certManagerPkiCollectionDetailsByIDPageRoutesRoute: typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute @@ -3755,6 +3831,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = { AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren, certManagerCertAuthDetailsByIDPageRouteRoute: certManagerCertAuthDetailsByIDPageRouteRoute, + projectGroupDetailsByIDPageRouteCertManagerRoute: + projectGroupDetailsByIDPageRouteCertManagerRoute, projectIdentityDetailsByIDPageRouteCertManagerRoute: projectIdentityDetailsByIDPageRouteCertManagerRoute, projectMemberDetailsByIDPageRouteCertManagerRoute: @@ -3787,6 +3865,7 @@ interface kmsLayoutRouteChildren { kmsOverviewPageRouteRoute: typeof kmsOverviewPageRouteRoute kmsSettingsPageRouteRoute: typeof kmsSettingsPageRouteRoute projectAccessControlPageRouteKmsRoute: typeof projectAccessControlPageRouteKmsRoute + projectGroupDetailsByIDPageRouteKmsRoute: typeof projectGroupDetailsByIDPageRouteKmsRoute projectIdentityDetailsByIDPageRouteKmsRoute: typeof projectIdentityDetailsByIDPageRouteKmsRoute projectMemberDetailsByIDPageRouteKmsRoute: typeof projectMemberDetailsByIDPageRouteKmsRoute projectRoleDetailsBySlugPageRouteKmsRoute: typeof projectRoleDetailsBySlugPageRouteKmsRoute @@ -3797,6 +3876,8 @@ const kmsLayoutRouteChildren: kmsLayoutRouteChildren = { kmsOverviewPageRouteRoute: kmsOverviewPageRouteRoute, kmsSettingsPageRouteRoute: kmsSettingsPageRouteRoute, projectAccessControlPageRouteKmsRoute: projectAccessControlPageRouteKmsRoute, + projectGroupDetailsByIDPageRouteKmsRoute: + projectGroupDetailsByIDPageRouteKmsRoute, projectIdentityDetailsByIDPageRouteKmsRoute: projectIdentityDetailsByIDPageRouteKmsRoute, projectMemberDetailsByIDPageRouteKmsRoute: @@ -4078,6 +4159,7 @@ interface secretManagerLayoutRouteChildren { projectAccessControlPageRouteSecretManagerRoute: typeof projectAccessControlPageRouteSecretManagerRoute AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren secretManagerSecretDashboardPageRouteRoute: typeof secretManagerSecretDashboardPageRouteRoute + projectGroupDetailsByIDPageRouteSecretManagerRoute: typeof projectGroupDetailsByIDPageRouteSecretManagerRoute projectIdentityDetailsByIDPageRouteSecretManagerRoute: typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute projectMemberDetailsByIDPageRouteSecretManagerRoute: typeof projectMemberDetailsByIDPageRouteSecretManagerRoute projectRoleDetailsBySlugPageRouteSecretManagerRoute: typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute @@ -4098,6 +4180,8 @@ const secretManagerLayoutRouteChildren: secretManagerLayoutRouteChildren = { AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren, secretManagerSecretDashboardPageRouteRoute: secretManagerSecretDashboardPageRouteRoute, + projectGroupDetailsByIDPageRouteSecretManagerRoute: + projectGroupDetailsByIDPageRouteSecretManagerRoute, projectIdentityDetailsByIDPageRouteSecretManagerRoute: projectIdentityDetailsByIDPageRouteSecretManagerRoute, projectMemberDetailsByIDPageRouteSecretManagerRoute: @@ -4146,6 +4230,7 @@ interface secretScanningLayoutRouteChildren { secretScanningSettingsPageRouteRoute: typeof secretScanningSettingsPageRouteRoute projectAccessControlPageRouteSecretScanningRoute: typeof projectAccessControlPageRouteSecretScanningRoute AuthenticateInjectOrgDetailsOrgLayoutSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren + projectGroupDetailsByIDPageRouteSecretScanningRoute: typeof projectGroupDetailsByIDPageRouteSecretScanningRoute projectIdentityDetailsByIDPageRouteSecretScanningRoute: typeof projectIdentityDetailsByIDPageRouteSecretScanningRoute projectMemberDetailsByIDPageRouteSecretScanningRoute: typeof projectMemberDetailsByIDPageRouteSecretScanningRoute projectRoleDetailsBySlugPageRouteSecretScanningRoute: typeof projectRoleDetailsBySlugPageRouteSecretScanningRoute @@ -4159,6 +4244,8 @@ const secretScanningLayoutRouteChildren: secretScanningLayoutRouteChildren = { projectAccessControlPageRouteSecretScanningRoute, AuthenticateInjectOrgDetailsOrgLayoutSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: AuthenticateInjectOrgDetailsOrgLayoutSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren, + projectGroupDetailsByIDPageRouteSecretScanningRoute: + projectGroupDetailsByIDPageRouteSecretScanningRoute, projectIdentityDetailsByIDPageRouteSecretScanningRoute: projectIdentityDetailsByIDPageRouteSecretScanningRoute, projectMemberDetailsByIDPageRouteSecretScanningRoute: @@ -4192,6 +4279,7 @@ interface sshLayoutRouteChildren { projectAccessControlPageRouteSshRoute: typeof projectAccessControlPageRouteSshRoute sshSshCaByIDPageRouteRoute: typeof sshSshCaByIDPageRouteRoute sshSshHostGroupDetailsByIDPageRouteRoute: typeof sshSshHostGroupDetailsByIDPageRouteRoute + projectGroupDetailsByIDPageRouteSshRoute: typeof projectGroupDetailsByIDPageRouteSshRoute projectIdentityDetailsByIDPageRouteSshRoute: typeof projectIdentityDetailsByIDPageRouteSshRoute projectMemberDetailsByIDPageRouteSshRoute: typeof projectMemberDetailsByIDPageRouteSshRoute projectRoleDetailsBySlugPageRouteSshRoute: typeof projectRoleDetailsBySlugPageRouteSshRoute @@ -4206,6 +4294,8 @@ const sshLayoutRouteChildren: sshLayoutRouteChildren = { sshSshCaByIDPageRouteRoute: sshSshCaByIDPageRouteRoute, sshSshHostGroupDetailsByIDPageRouteRoute: sshSshHostGroupDetailsByIDPageRouteRoute, + projectGroupDetailsByIDPageRouteSshRoute: + projectGroupDetailsByIDPageRouteSshRoute, projectIdentityDetailsByIDPageRouteSshRoute: projectIdentityDetailsByIDPageRouteSshRoute, projectMemberDetailsByIDPageRouteSshRoute: @@ -4561,19 +4651,24 @@ export interface FileRoutesByFullPath { '/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute '/ssh/$projectId/ca/$caId': typeof sshSshCaByIDPageRouteRoute '/ssh/$projectId/ssh-host-groups/$sshHostGroupId': typeof sshSshHostGroupDetailsByIDPageRouteRoute + '/cert-manager/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute '/cert-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteCertManagerRoute + '/kms/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteKmsRoute '/kms/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute '/kms/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute '/kms/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute + '/secret-manager/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute + '/secret-scanning/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretScanningRoute + '/ssh/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSshRoute '/ssh/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSshRoute '/ssh/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSshRoute '/ssh/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSshRoute @@ -4764,19 +4859,24 @@ export interface FileRoutesByTo { '/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute '/ssh/$projectId/ca/$caId': typeof sshSshCaByIDPageRouteRoute '/ssh/$projectId/ssh-host-groups/$sshHostGroupId': typeof sshSshHostGroupDetailsByIDPageRouteRoute + '/cert-manager/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute '/cert-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteCertManagerRoute + '/kms/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteKmsRoute '/kms/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute '/kms/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute '/kms/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute + '/secret-manager/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute + '/secret-scanning/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretScanningRoute + '/ssh/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSshRoute '/ssh/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSshRoute '/ssh/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSshRoute '/ssh/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSshRoute @@ -4990,19 +5090,24 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId': typeof sshSshCaByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId': typeof sshSshHostGroupDetailsByIDPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteCertManagerRoute + '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteKmsRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute + '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute + '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretScanningRoute '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretScanningRoute '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretScanningRoute '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretScanningRoute + '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSshRoute @@ -5207,19 +5312,24 @@ export interface FileRouteTypes { | '/secret-manager/$projectId/secrets/$envSlug' | '/ssh/$projectId/ca/$caId' | '/ssh/$projectId/ssh-host-groups/$sshHostGroupId' + | '/cert-manager/$projectId/groups/$groupId' | '/cert-manager/$projectId/identities/$identityId' | '/cert-manager/$projectId/members/$membershipId' | '/cert-manager/$projectId/pki-collections/$collectionId' | '/cert-manager/$projectId/roles/$roleSlug' + | '/kms/$projectId/groups/$groupId' | '/kms/$projectId/identities/$identityId' | '/kms/$projectId/members/$membershipId' | '/kms/$projectId/roles/$roleSlug' + | '/secret-manager/$projectId/groups/$groupId' | '/secret-manager/$projectId/identities/$identityId' | '/secret-manager/$projectId/members/$membershipId' | '/secret-manager/$projectId/roles/$roleSlug' + | '/secret-scanning/$projectId/groups/$groupId' | '/secret-scanning/$projectId/identities/$identityId' | '/secret-scanning/$projectId/members/$membershipId' | '/secret-scanning/$projectId/roles/$roleSlug' + | '/ssh/$projectId/groups/$groupId' | '/ssh/$projectId/identities/$identityId' | '/ssh/$projectId/members/$membershipId' | '/ssh/$projectId/roles/$roleSlug' @@ -5409,19 +5519,24 @@ export interface FileRouteTypes { | '/secret-manager/$projectId/secrets/$envSlug' | '/ssh/$projectId/ca/$caId' | '/ssh/$projectId/ssh-host-groups/$sshHostGroupId' + | '/cert-manager/$projectId/groups/$groupId' | '/cert-manager/$projectId/identities/$identityId' | '/cert-manager/$projectId/members/$membershipId' | '/cert-manager/$projectId/pki-collections/$collectionId' | '/cert-manager/$projectId/roles/$roleSlug' + | '/kms/$projectId/groups/$groupId' | '/kms/$projectId/identities/$identityId' | '/kms/$projectId/members/$membershipId' | '/kms/$projectId/roles/$roleSlug' + | '/secret-manager/$projectId/groups/$groupId' | '/secret-manager/$projectId/identities/$identityId' | '/secret-manager/$projectId/members/$membershipId' | '/secret-manager/$projectId/roles/$roleSlug' + | '/secret-scanning/$projectId/groups/$groupId' | '/secret-scanning/$projectId/identities/$identityId' | '/secret-scanning/$projectId/members/$membershipId' | '/secret-scanning/$projectId/roles/$roleSlug' + | '/ssh/$projectId/groups/$groupId' | '/ssh/$projectId/identities/$identityId' | '/ssh/$projectId/members/$membershipId' | '/ssh/$projectId/roles/$roleSlug' @@ -5633,19 +5748,24 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/roles/$roleSlug' + | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/roles/$roleSlug' + | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug' + | '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/roles/$roleSlug' + | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/roles/$roleSlug' @@ -6206,6 +6326,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId", @@ -6220,6 +6341,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/roles/$roleSlug" @@ -6237,6 +6359,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management", "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations", "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug", + "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug" @@ -6250,6 +6373,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/settings", "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/access-management", "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/data-sources", + "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/roles/$roleSlug" @@ -6266,6 +6390,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId", + "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/roles/$roleSlug" @@ -6558,6 +6683,10 @@ export const routeTree = rootRoute "filePath": "ssh/SshHostGroupDetailsByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" }, + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId": { + "filePath": "project/GroupDetailsByIDPage/route-cert-manager.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" + }, "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId": { "filePath": "project/IdentityDetailsByIDPage/route-cert-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" @@ -6574,6 +6703,10 @@ export const routeTree = rootRoute "filePath": "project/RoleDetailsBySlugPage/route-cert-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId": { + "filePath": "project/GroupDetailsByIDPage/route-kms.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout" + }, "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId": { "filePath": "project/IdentityDetailsByIDPage/route-kms.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout" @@ -6586,6 +6719,10 @@ export const routeTree = rootRoute "filePath": "project/RoleDetailsBySlugPage/route-kms.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout" }, + "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId": { + "filePath": "project/GroupDetailsByIDPage/route-secret-manager.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout" + }, "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId": { "filePath": "project/IdentityDetailsByIDPage/route-secret-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout" @@ -6598,6 +6735,10 @@ export const routeTree = rootRoute "filePath": "project/RoleDetailsBySlugPage/route-secret-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId": { + "filePath": "project/GroupDetailsByIDPage/route-secret-scanning.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout" + }, "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId": { "filePath": "project/IdentityDetailsByIDPage/route-secret-scanning.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout" @@ -6610,6 +6751,10 @@ export const routeTree = rootRoute "filePath": "project/RoleDetailsBySlugPage/route-secret-scanning.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout" }, + "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId": { + "filePath": "project/GroupDetailsByIDPage/route-ssh.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" + }, "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId": { "filePath": "project/IdentityDetailsByIDPage/route-ssh.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 0c14e27c9..2572f9ce2 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -270,7 +270,8 @@ const secretManagerRoutes = route("/secret-manager/$projectId", [ route("/access-management", "project/AccessControlPage/route-secret-manager.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-manager.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-manager.tsx"), - route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-manager.tsx") + route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-manager.tsx"), + route("/groups/$groupId", "project/GroupDetailsByIDPage/route-secret-manager.tsx") ]) ]); @@ -314,7 +315,8 @@ const certManagerRoutes = route("/cert-manager/$projectId", [ route("/access-management", "project/AccessControlPage/route-cert-manager.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-cert-manager.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-cert-manager.tsx"), - route("/members/$membershipId", "project/MemberDetailsByIDPage/route-cert-manager.tsx") + route("/members/$membershipId", "project/MemberDetailsByIDPage/route-cert-manager.tsx"), + route("/groups/$groupId", "project/GroupDetailsByIDPage/route-cert-manager.tsx") ]) ]); @@ -326,7 +328,8 @@ const kmsRoutes = route("/kms/$projectId", [ route("/access-management", "project/AccessControlPage/route-kms.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-kms.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-kms.tsx"), - route("/members/$membershipId", "project/MemberDetailsByIDPage/route-kms.tsx") + route("/members/$membershipId", "project/MemberDetailsByIDPage/route-kms.tsx"), + route("/groups/$groupId", "project/GroupDetailsByIDPage/route-kms.tsx") ]) ]); @@ -341,7 +344,8 @@ const sshRoutes = route("/ssh/$projectId", [ route("/access-management", "project/AccessControlPage/route-ssh.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-ssh.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-ssh.tsx"), - route("/members/$membershipId", "project/MemberDetailsByIDPage/route-ssh.tsx") + route("/members/$membershipId", "project/MemberDetailsByIDPage/route-ssh.tsx"), + route("/groups/$groupId", "project/GroupDetailsByIDPage/route-ssh.tsx") ]) ]); @@ -356,7 +360,8 @@ const secretScanningRoutes = route("/secret-scanning/$projectId", [ route("/access-management", "project/AccessControlPage/route-secret-scanning.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-scanning.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-scanning.tsx"), - route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-scanning.tsx") + route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-scanning.tsx"), + route("/groups/$groupId", "project/GroupDetailsByIDPage/route-secret-scanning.tsx") ]) ]); diff --git a/helm-charts/infisical-gateway/Chart.yaml b/helm-charts/infisical-gateway/Chart.yaml index 8d9d4dac3..5bb18e702 100644 --- a/helm-charts/infisical-gateway/Chart.yaml +++ b/helm-charts/infisical-gateway/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.0.3 +version: 0.0.4 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.0.3" +appVersion: "0.0.4" diff --git a/helm-charts/infisical-gateway/templates/deployment.yaml b/helm-charts/infisical-gateway/templates/deployment.yaml index 223bdabd8..a6fac0e7c 100644 --- a/helm-charts/infisical-gateway/templates/deployment.yaml +++ b/helm-charts/infisical-gateway/templates/deployment.yaml @@ -39,18 +39,9 @@ spec: imagePullPolicy: {{ .Values.image.pullPolicy }} args: - gateway - - --token - - $(TOKEN) envFrom: - secretRef: name: {{ .Values.secret.name }} - env: - - name: TOKEN_VALIDATION - valueFrom: - secretKeyRef: - name: {{ .Values.secret.name }} - key: TOKEN - optional: false ports: - name: http containerPort: {{ .Values.service.port }} diff --git a/helm-charts/infisical-gateway/values.yaml b/helm-charts/infisical-gateway/values.yaml index 9e897f461..bf1a81767 100644 --- a/helm-charts/infisical-gateway/values.yaml +++ b/helm-charts/infisical-gateway/values.yaml @@ -1,6 +1,6 @@ image: pullPolicy: IfNotPresent - tag: "0.41.81" + tag: "0.41.83" secret: # The secret that contains the environment variables to be used by the gateway, such as INFISICAL_API_URL and TOKEN