Merge branch 'heads/main' into daniel/gateway-docs

This commit is contained in:
Daniel Hougaard
2025-06-07 03:00:16 +04:00
103 changed files with 3194 additions and 756 deletions
+4
View File
@@ -119,6 +119,10 @@ declare module "@fastify/request-context" {
oidc?: { oidc?: {
claims: Record<string, string>; claims: Record<string, string>;
}; };
kubernetes?: {
namespace: string;
name: string;
};
}; };
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string }; assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string };
@@ -23,7 +23,10 @@ const validateUsernameTemplateCharacters = characterValidator([
CharacterType.CloseBrace, CharacterType.CloseBrace,
CharacterType.CloseBracket, CharacterType.CloseBracket,
CharacterType.OpenBracket, CharacterType.OpenBracket,
CharacterType.Fullstop CharacterType.Fullstop,
CharacterType.SingleQuote,
CharacterType.Spaces,
CharacterType.Pipe
]); ]);
const userTemplateSchema = z const userTemplateSchema = z
@@ -33,7 +36,7 @@ const userTemplateSchema = z
.refine((el) => validateUsernameTemplateCharacters(el)) .refine((el) => validateUsernameTemplateCharacters(el))
.refine((el) => .refine((el) =>
isValidHandleBarTemplate(el, { isValidHandleBarTemplate(el, {
allowedExpressions: (val) => ["randomUsername", "unixTimestamp"].includes(val) allowedExpressions: (val) => ["randomUsername", "unixTimestamp", "identity.name"].includes(val)
}) })
); );
@@ -99,7 +99,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object; ) as object;
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, {
projectId: folder.projectId
});
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id); await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
return; return;
} }
@@ -133,7 +135,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
await Promise.all( await Promise.all(
dynamicSecretLeases.map(({ externalEntityId }) => dynamicSecretLeases.map(({ externalEntityId }) =>
selectedProvider.revoke(decryptedStoredInput, externalEntityId) selectedProvider.revoke(decryptedStoredInput, externalEntityId, {
projectId: folder.projectId
})
) )
); );
} }
@@ -1,4 +1,5 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import RE2 from "re2";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; 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 { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { ms } from "@app/lib/ms"; 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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-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 { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models"; import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
@@ -39,6 +43,8 @@ type TDynamicSecretLeaseServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
userDAL: Pick<TUserDALFactory, "findById">;
identityDAL: TIdentityDALFactory;
}; };
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>; export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
@@ -52,8 +58,16 @@ export const dynamicSecretLeaseServiceFactory = ({
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
licenseService, licenseService,
kmsService kmsService,
userDAL,
identityDAL
}: TDynamicSecretLeaseServiceFactoryDep) => { }: TDynamicSecretLeaseServiceFactoryDep) => {
const extractEmailUsername = (email: string) => {
const regex = new RE2(/^([^@]+)/);
const match = email.match(regex);
return match ? match[1] : email;
};
const create = async ({ const create = async ({
environmentSlug, environmentSlug,
path, path,
@@ -132,10 +146,24 @@ export const dynamicSecretLeaseServiceFactory = ({
let result; let result;
try { 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({ result = await selectedProvider.create({
inputs: decryptedStoredInput, inputs: decryptedStoredInput,
expireAt: expireAt.getTime(), expireAt: expireAt.getTime(),
usernameTemplate: dynamicSecretCfg.usernameTemplate usernameTemplate: dynamicSecretCfg.usernameTemplate,
identity,
metadata: { projectId }
}); });
} catch (error: unknown) { } catch (error: unknown) {
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) { if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
@@ -237,7 +265,8 @@ export const dynamicSecretLeaseServiceFactory = ({
const { entityId } = await selectedProvider.renew( const { entityId } = await selectedProvider.renew(
decryptedStoredInput, decryptedStoredInput,
dynamicSecretLease.externalEntityId, dynamicSecretLease.externalEntityId,
expireAt.getTime() expireAt.getTime(),
{ projectId }
); );
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
@@ -313,7 +342,7 @@ export const dynamicSecretLeaseServiceFactory = ({
) as object; ) as object;
const revokeResponse = await selectedProvider const revokeResponse = await selectedProvider
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId) .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { projectId })
.catch(async (err) => { .catch(async (err) => {
// only propogate this error if forced is false // only propogate this error if forced is false
if (!isForced) return { error: err as Error }; if (!isForced) return { error: err as Error };
@@ -116,7 +116,7 @@ export const dynamicSecretServiceFactory = ({
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" }); throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
const selectedProvider = dynamicSecretProviders[provider.type]; 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; let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) { if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
@@ -146,7 +146,7 @@ export const dynamicSecretServiceFactory = ({
selectedGatewayId = gateway.id; 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" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
@@ -272,7 +272,7 @@ export const dynamicSecretServiceFactory = ({
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object; ) as object;
const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const updatedInput = await selectedProvider.validateProviderInputs(newInput); const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId });
let selectedGatewayId: string | null = null; let selectedGatewayId: string | null = null;
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) { if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
@@ -301,7 +301,7 @@ export const dynamicSecretServiceFactory = ({
selectedGatewayId = gateway.id; 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" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => { const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
@@ -472,7 +472,9 @@ export const dynamicSecretServiceFactory = ({
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object; ) as object;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; 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 }; return { ...dynamicSecretCfg, inputs: providerInputs };
}; };
@@ -16,6 +16,7 @@ import { BadRequestError } from "@app/lib/errors";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models"; import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const CreateElastiCacheUserSchema = z.object({ const CreateElastiCacheUserSchema = z.object({
UserId: z.string().trim().min(1), UserId: z.string().trim().min(1),
@@ -132,14 +133,14 @@ const generatePassword = () => {
return customAlphabet(charset, 64)(); return customAlphabet(charset, 64)();
}; };
const generateUsername = (usernameTemplate?: string | null) => { const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-"; const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-";
const randomUsername = `inf-${customAlphabet(charset, 32)()}`; const randomUsername = `inf-${customAlphabet(charset, 32)()}`;
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -174,14 +175,21 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
return true; return true;
}; };
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { const create = async (data: {
const { inputs, expireAt, usernameTemplate } = data; inputs: unknown;
expireAt: number;
usernameTemplate?: string | null;
identity?: {
name: string;
};
}) => {
const { inputs, expireAt, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
if (!(await validateConnection(providerInputs))) { if (!(await validateConnection(providerInputs))) {
throw new BadRequestError({ message: "Failed to establish connection" }); throw new BadRequestError({ message: "Failed to establish connection" });
} }
const leaseUsername = generateUsername(usernameTemplate); const leaseUsername = generateUsername(usernameTemplate, identity);
const leasePassword = generatePassword(); const leasePassword = generatePassword();
const leaseExpiration = new Date(expireAt).toISOString(); const leaseExpiration = new Date(expireAt).toISOString();
@@ -16,21 +16,25 @@ import {
PutUserPolicyCommand, PutUserPolicyCommand,
RemoveUserFromGroupCommand RemoveUserFromGroupCommand
} from "@aws-sdk/client-iam"; } 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 { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; 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); const randomUsername = alphaNumericNanoId(32);
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return handlebars.compile(usernameTemplate)({ return compileUsernameTemplate({
usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -40,7 +44,43 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
return providerInputs; return providerInputs;
}; };
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => { const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>, 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({ const client = new IAMClient({
region: providerInputs.region, region: providerInputs.region,
credentials: { credentials: {
@@ -52,21 +92,41 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
return client; return client;
}; };
const validateConnection = async (inputs: unknown) => { const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs, projectId);
const isConnected = await client
const isConnected = await client.send(new GetUserCommand({})).then(() => true); .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; return isConnected;
}; };
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { const create = async (data: {
const { inputs, usernameTemplate } = 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 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 { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
const createUserRes = await client.send( const createUserRes = await client.send(
new CreateUserCommand({ new CreateUserCommand({
@@ -76,6 +136,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
UserName: username UserName: username
}) })
); );
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" }); if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
if (userGroups) { if (userGroups) {
await Promise.all( 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 providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs, metadata.projectId);
const username = entityId; const username = entityId;
@@ -8,19 +8,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars
import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models"; import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const generatePassword = (size = 48) => { const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 48)(size); 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-" const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -75,12 +76,17 @@ export const CassandraProvider = (): TDynamicProviderFns => {
return isConnected; return isConnected;
}; };
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { const create = async (data: {
const { inputs, expireAt, usernameTemplate } = data; inputs: unknown;
expireAt: number;
usernameTemplate?: string | null;
identity?: { name: string };
}) => {
const { inputs, expireAt, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs);
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(); const password = generatePassword();
const { keyspace } = providerInputs; const { keyspace } = providerInputs;
const expiration = new Date(expireAt).toISOString(); const expiration = new Date(expireAt).toISOString();
@@ -1,5 +1,4 @@
import { Client as ElasticSearchClient } from "@elastic/elasticsearch"; import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
import handlebars from "handlebars";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -7,19 +6,20 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models"; import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const generatePassword = () => { const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 64)(); 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-" const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -71,12 +71,12 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
return infoResponse; return infoResponse;
}; };
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => { const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
const { inputs, usernameTemplate } = data; const { inputs, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs); const connection = await $getClient(providerInputs);
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(); const password = generatePassword();
await connection.security.putUser({ await connection.security.putUser({
@@ -9,6 +9,7 @@ import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models"; import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const generatePassword = () => { const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
@@ -22,13 +23,13 @@ const encodePassword = (password?: string) => {
return base64Password; 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-" const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -196,8 +197,8 @@ export const LdapProvider = (): TDynamicProviderFns => {
return dnArray; return dnArray;
}; };
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => { const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
const { inputs, usernameTemplate } = data; const { inputs, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs);
@@ -224,7 +225,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
}); });
} }
} else { } else {
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(); const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif }); const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
@@ -20,6 +20,11 @@ export enum SqlProviders {
Vertica = "vertica" Vertica = "vertica"
} }
export enum AwsIamAuthType {
AssumeRole = "assume-role",
AccessKey = "access-key"
}
export enum ElasticSearchAuthTypes { export enum ElasticSearchAuthTypes {
User = "user", User = "user",
ApiKey = "api-key" ApiKey = "api-key"
@@ -168,16 +173,38 @@ export const DynamicSecretSapAseSchema = z.object({
revocationStatement: z.string().trim() revocationStatement: z.string().trim()
}); });
export const DynamicSecretAwsIamSchema = z.object({ export const DynamicSecretAwsIamSchema = z.preprocess(
accessKey: z.string().trim().min(1), (val) => {
secretAccessKey: z.string().trim().min(1), if (typeof val === "object" && val !== null && !Object.hasOwn(val, "method")) {
region: z.string().trim().min(1), // eslint-disable-next-line no-param-reassign
awsPath: z.string().trim().optional(), (val as { method: string }).method = AwsIamAuthType.AccessKey;
permissionBoundaryPolicyArn: z.string().trim().optional(), }
policyDocument: z.string().trim().optional(), return val;
userGroups: z.string().trim().optional(), },
policyArns: z.string().trim().optional() 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({ export const DynamicSecretMongoAtlasSchema = z.object({
adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"), adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"),
@@ -400,9 +427,18 @@ export type TDynamicProviderFns = {
inputs: unknown; inputs: unknown;
expireAt: number; expireAt: number;
usernameTemplate?: string | null; usernameTemplate?: string | null;
identity?: {
name: string;
};
metadata: { projectId: string };
}) => Promise<{ entityId: string; data: unknown }>; }) => Promise<{ entityId: string; data: unknown }>;
validateConnection: (inputs: unknown) => Promise<boolean>; validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;
validateProviderInputs: (inputs: object) => Promise<unknown>; validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise<unknown>;
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>; revoke: (inputs: unknown, entityId: string, metadata: { projectId: string }) => Promise<{ entityId: string }>;
renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>; renew: (
inputs: unknown,
entityId: string,
expireAt: number,
metadata: { projectId: string }
) => Promise<{ entityId: string }>;
}; };
@@ -1,5 +1,4 @@
import axios, { AxiosError } from "axios"; import axios, { AxiosError } from "axios";
import handlebars from "handlebars";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -7,19 +6,20 @@ import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models"; import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const generatePassword = (size = 48) => { const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 48)(size); return customAlphabet(charset, 48)(size);
}; };
const generateUsername = (usernameTemplate?: string | null) => { const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
const randomUsername = alphaNumericNanoId(32); const randomUsername = alphaNumericNanoId(32);
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -64,12 +64,17 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
return isConnected; return isConnected;
}; };
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { const create = async (data: {
const { inputs, expireAt, usernameTemplate } = data; inputs: unknown;
expireAt: number;
usernameTemplate?: string | null;
identity?: { name: string };
}) => {
const { inputs, expireAt, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs);
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(); const password = generatePassword();
const expiration = new Date(expireAt).toISOString(); const expiration = new Date(expireAt).toISOString();
await client({ await client({
@@ -1,4 +1,3 @@
import handlebars from "handlebars";
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -7,19 +6,20 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models"; import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const generatePassword = (size = 48) => { const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 48)(size); return customAlphabet(charset, 48)(size);
}; };
const generateUsername = (usernameTemplate?: string | null) => { const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
const randomUsername = alphaNumericNanoId(32); const randomUsername = alphaNumericNanoId(32);
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -60,12 +60,12 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
return isConnected; return isConnected;
}; };
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => { const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
const { inputs, usernameTemplate } = data; const { inputs, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs);
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(); const password = generatePassword();
const db = client.db(providerInputs.database); const db = client.db(providerInputs.database);
@@ -1,5 +1,4 @@
import axios, { Axios } from "axios"; import axios, { Axios } from "axios";
import handlebars from "handlebars";
import https from "https"; import https from "https";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -9,19 +8,20 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models"; import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const generatePassword = () => { const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 64)(); 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-" const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -117,12 +117,12 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
return infoResponse; return infoResponse;
}; };
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => { const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
const { inputs, usernameTemplate } = data; const { inputs, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs); const connection = await $getClient(providerInputs);
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(); const password = generatePassword();
await createRabbitMqUser({ await createRabbitMqUser({
@@ -9,19 +9,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars
import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models"; import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const generatePassword = () => { const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 64)(); 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-" const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -121,12 +122,17 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
return pingResponse; return pingResponse;
}; };
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { const create = async (data: {
const { inputs, expireAt, usernameTemplate } = data; inputs: unknown;
expireAt: number;
usernameTemplate?: string | null;
identity?: { name: string };
}) => {
const { inputs, expireAt, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs); const connection = await $getClient(providerInputs);
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(); const password = generatePassword();
const expiration = new Date(expireAt).toISOString(); const expiration = new Date(expireAt).toISOString();
@@ -9,19 +9,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars
import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models"; import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const generatePassword = (size = 48) => { const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return customAlphabet(charset, 48)(size); 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-" const randomUsername = `inf_${alphaNumericNanoId(25)}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -87,11 +88,11 @@ export const SapAseProvider = (): TDynamicProviderFns => {
return true; return true;
}; };
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => { const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
const { inputs, usernameTemplate } = data; const { inputs, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(); const password = generatePassword();
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs);
@@ -15,19 +15,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars
import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models"; import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const generatePassword = (size = 48) => { const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return customAlphabet(charset, 48)(size); 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-" const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -97,11 +98,16 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
return testResult; return testResult;
}; };
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { const create = async (data: {
const { inputs, expireAt, usernameTemplate } = data; inputs: unknown;
expireAt: number;
usernameTemplate?: string | null;
identity?: { name: string };
}) => {
const { inputs, expireAt, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(); const password = generatePassword();
const expiration = new Date(expireAt).toISOString(); const expiration = new Date(expireAt).toISOString();
@@ -8,6 +8,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models"; import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
// destroy client requires callback... // destroy client requires callback...
const noop = () => {}; const noop = () => {};
@@ -17,13 +18,13 @@ const generatePassword = (size = 48) => {
return customAlphabet(charset, 48)(size); 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-" const randomUsername = `infisical_${alphaNumericNanoId(32)}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity
}); });
}; };
@@ -88,13 +89,18 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
return isValidConnection; return isValidConnection;
}; };
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { const create = async (data: {
const { inputs, expireAt, usernameTemplate } = data; inputs: unknown;
expireAt: number;
usernameTemplate?: string | null;
identity?: { name: string };
}) => {
const { inputs, expireAt, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs); const client = await $getClient(providerInputs);
const username = generateUsername(usernameTemplate); const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(); const password = generatePassword();
try { try {
@@ -10,6 +10,7 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars
import { TGatewayServiceFactory } from "../../gateway/gateway-service"; import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models"; import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; 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 = ""; let randomUsername = "";
// For oracle, the client assumes everything is upper case when not using quotes around the password // For oracle, the client assumes everything is upper case when not using quotes around the password
if (provider === SqlProviders.Oracle) { if (provider === SqlProviders.Oracle) {
randomUsername = alphaNumericNanoId(32).toUpperCase(); randomUsername = alphaNumericNanoId(32).toUpperCase();
@@ -114,10 +114,13 @@ const generateUsername = (provider: SqlProviders, usernameTemplate?: string | nu
randomUsername = alphaNumericNanoId(32); randomUsername = alphaNumericNanoId(32);
} }
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
return handlebars.compile(usernameTemplate)({ usernameTemplate,
randomUsername, randomUsername,
unixTimestamp: Math.floor(Date.now() / 100) identity,
options: {
toUpperCase: provider === SqlProviders.Oracle
}
}); });
}; };
@@ -221,11 +224,16 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
return isConnected; return isConnected;
}; };
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => { const create = async (data: {
const { inputs, expireAt, usernameTemplate } = data; inputs: unknown;
expireAt: number;
usernameTemplate?: string | null;
identity?: { name: string };
}) => {
const { inputs, expireAt, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs); 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 password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
@@ -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;
};
@@ -42,6 +42,10 @@ export type TListGroupUsersDTO = {
filter?: EFilterReturnedUsers; filter?: EFilterReturnedUsers;
} & TGenericPermission; } & TGenericPermission;
export type TListProjectGroupUsersDTO = TListGroupUsersDTO & {
projectId: string;
};
export type TAddUserToGroupDTO = { export type TAddUserToGroupDTO = {
id: string; id: string;
username: string; username: string;
+3 -1
View File
@@ -89,6 +89,7 @@ export const GROUPS = {
limit: "The number of users to return.", limit: "The number of users to return.",
username: "The username to search for.", username: "The username to search for.",
search: "The text string that user email or name will be filtered by.", search: "The text string that user email or name will be filtered by.",
projectId: "The ID of the project the group belongs to.",
filterUsers: 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." "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: { GCP: {
scope: "The Google project scope that secrets should be synced to.", 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: { DATABRICKS: {
scope: "The Databricks secret scope that secrets should be synced to." scope: "The Databricks secret scope that secrets should be synced to."
+6
View File
@@ -213,6 +213,12 @@ const envSchema = z
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()), GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"), 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 ----------------------------------------------------------------------------- */ /* App Connections ----------------------------------------------------------------------------- */
@@ -7,13 +7,24 @@ type SanitizationArg = {
allowedExpressions?: (arg: string) => boolean; 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) => { export const validateHandlebarTemplate = (templateName: string, template: string, dto: SanitizationArg) => {
const parsedAst = handlebars.parse(template); const parsedAst = handlebars.parse(template);
parsedAst.body.forEach((el) => { parsedAst.body.forEach((el) => {
if (el.type === "ContentStatement") return; if (el.type === "ContentStatement") return;
if (el.type === "MustacheStatement" && "path" in el) { if (el.type === "MustacheStatement" && "path" in el) {
const { path } = el as { type: "MustacheStatement"; path: { type: "PathExpression"; original: string } }; 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"); logger.error(el, "Template sanitization failed");
throw new BadRequestError({ message: `Template sanitization failed: ${templateName}` }); 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 === "ContentStatement") return true;
if (el.type === "MustacheStatement" && "path" in el) { if (el.type === "MustacheStatement" && "path" in el) {
const { path } = el as { type: "MustacheStatement"; path: { type: "PathExpression"; original: string } }; 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; return false;
}); });
@@ -155,6 +155,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
oidc: token?.identityAuth?.oidc oidc: token?.identityAuth?.oidc
}); });
} }
if (token?.identityAuth?.kubernetes) {
requestContext.set("identityAuthInfo", {
identityId: identity.identityId,
kubernetes: token?.identityAuth?.kubernetes
});
}
break; break;
} }
case AuthMode.SERVICE_TOKEN: { case AuthMode.SERVICE_TOKEN: {
+3 -1
View File
@@ -1516,7 +1516,9 @@ export const registerRoutes = async (
dynamicSecretProviders, dynamicSecretProviders,
folderDAL, folderDAL,
licenseService, licenseService,
kmsService kmsService,
userDAL,
identityDAL
}); });
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
auditLogDAL, auditLogDAL,
@@ -45,4 +45,37 @@ export const registerGcpConnectionRouter = async (server: FastifyZodProvider) =>
return projects; 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;
}
});
}; };
@@ -4,9 +4,11 @@ import {
GroupProjectMembershipsSchema, GroupProjectMembershipsSchema,
GroupsSchema, GroupsSchema,
ProjectMembershipRole, ProjectMembershipRole,
ProjectUserMembershipRolesSchema ProjectUserMembershipRolesSchema,
UsersSchema
} from "@app/db/schemas"; } 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 { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -301,4 +303,61 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
return { groupMembership }; 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 };
}
});
}; };
@@ -11,8 +11,10 @@ import { AppConnection } from "../app-connection-enums";
import { GcpConnectionMethod } from "./gcp-connection-enums"; import { GcpConnectionMethod } from "./gcp-connection-enums";
import { import {
GCPApp, GCPApp,
GCPGetProjectLocationsRes,
GCPGetProjectsRes, GCPGetProjectsRes,
GCPGetServiceRes, GCPGetServiceRes,
GCPLocation,
TGcpConnection, TGcpConnection,
TGcpConnectionConfig TGcpConnectionConfig
} from "./gcp-connection-types"; } from "./gcp-connection-types";
@@ -145,6 +147,45 @@ export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection)
return projects; 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<GCPGetProjectLocationsRes>(
`${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) => { export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => {
// Check if provided service account email suffix matches organization ID. // Check if provided service account email suffix matches organization ID.
// We do this to mitigate confused deputy attacks in multi-tenant instances // We do this to mitigate confused deputy attacks in multi-tenant instances
@@ -1,8 +1,8 @@
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { getGcpSecretManagerProjects } from "./gcp-connection-fns"; import { getGcpSecretManagerProjectLocations, getGcpSecretManagerProjects } from "./gcp-connection-fns";
import { TGcpConnection } from "./gcp-connection-types"; import { TGcpConnection, TGetGCPProjectLocationsDTO } from "./gcp-connection-types";
type TGetAppConnectionFunc = ( type TGetAppConnectionFunc = (
app: AppConnection, 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 { return {
listSecretManagerProjects listSecretManagerProjects,
listSecretManagerProjectLocations
}; };
}; };
@@ -38,6 +38,22 @@ export type GCPGetProjectsRes = {
nextPageToken?: string; 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 = { export type GCPGetServiceRes = {
name: string; name: string;
parent: string; parent: string;
@@ -397,7 +397,7 @@ export const authLoginServiceFactory = ({
// Check if the user actually has access to the specified organization. // Check if the user actually has access to the specified organization.
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); 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); const selectedOrg = await orgDAL.findById(organizationId);
if (!hasOrganizationMembership) { if (!hasOrganizationMembership) {
@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types";
import { import {
constructPermissionErrorMessage, constructPermissionErrorMessage,
validatePrivilegeChangeOperation validatePrivilegeChangeOperation
@@ -42,7 +43,7 @@ type TGroupProjectServiceFactoryDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">; projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectBotDAL: TProjectBotDALFactory; projectBotDAL: TProjectBotDALFactory;
groupDAL: Pick<TGroupDALFactory, "findOne">; groupDAL: Pick<TGroupDALFactory, "findOne" | "findAllGroupPossibleMembers">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
}; };
@@ -471,11 +472,54 @@ export const groupProjectServiceFactory = ({
return groupMembership; 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 { return {
addGroupToProject, addGroupToProject,
updateGroupInProject, updateGroupInProject,
removeGroupFromProject, removeGroupFromProject,
listGroupsInProject, listGroupsInProject,
getGroupInProject getGroupInProject,
listProjectGroupUsers
}; };
}; };
@@ -11,5 +11,9 @@ export type TIdentityAccessTokenJwtPayload = {
oidc?: { oidc?: {
claims: Record<string, string>; claims: Record<string, string>;
}; };
kubernetes?: {
namespace: string;
name: string;
};
}; };
}; };
@@ -416,7 +416,13 @@ export const identityKubernetesAuthServiceFactory = ({
{ {
identityId: identityKubernetesAuth.identityId, identityId: identityKubernetesAuth.identityId,
identityAccessTokenId: identityAccessToken.id, identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
identityAuth: {
kubernetes: {
namespace: targetNamespace,
name: targetName
}
}
} as TIdentityAccessTokenJwtPayload, } as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET, appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
+3 -2
View File
@@ -212,7 +212,7 @@ export const orgDALFactory = (db: TDbClient) => {
// special query // special query
const findAllOrgsByUserId = async ( const findAllOrgsByUserId = async (
userId: string userId: string
): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string })[]> => { ): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string; userStatus: string })[]> => {
try { try {
const org = (await db const org = (await db
.replicaNode()(TableName.OrgMembership) .replicaNode()(TableName.OrgMembership)
@@ -234,6 +234,7 @@ export const orgDALFactory = (db: TDbClient) => {
}) })
.select(selectAllTableCols(TableName.Organization)) .select(selectAllTableCols(TableName.Organization))
.select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole")) .select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole"))
.select(db.ref("status").withSchema(TableName.OrgMembership).as("userStatus"))
.select( .select(
db.raw(` db.raw(`
CASE CASE
@@ -242,7 +243,7 @@ export const orgDALFactory = (db: TDbClient) => {
ELSE '' ELSE ''
END as "orgAuthMethod" END as "orgAuthMethod"
`) `)
)) as (TOrganizations & { orgAuthMethod: string; userRole: string })[]; )) as (TOrganizations & { orgAuthMethod: string; userRole: string; userStatus: string })[];
return org; return org;
} catch (error) { } catch (error) {
+3 -1
View File
@@ -183,7 +183,9 @@ export const orgServiceFactory = ({
* */ * */
const findAllOrganizationOfUser = async (userId: string) => { const findAllOrganizationOfUser = async (userId: string) => {
const orgs = await orgDAL.findAllOrgsByUserId(userId); 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 * Get all workspace members
@@ -1,3 +1,63 @@
export enum GcpSyncScope { 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
} }
@@ -4,6 +4,7 @@ import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp"; import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; 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 { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SecretSyncError } from "../secret-sync-errors"; import { SecretSyncError } from "../secret-sync-errors";
@@ -15,9 +16,17 @@ import {
TGcpSyncWithCredentials TGcpSyncWithCredentials
} from "./gcp-sync-types"; } from "./gcp-sync-types";
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => { const getProjectUrl = (secretSync: TGcpSyncWithCredentials) => {
const { destinationConfig } = secretSync; 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[] = []; let gcpSecrets: GCPSecret[] = [];
const pageSize = 100; const pageSize = 100;
@@ -31,16 +40,13 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
}); });
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const { data: secretsRes } = await request.get<GCPSMListSecretsRes>( const { data: secretsRes } = await request.get<GCPSMListSecretsRes>(`${getProjectUrl(secretSync)}/secrets`, {
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${secretSync.destinationConfig.projectId}/secrets`, params,
{ headers: {
params, Authorization: `Bearer ${accessToken}`,
headers: { "Accept-Encoding": "application/json"
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
} }
); });
if (secretsRes.secrets) { if (secretsRes.secrets) {
gcpSecrets = gcpSecrets.concat(secretsRes.secrets); gcpSecrets = gcpSecrets.concat(secretsRes.secrets);
@@ -61,7 +67,7 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
try { try {
const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>( const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`, `${getProjectUrl(secretSync)}/secrets/${key}/versions/latest:access`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
@@ -113,11 +119,14 @@ export const GcpSyncFns = {
if (!(key in gcpSecrets)) { if (!(key in gcpSecrets)) {
// case: create secret // case: create secret
await request.post( await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`, `${getProjectUrl(secretSync)}/secrets`,
{ {
replication: { replication:
automatic: {} destinationConfig.scope === GcpSyncScope.Global
} ? {
automatic: {}
}
: undefined
}, },
{ {
params: { params: {
@@ -131,7 +140,7 @@ export const GcpSyncFns = {
); );
await request.post( await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`, `${getProjectUrl(secretSync)}/secrets/${key}:addVersion`,
{ {
payload: { payload: {
data: Buffer.from(secretMap[key].value).toString("base64") data: Buffer.from(secretMap[key].value).toString("base64")
@@ -163,15 +172,12 @@ export const GcpSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) continue; if (secretSync.syncOptions.disableSecretDeletion) continue;
// case: delete secret // case: delete secret
await request.delete( await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, {
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`, headers: {
{ Authorization: `Bearer ${accessToken}`,
headers: { "Accept-Encoding": "application/json"
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
} }
); });
} else if (secretMap[key].value !== gcpSecrets[key]) { } else if (secretMap[key].value !== gcpSecrets[key]) {
if (!secretMap[key].value) { if (!secretMap[key].value) {
logger.warn( logger.warn(
@@ -180,7 +186,7 @@ export const GcpSyncFns = {
} }
await request.post( await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`, `${getProjectUrl(secretSync)}/secrets/${key}:addVersion`,
{ {
payload: { payload: {
data: Buffer.from(secretMap[key].value).toString("base64") data: Buffer.from(secretMap[key].value).toString("base64")
@@ -212,21 +218,18 @@ export const GcpSyncFns = {
}, },
removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => { removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
const { destinationConfig, connection } = secretSync; const { connection } = secretSync;
const accessToken = await getGcpConnectionAuthToken(connection); const accessToken = await getGcpConnectionAuthToken(connection);
const gcpSecrets = await getGcpSecrets(accessToken, secretSync); const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
for await (const [key] of Object.entries(gcpSecrets)) { for await (const [key] of Object.entries(gcpSecrets)) {
if (key in secretMap) { if (key in secretMap) {
await request.delete( await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, {
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`, headers: {
{ Authorization: `Bearer ${accessToken}`,
headers: { "Accept-Encoding": "application/json"
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
} }
); });
} }
} }
} }
@@ -10,14 +10,33 @@ import {
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
import { SecretSync } from "../secret-sync-enums"; 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 GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
const GcpSyncDestinationConfigSchema = z.object({ const GcpSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope), z
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId) .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({ export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({
destination: z.literal(SecretSync.GCPSecretManager), destination: z.literal(SecretSync.GCPSecretManager),
+93 -73
View File
@@ -26,9 +26,13 @@ func handleConnection(ctx context.Context, quicConn quic.Connection) {
log.Info().Msgf("New connection from: %s", quicConn.RemoteAddr().String()) log.Info().Msgf("New connection from: %s", quicConn.RemoteAddr().String())
// Use WaitGroup to track all streams // Use WaitGroup to track all streams
var wg sync.WaitGroup var wg sync.WaitGroup
contextWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
for { for {
// Accept the first stream, which we'll use for commands // Accept the first stream, which we'll use for commands
stream, err := quicConn.AcceptStream(ctx) stream, err := quicConn.AcceptStream(contextWithTimeout)
if err != nil { if err != nil {
log.Printf("Failed to accept QUIC stream: %v", err) log.Printf("Failed to accept QUIC stream: %v", err)
break break
@@ -52,7 +56,12 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
// Use buffered reader for better handling of fragmented data // Use buffered reader for better handling of fragmented data
reader := bufio.NewReader(stream) reader := bufio.NewReader(stream)
defer stream.Close() defer func() {
log.Info().Msgf("Closing stream %d", streamID)
if stream != nil {
stream.Close()
}
}()
for { for {
msg, err := reader.ReadBytes('\n') 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 != "" { if verifyParam != "" {
tlsConfig.InsecureSkipVerify = verifyParam == "false" tlsConfig.InsecureSkipVerify = verifyParam == "false"
log.Info().Msgf("TLS verification set to: %s", verifyParam) 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 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{ client := &http.Client{
Transport: transport, Transport: transport,
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
} }
// make the request to the target // Loop to handle multiple HTTP requests on the same stream
resp, err := client.Do(proxyReq) for {
if err != nil { req, err := http.ReadRequest(reader)
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) 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 { func buildHttpInternalServerError(message string) string {
@@ -1,5 +1,5 @@
--- ---
title: "Machine identities" title: "Machine identities"
description: "Learn how to set metadata and leverage authentication attributes for 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 #### 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. For methods like OIDC, these come as claims in the token and can be made available in your policies.
<Tabs> <Tabs>
@@ -50,17 +50,29 @@ For methods like OIDC, these come as claims in the token and can be made availab
``` ```
You might map: You might map:
- **department:** to `user.department` - **department:** to `user.department`
- **role:** to `user.role` - **role:** to `user.role`
Once configured, these attributes become available in your policies using the following format: Once configured, these attributes become available in your policies using the following format:
``` ```
{{ identity.auth.oidc.claims.<permission claim name> }} {{ identity.auth.oidc.claims.<permission claim name> }}
``` ```
<img src="/images/platform/access-controls/abac-policy-oidc-format.png" /> <img src="/images/platform/access-controls/abac-policy-oidc-format.png" />
</Tab>
<Tab title="Kubernetes Login Attributes">
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 }}
```
<img src="/images/platform/access-controls/abac-policy-k8s-format.png" />
</Tab> </Tab>
<Tab title="Other Authentication Method Attributes"> <Tab title="Other Authentication Method Attributes">
At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible. At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible.
@@ -95,12 +95,28 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
</Step> </Step>
<Step title="(Optional) Modify ElastiCache Statements"> <Step title="(Optional) Modify ElastiCache Statements">
![Modify ElastiCache Statements Modal](/images/platform/dynamic-secrets/modify-elasticache-statement.png) ![Modify ElastiCache Statements Modal](/images/platform/dynamic-secrets/modify-elasticache-statement.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{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
```
</ParamField> </ParamField>
<ParamField path="Customize ElastiCache Statement" type="string"> <ParamField path="Customize ElastiCache Statement" type="string">
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. 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.
@@ -50,110 +50,304 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
## Set up Dynamic Secrets with AWS IAM ## Set up Dynamic Secrets with AWS IAM
<Steps> <Tabs>
<Step title="Secret Overview Dashboard"> <Tab title="Assume Role (Recommended)">
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to. Infisical will assume the provided role in your AWS account securely, without the need to share any credentials.
</Step> <Accordion title="Self-Hosted Instance">
<Step title="Click on the 'Add Dynamic Secret' button"> 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.
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select AWS IAM">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required> 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.
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required> The following steps are for instances not deployed on AWS:
Maximum time-to-live for a generated secret <Steps>
</ParamField> <Step title="Create an IAM User">
Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console.
</Step>
<Step title="Create an Inline Policy">
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/*"
}
]
}
```
</Step>
<Step title="Obtain the IAM User Credentials">
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**.
<ParamField path="AWS Access Key" type="string" required> ![Access Key Step 1](/images/integrations/aws/integrations-aws-access-key-1.png)
The managing AWS IAM User Access Key ![Access Key Step 2](/images/integrations/aws/integrations-aws-access-key-2.png)
</ParamField> ![Access Key Step 3](/images/integrations/aws/integrations-aws-access-key-3.png)
</Step>
<Step title="Set Up Connection Keys">
1. Set the access key as **DYNAMIC_SECRET_AWS_ACCESS_KEY_ID**.
2. Set the secret key as **DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY**.
</Step>
</Steps>
</Accordion>
<ParamField path="AWS Secret Key" type="string" required> <Steps>
The managing AWS IAM User Secret Key <Step title="Create the Managing User IAM Role for Infisical">
</ParamField> 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)
<ParamField path="AWS IAM Path" type="string"> 2. Select **AWS Account** as the **Trusted Entity Type**.
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. 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.
</ParamField> 4. (Recommended) <strong>Enable "Require external ID"</strong> 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.
<ParamField path="AWS Region" type="string" required> <Warning type="warning" title="Security Best Practice: Use External ID to Prevent Confused Deputy Attacks">
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**.
</ParamField>
<ParamField path="IAM User Permission Boundary" type="string" required> 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 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.
</ParamField>
<ParamField path="AWS IAM Groups" type="string"> <strong>Always enable "Require external ID" and use your Project ID when setting up the IAM Role.</strong>
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas </Warning>
</ParamField> </Step>
<Step title="Copy the AWS IAM Role ARN">
![Copy IAM Role ARN](/images/integrations/aws/integration-aws-iam-assume-arn.png)
</Step>
<Step title="Secret Overview Dashboard">
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select AWS IAM">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-assume-role.png)
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="AWS Policy ARNs" type="string"> <ParamField path="Default TTL" type="string" required>
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)
</ParamField> </ParamField>
<ParamField path="AWS IAM Policy Document" type="string"> <ParamField path="Max TTL" type="string" required>
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
</ParamField> </ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{unixTimestamp}}`: Current Unix timestamp
</ParamField> - `{{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
</Step> Examples:
<Step title="Click 'Submit'"> ```
After submitting the form, you will see a dynamic secret created in the dashboard. {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
{{unixTimestamp}} // 17490641580
{{identity.name}} // testuser
{{random-5}} // x9k2m
{{truncate identity.name 4}} // test
{{replace identity.name 'user' 'replace'}} // testreplace
```
</ParamField>
<ParamField path="Method" type="string" required>
Select *Assume Role* method.
</ParamField>
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) <ParamField path="Aws Role ARN" type="string" required>
</Step> The ARN of the AWS Role to assume.
<Step title="Generate dynamic secrets"> </ParamField>
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) <ParamField path="AWS IAM Path" type="string">
![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.
</ParamField>
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. <ParamField path="AWS Region" type="string" required>
The AWS data center region.
</ParamField>
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) <ParamField path="IAM User Permission Boundary" type="string" required>
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.
</ParamField>
<Tip> <ParamField path="AWS IAM Groups" type="string">
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
</Tip> </ParamField>
<ParamField path="AWS Policy ARNs" type="string">
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
</ParamField>
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. <ParamField path="AWS IAM Policy Document" type="string">
The AWS IAM inline policy that should be attached to the created users.
Multiple values can be provided by separating them with commas
</ParamField>
![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) <ParamField path="Username Template" type="string" default="{{randomUsername}}">
</Step> Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
</Steps>
Allowed template variables are
- `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp
</ParamField>
</Step>
<Step title="Click 'Submit'">
After submitting the form, you will see a dynamic secret created in the dashboard.
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png)
</Step>
<Step title="Generate dynamic secrets">
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)
<Tip>
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4.
</Tip>
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)
</Step>
</Steps>
</Tab>
<Tab title="Access Key">
Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance.
<Steps>
<Step title="Secret Overview Dashboard">
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select AWS IAM">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-access-key.png)
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret
</ParamField>
<ParamField path="Method" type="string" required>
Select *Access Key* method.
</ParamField>
<ParamField path="AWS Access Key" type="string" required>
The managing AWS IAM User Access Key
</ParamField>
<ParamField path="AWS Secret Key" type="string" required>
The managing AWS IAM User Secret Key
</ParamField>
<ParamField path="AWS IAM Path" type="string">
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
</ParamField>
<ParamField path="AWS Region" type="string" required>
The AWS data center region.
</ParamField>
<ParamField path="IAM User Permission Boundary" type="string" required>
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.
</ParamField>
<ParamField path="AWS IAM Groups" type="string">
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="AWS Policy ARNs" type="string">
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="AWS IAM Policy Document" type="string">
The AWS IAM inline policy that should be attached to the created users.
Multiple values can be provided by separating them with commas
</ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
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
</ParamField>
</Step>
<Step title="Click 'Submit'">
After submitting the form, you will see a dynamic secret created in the dashboard.
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png)
</Step>
<Step title="Generate dynamic secrets">
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)
<Tip>
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4.
</Tip>
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)
</Step>
</Steps>
</Tab>
</Tabs>
## Audit or Revoke Leases ## 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. 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. 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) ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases ## 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. 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) ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning> <Warning>
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
</Warning> </Warning>
@@ -80,11 +80,27 @@ The above configuration allows user creation and granting permissions.
<Step title="(Optional) Modify CQL Statements"> <Step title="(Optional) Modify CQL Statements">
![Modify CQL Statements Modal](../../../images/platform/dynamic-secrets/modify-cql-statements.png) ![Modify CQL Statements Modal](../../../images/platform/dynamic-secrets/modify-cql-statements.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{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
```
</ParamField> </ParamField>
<ParamField path="Customize CQL Statement" type="string"> <ParamField path="Customize CQL Statement" type="string">
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). 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).
@@ -87,13 +87,29 @@ The port that your Elasticsearch instance is running on. _(Example: 9200)_
<ParamField path="CA(SSL)" type="string"> <ParamField path="CA(SSL)" type="string">
A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service. A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service.
</ParamField> </ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{unixTimestamp}}`: Current Unix timestamp
</ParamField> - `{{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
```
</ParamField>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-input-modal-elastic-search.png) ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-input-modal-elastic-search.png)
@@ -123,13 +123,29 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
changetype: delete changetype: delete
``` ```
</ParamField> </ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{unixTimestamp}}`: Current Unix timestamp
</ParamField> - `{{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
```
</ParamField>
</Step> </Step>
<Step title="Click `Submit`"> <Step title="Click `Submit`">
@@ -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) ![Modify Scope Modal](../../../images/platform/dynamic-secrets/advanced-option-atlas.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{unixTimestamp}}`: Current Unix timestamp
</ParamField> - `{{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
```
</ParamField>
<ParamField path="Customize Scope" type="string"> <ParamField path="Customize Scope" type="string">
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. 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.
@@ -66,12 +66,28 @@ Create a user with the required permission in your MongoDB instance. This user w
<ParamField path="CA(SSL)" type="string"> <ParamField path="CA(SSL)" type="string">
A CA may be required if your DB requires it for incoming connections. A CA may be required if your DB requires it for incoming connections.
</ParamField> </ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{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
```
</ParamField> </ParamField>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-mongodb.png) ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-mongodb.png)
@@ -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. 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 ## Set up Dynamic Secrets with MS SQL
<Steps> <Steps>
@@ -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 Name by which you want the secret to be referenced
</ParamField> </ParamField>
<ParamField path="Default TTL" type="string" required> <ParamField path="Default TTL" type="string" required>
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)
</ParamField> </ParamField>
<ParamField path="Max TTL" type="string" required> <ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret Maximum time-to-live for a generated secret
</ParamField> </ParamField>
<ParamField path="Metadata" type="list" required> <ParamField path="Metadata" type="list" required>
List of key/value metadata pairs List of key/value metadata pairs
</ParamField> </ParamField>
<ParamField path="Service" type="string" required> <ParamField path="Service" type="string" required>
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**.
</ParamField> </ParamField>
<ParamField path="Host" type="string" required> <ParamField path="Host" type="string" required>
Database host Database host
</ParamField> </ParamField>
<ParamField path="Port" type="number" required> <ParamField path="Port" type="number" required>
Database port Database port
</ParamField> </ParamField>
<ParamField path="User" type="string" required> <ParamField path="User" type="string" required>
Username that will be used to create dynamic secrets Username that will be used to create dynamic secrets
</ParamField> </ParamField>
<ParamField path="Password" type="string" required> <ParamField path="Password" type="string" required>
Password that will be used to create dynamic secrets Password that will be used to create dynamic secrets
</ParamField> </ParamField>
<ParamField path="Database Name" type="string" required> <ParamField path="Database Name" type="string" required>
Name of the database for which you want to create dynamic secrets Name of the database for which you want to create dynamic secrets
</ParamField> </ParamField>
<ParamField path="CA(SSL)" type="string"> <ParamField path="CA(SSL)" type="string">
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).
</ParamField> </ParamField>
![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)
</Step> </Step>
<Step title="(Optional) Modify SQL Statements"> <Step title="(Optional) Modify SQL Statements">
![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statements-mssql.png) ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statements-mssql.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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
```
</ParamField>
Allowed template variables are
- `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp
</ParamField>
<ParamField path="Customize SQL Statement" type="string">
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).
</ParamField>
</Step> </Step>
<Step title="Click 'Submit'"> <Step title="Click 'Submit'">
After submitting the form, you will see a dynamic secret created in the dashboard. After submitting the form, you will see a dynamic secret created in the dashboard.
<Note> <Note>
If this step fails, you may have to add the CA certificate. If this step fails, you may have to add the CA certificate.
</Note> </Note>
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png)
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png)
</Step> </Step>
<Step title="Generate dynamic secrets"> <Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. 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. 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. 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-generate.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.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)
<Tip> <Tip>
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.
</Tip> </Tip>
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)
</Step> </Step>
</Steps> </Steps>
## Audit or Revoke Leases ## 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. 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. 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) ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases ## 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. 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) ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning> <Warning>
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
</Warning> </Warning>
@@ -69,15 +69,28 @@ Create a user with the required permission in your SQL instance. This user will
</Step> </Step>
<Step title="(Optional) Modify SQL Statements"> <Step title="(Optional) Modify SQL Statements">
![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statement-mysql.png) ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statement-mysql.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{unixTimestamp}}`: Current Unix timestamp
</ParamField> - `{{identity.name}}`: Name of the identity that is generating the secret
<ParamField path="Customize SQL Statement" type="string"> - `{{random N}}`: Random string of N characters
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 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
```
</ParamField> </ParamField>
</Step> </Step>
<Step title="Click `Submit`"> <Step title="Click `Submit`">
@@ -71,15 +71,28 @@ Create a user with the required permission in your SQL instance. This user will
</Step> </Step>
<Step title="(Optional) Modify SQL Statements"> <Step title="(Optional) Modify SQL Statements">
![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statement-oracle.png) ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statement-oracle.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{unixTimestamp}}`: Current Unix timestamp
</ParamField> - `{{identity.name}}`: Name of the identity that is generating the secret
<ParamField path="Customize SQL Statement" type="string"> - `{{random N}}`: Random string of N characters
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 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
```
</ParamField> </ParamField>
</Step> </Step>
<Step title="Click 'Submit'"> <Step title="Click 'Submit'">
@@ -72,12 +72,28 @@ Create a user with the required permission in your SQL instance. This user will
</Step> </Step>
<Step title="(Optional) Modify SQL Statements"> <Step title="(Optional) Modify SQL Statements">
![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statements.png) ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statements.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{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
```
</ParamField> </ParamField>
<ParamField path="Customize SQL Statement" type="string"> <ParamField path="Customize SQL Statement" type="string">
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). 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).
@@ -66,12 +66,28 @@ The port that the RabbitMQ management plugin is listening on. This is `15672` by
</ParamField> </ParamField>
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{unixTimestamp}}`: Current Unix timestamp
</ParamField> - `{{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
```
</ParamField>
<ParamField path="CA(SSL)" type="string"> <ParamField path="CA(SSL)" type="string">
A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service. A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service.
@@ -57,12 +57,28 @@ Create a user with the required permission in your Redis instance. This user wil
</Step> </Step>
<Step title="(Optional) Modify Redis Statements"> <Step title="(Optional) Modify Redis Statements">
![Modify Redis Statements Modal](/images/platform/dynamic-secrets/modify-redis-statement.png) ![Modify Redis Statements Modal](/images/platform/dynamic-secrets/modify-redis-statement.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{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
```
</ParamField> </ParamField>
<ParamField path="Customize Redis Statement" type="string"> <ParamField path="Customize Redis Statement" type="string">
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). 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).
@@ -64,13 +64,29 @@ The Infisical SAP ASE dynamic secret allows you to generate SAP ASE database cre
</Step> </Step>
<Step title="(Optional) Modify SAP SQL Statements"> <Step title="(Optional) Modify SAP SQL Statements">
![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png) ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{unixTimestamp}}`: Current Unix timestamp
</ParamField> - `{{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
```
</ParamField>
<ParamField path="Customize Statement" type="string"> <ParamField path="Customize Statement" type="string">
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs.
@@ -64,12 +64,28 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c
</Step> </Step>
<Step title="(Optional) Modify SAP SQL Statements"> <Step title="(Optional) Modify SAP SQL Statements">
![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sap-hana-sql-statements.png) ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sap-hana-sql-statements.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{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
```
</ParamField> </ParamField>
<ParamField path="Customize Statement" type="string"> <ParamField path="Customize Statement" type="string">
@@ -78,12 +78,28 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede
<Step title="(Optional) Modify SQL Statements"> <Step title="(Optional) Modify SQL Statements">
![Modify SQL Statements Modal](/images/platform/dynamic-secrets/snowflake/dynamic-secret-snowflake-sql-statements.png) ![Modify SQL Statements Modal](/images/platform/dynamic-secrets/snowflake/dynamic-secret-snowflake-sql-statements.png)
<ParamField path="Username Template" type="string" default="{{randomUsername}}"> <ParamField path="Username Template" type="string" default="{{randomUsername}}">
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 Allowed template variables are
- `{{randomUsername}}`: Random username string - `{{randomUsername}}`: Random username string
- `{{unixTimestamp}}`: Current Unix timestamp - `{{unixTimestamp}}`: Current Unix timestamp
</ParamField> - `{{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
```
</ParamField>
<ParamField path="Customize Statement" type="string"> <ParamField path="Customize Statement" type="string">
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL
Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 KiB

After

Width:  |  Height:  |  Size: 793 KiB

@@ -34,6 +34,9 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
- **GCP Connection**: The GCP Connection to authenticate with. - **GCP Connection**: The GCP Connection to authenticate with.
- **Project**: The GCP project to sync 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**. 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) ![Configure Options](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png)
@@ -5,27 +5,56 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; import {
import { useGcpConnectionListProjects } from "@app/hooks/api/appConnections/gcp/queries"; Badge,
import { TGitHubConnectionEnvironment } from "@app/hooks/api/appConnections/github"; 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 { SecretSync } from "@app/hooks/api/secretSyncs";
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
import { TSecretSyncForm } from "../schemas"; import { TSecretSyncForm } from "../schemas";
const formatOptionLabel = ({ displayName, locationId }: TGcpLocation) => (
<div className="flex w-full flex-row items-center gap-1">
<span>{displayName}</span>{" "}
<Badge className="h-5 leading-5" variant="success">
{locationId}
</Badge>
</div>
);
export const GcpSyncFields = () => { export const GcpSyncFields = () => {
const { control, setValue } = useFormContext< const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.GCPSecretManager } TSecretSyncForm & { destination: SecretSync.GCPSecretManager }
>(); >();
const connectionId = useWatch({ name: "connection.id", control }); 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, { const { data: projects, isPending } = useGcpConnectionListProjects(connectionId, {
enabled: Boolean(connectionId) enabled: Boolean(connectionId)
}); });
const { data: locations, isPending: areLocationsPending } = useGcpConnectionListProjectLocations(
{ connectionId, projectId },
{
enabled: Boolean(connectionId) && Boolean(projectId)
}
);
useEffect(() => { useEffect(() => {
setValue("destinationConfig.scope", GcpSyncScope.Global); if (!selectedScope) setValue("destinationConfig.scope", GcpSyncScope.Global);
}, []); }, []);
return ( return (
@@ -33,6 +62,7 @@ export const GcpSyncFields = () => {
<SecretSyncConnectionField <SecretSyncConnectionField
onChange={() => { onChange={() => {
setValue("destinationConfig.projectId", ""); setValue("destinationConfig.projectId", "");
setValue("destinationConfig.locationId", "");
}} }}
/> />
<Controller <Controller
@@ -60,9 +90,10 @@ export const GcpSyncFields = () => {
isLoading={isPending && Boolean(connectionId)} isLoading={isPending && Boolean(connectionId)}
isDisabled={!connectionId} isDisabled={!connectionId}
value={projects?.find((project) => project.id === value) ?? null} value={projects?.find((project) => project.id === value) ?? null}
onChange={(option) => onChange={(option) => {
onChange((option as SingleValue<TGitHubConnectionEnvironment>)?.id ?? null) setValue("destinationConfig.locationId", "");
} onChange((option as SingleValue<TGcpProject>)?.id ?? null);
}}
options={projects} options={projects}
placeholder="Select a GCP project..." placeholder="Select a GCP project..."
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
@@ -71,6 +102,76 @@ export const GcpSyncFields = () => {
</FormControl> </FormControl>
)} )}
/> />
<Controller
name="destinationConfig.scope"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
tooltipText={
<div className="flex flex-col gap-3">
<p>
Specify how Infisical should sync secrets to GCP. The following options are
available:
</p>
<ul className="flex list-disc flex-col gap-3 pl-4">
{Object.values(GCP_SYNC_SCOPES).map(({ name, description }) => {
return (
<li key={name}>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">{name}</span>: {description}
</p>
</li>
);
})}
</ul>
</div>
}
tooltipClassName="max-w-lg"
label="Scope"
>
<Select
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-none"
isDisabled={!projectId}
>
{Object.values(GcpSyncScope).map((scope) => {
return (
<SelectItem className="capitalize" value={scope} key={scope}>
{scope}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
{selectedScope === GcpSyncScope.Region && (
<Controller
name="destinationConfig.locationId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
<FilterableSelect
menuPlacement="top"
isLoading={areLocationsPending && Boolean(projectId)}
isDisabled={!projectId}
value={locations?.find((option) => option.locationId === value) ?? null}
onChange={(option) =>
onChange((option as SingleValue<TGcpLocation>)?.locationId ?? null)
}
options={locations}
placeholder="Select a region..."
getOptionValue={(option) => option.locationId}
formatOptionLabel={formatOptionLabel}
/>
</FormControl>
)}
/>
)}
</> </>
); );
}; };
@@ -3,12 +3,23 @@ import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/secret-syncs"; import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
export const GcpSyncReviewFields = () => { export const GcpSyncReviewFields = () => {
const { watch } = useFormContext< const { watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.GCPSecretManager } TSecretSyncForm & { destination: SecretSync.GCPSecretManager }
>(); >();
const projectId = watch("destinationConfig.projectId"); const destinationConfig = watch("destinationConfig");
return <GenericFieldLabel label="Project ID">{projectId}</GenericFieldLabel>; return (
<>
<GenericFieldLabel label="Project ID">{destinationConfig.projectId}</GenericFieldLabel>
<GenericFieldLabel label="Scope" className="capitalize">
{destinationConfig.scope}
</GenericFieldLabel>
{destinationConfig.scope === GcpSyncScope.Region && (
<GenericFieldLabel label="Region">{destinationConfig.locationId}</GenericFieldLabel>
)}
</>
);
}; };
@@ -7,9 +7,16 @@ import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge( export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({ z.object({
destination: z.literal(SecretSync.GCPSecretManager), destination: z.literal(SecretSync.GCPSecretManager),
destinationConfig: z.object({ destinationConfig: z.discriminatedUnion("scope", [
scope: z.literal(GcpSyncScope.Global), z.object({
projectId: z.string().min(1, "Project ID required") 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")
})
])
}) })
); );
+12
View File
@@ -4,6 +4,7 @@ import {
SecretSyncImportBehavior, SecretSyncImportBehavior,
SecretSyncInitialSyncBehavior SecretSyncInitialSyncBehavior
} from "@app/hooks/api/secretSyncs"; } 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"; import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = { export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
@@ -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." "Infisical will sync secrets as environment level shared values to the specified Humanitec application environment."
} }
}; };
export const GCP_SYNC_SCOPES: Record<GcpSyncScope, { name: string; description: string }> = {
[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."
}
};
@@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries"; import { appConnectionKeys } from "../queries";
import { TGcpProject } from "./types"; import { TGcpLocation, TGcpProject, TListProjectLocations } from "./types";
const gcpConnectionKeys = { const gcpConnectionKeys = {
all: [...appConnectionKeys.all, "gcp"] as const, all: [...appConnectionKeys.all, "gcp"] as const,
listProjects: (connectionId: string) => 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 = ( export const useGcpConnectionListProjects = (
@@ -35,3 +37,29 @@ export const useGcpConnectionListProjects = (
...options ...options
}); });
}; };
export const useGcpConnectionListProjectLocations = (
{ connectionId, projectId }: TListProjectLocations,
options?: Omit<
UseQueryOptions<
TGcpLocation[],
unknown,
TGcpLocation[],
ReturnType<typeof gcpConnectionKeys.listProjectLocations>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: gcpConnectionKeys.listProjectLocations({ connectionId, projectId }),
queryFn: async () => {
const { data } = await apiRequest.get<TGcpLocation[]>(
`/api/v1/app-connections/gcp/${connectionId}/secret-manager-project-locations`,
{ params: { projectId } }
);
return data;
},
...options
});
};
@@ -2,3 +2,13 @@ export type TGcpProject = {
id: string; id: string;
name: string; name: string;
}; };
export type TListProjectLocations = {
connectionId: string;
projectId: string;
};
export type TGcpLocation = {
displayName: string;
locationId: string;
};
+25 -9
View File
@@ -44,6 +44,11 @@ export enum SqlProviders {
MsSQL = "mssql" MsSQL = "mssql"
} }
export enum DynamicSecretAwsIamAuth {
AssumeRole = "assume-role",
AccessKey = "access-key"
}
export type TDynamicSecretProvider = export type TDynamicSecretProvider =
| { | {
type: DynamicSecretProviders.SqlDatabase; type: DynamicSecretProviders.SqlDatabase;
@@ -78,15 +83,26 @@ export type TDynamicSecretProvider =
} }
| { | {
type: DynamicSecretProviders.AwsIam; type: DynamicSecretProviders.AwsIam;
inputs: { inputs:
accessKey: string; | {
secretAccessKey: string; method: DynamicSecretAwsIamAuth.AccessKey;
region: string; accessKey: string;
awsPath?: string; secretAccessKey: string;
policyDocument?: string; region: string;
userGroups?: string; awsPath?: string;
policyArns?: 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; type: DynamicSecretProviders.Redis;
+69 -1
View File
@@ -21,7 +21,27 @@ export const groupKeys = {
limit: number; limit: number;
search: string; search: string;
filter?: EFilterReturnedUsers; 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) => { 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;
}
});
};
@@ -3,15 +3,22 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export enum GcpSyncScope { export enum GcpSyncScope {
Global = "global" Global = "global",
Region = "region"
} }
export type TGcpSync = TRootSecretSync & { export type TGcpSync = TRootSecretSync & {
destination: SecretSync.GCPSecretManager; destination: SecretSync.GCPSecretManager;
destinationConfig: { destinationConfig:
scope: GcpSyncScope.Global; | {
projectId: string; scope: GcpSyncScope.Global;
}; projectId: string;
}
| {
scope: GcpSyncScope.Region;
projectId: string;
locationId: string;
};
connection: { connection: {
app: AppConnection.GCP; app: AppConnection.GCP;
name: string; name: string;
@@ -50,10 +50,13 @@ export const useUpdateGroupWorkspaceRole = () => {
return groupMembership; return groupMembership;
}, },
onSuccess: (_, { projectId }) => { onSuccess: (_, { projectId, groupId }) => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId) queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId)
}); });
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceGroupMembershipDetails(projectId, groupId)
});
} }
}); });
}; };
@@ -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) => { export const useListWorkspaceGroups = (projectId: string) => {
return useQuery({ return useQuery({
queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId), queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId),
@@ -36,6 +36,8 @@ export const workspaceKeys = {
searchWorkspace: (dto: TSearchProjectsDTO) => ["search-projects", dto] as const, searchWorkspace: (dto: TSearchProjectsDTO) => ["search-projects", dto] as const,
getWorkspaceGroupMemberships: (workspaceId: string) => getWorkspaceGroupMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-groups"] as const, [{ workspaceId }, "workspace-groups"] as const,
getWorkspaceGroupMembershipDetails: (workspaceId: string, groupId: string) =>
[{ workspaceId, groupId }, "workspace-group-membership-details"] as const,
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) => getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
[{ projectSlug }, "workspace-cas"] as const, [{ projectSlug }, "workspace-cas"] as const,
specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) => specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) =>
@@ -3,7 +3,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 axios from "axios";
import { addSeconds, formatISO } from "date-fns"; import { addSeconds, formatISO } from "date-fns";
import { jwtDecode } from "jwt-decode"; import { jwtDecode } from "jwt-decode";
@@ -51,6 +51,7 @@ export const SelectOrganizationSection = () => {
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const router = useRouter();
const queryParams = new URLSearchParams(window.location.search); const queryParams = new URLSearchParams(window.location.search);
const orgId = queryParams.get("org_id"); const orgId = queryParams.get("org_id");
const callbackPort = queryParams.get("callback_port"); const callbackPort = queryParams.get("callback_port");
@@ -118,6 +119,8 @@ export const SelectOrganizationSection = () => {
}) })
.finally(() => setIsInitialOrgCheckLoading(false)); .finally(() => setIsInitialOrgCheckLoading(false));
await router.invalidate();
if (isMfaEnabled) { if (isMfaEnabled) {
SecurityClient.setMfaToken(token); SecurityClient.setMfaToken(token);
if (mfaMethod) { if (mfaMethod) {
@@ -7,7 +7,8 @@ import { SelectOrganizationPage } from "./SelectOrgPage";
export const SelectOrganizationPageQueryParams = z.object({ export const SelectOrganizationPageQueryParams = z.object({
org_id: z.string().optional().catch(""), org_id: z.string().optional().catch(""),
callback_port: z.coerce.number().optional().catch(undefined), 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")({ export const Route = createFileRoute("/_restrict-login-signup/login/select-organization")({
@@ -28,8 +28,7 @@ import {
import { MfaMethod } from "@app/hooks/api/auth/types"; import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { isLoggedIn } from "@app/hooks/api/reactQuery";
import { navigateUserToOrg } from "../LoginPage/Login.utils";
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
@@ -71,6 +70,8 @@ export const SignupInvitePage = () => {
const { mutateAsync: selectOrganization } = useSelectOrganization(); 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. // Verifies if the information that the users entered (name, workspace) is there, and if the password matched the criteria.
const signupErrorCheck = async () => { const signupErrorCheck = async () => {
setIsLoading(true); setIsLoading(true);
@@ -242,29 +243,10 @@ export const SignupInvitePage = () => {
if (response?.token) { if (response?.token) {
SecurityClient.setSignupToken(response.token); SecurityClient.setSignupToken(response.token);
setStep(2); setStep(2);
} else if (loggedIn) {
navigate({ to: "/login/select-organization", search: { force: true } });
} else { } else {
const redirectExistingUser = async () => { navigate({ to: "/login" });
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();
} }
} }
} catch (err) { } catch (err) {
@@ -15,7 +15,8 @@ import { setAuthToken } from "@app/hooks/api/reactQuery";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
const QueryParamsSchema = z.object({ 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 = () => { export const AuthConsentWrapper = () => {
@@ -71,7 +72,7 @@ export const AuthConsentWrapper = () => {
export const Route = createFileRoute("/_restrict-login-signup")({ export const Route = createFileRoute("/_restrict-login-signup")({
validateSearch: zodValidator(QueryParamsSchema), validateSearch: zodValidator(QueryParamsSchema),
search: { search: {
middlewares: [stripSearchParams({ callback_port: undefined })] middlewares: [stripSearchParams({ callback_port: undefined, force: undefined })]
}, },
beforeLoad: async ({ context, location, search }) => { beforeLoad: async ({ context, location, search }) => {
if (!context.serverConfig.initialized) { if (!context.serverConfig.initialized) {
@@ -90,6 +91,12 @@ export const Route = createFileRoute("/_restrict-login-signup")({
if (!data) return; if (!data) return;
setAuthToken(data.token); 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 // to do cli login
if (search?.callback_port) { if (search?.callback_port) {
if (location.pathname.endsWith("select-organization") || location.pathname.endsWith("login")) if (location.pathname.endsWith("select-organization") || location.pathname.endsWith("login"))
@@ -152,7 +152,7 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
</Th> </Th>
<Th>Email</Th> <Th>Email</Th>
<Th>Added On</Th> <Th>Added On</Th>
<Th /> <Th className="w-5" />
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions"; 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 { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api"; import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
import { TGroupUser } from "@app/hooks/api/groups/types"; import { TGroupUser } from "@app/hooks/api/groups/types";
@@ -38,30 +47,47 @@ export const GroupMembershipRow = ({
<p>{new Date(joinedGroupAt).toLocaleDateString()}</p> <p>{new Date(joinedGroupAt).toLocaleDateString()}</p>
</Tooltip> </Tooltip>
</Td> </Td>
<Td className="justify-end"> <Td>
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}> <Tooltip className="max-w-sm text-center" content="Options">
{(isAllowed) => { <DropdownMenu>
return ( <DropdownMenuTrigger asChild>
<Tooltip <IconButton
content={ ariaLabel="Options"
isOidcManageGroupMembershipsEnabled colorSchema="secondary"
? "OIDC Group Membership Mapping Enabled. Remove user from this group in your OIDC provider." className="w-6"
: "Remove user from group" variant="plain"
}
> >
<IconButton <FontAwesomeIcon icon={faEllipsisV} />
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled} </IconButton>
ariaLabel="Remove user from group" </DropdownMenuTrigger>
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })} <DropdownMenuContent sideOffset={2} align="end">
variant="plain" <OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
colorSchema="danger" {(isAllowed) => {
> return (
<FontAwesomeIcon icon={faUserMinus} /> <Tooltip
</IconButton> content={
</Tooltip> isOidcManageGroupMembershipsEnabled
); ? "OIDC Group Membership Mapping Enabled. Remove user from this group in your OIDC provider."
}} : undefined
</OrgPermissionCan> }
position="left"
>
<div>
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faUserMinus} />}
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })}
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
>
Remove User From Group
</DropdownMenuItem>
</div>
</Tooltip>
);
}}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td> </Td>
</Tr> </Tr>
); );
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
import { faCheck, faClock, faEdit, faSearch } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faClock, faEdit, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PopperContentProps } from "@radix-ui/react-popper";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { z } from "zod"; import { z } from "zod";
@@ -28,6 +29,7 @@ import { formatProjectRoleName } from "@app/helpers/roles";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useGetProjectRoles, useUpdateGroupWorkspaceRole } from "@app/hooks/api"; import { useGetProjectRoles, useUpdateGroupWorkspaceRole } from "@app/hooks/api";
import { TGroupMembership } from "@app/hooks/api/groups/types"; 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 { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
import { groupBy } from "@app/lib/fn/array"; import { groupBy } from "@app/lib/fn/array";
@@ -196,33 +198,38 @@ type TForm = z.infer<typeof formSchema>;
export type TMemberRolesProp = { export type TMemberRolesProp = {
disableEdit?: boolean; disableEdit?: boolean;
groupId: string; groupId: string;
className?: string;
roles: TGroupMembership["roles"]; roles: TGroupMembership["roles"];
popperContentProps?: PopperContentProps;
}; };
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMemberRolesProp) => { type FormProps = {
projectRoles: Omit<TProjectRole, "permissions">[] | undefined;
roles: TGroupMembership["roles"];
groupId: string;
onClose: VoidFunction;
};
const GroupRolesForm = ({ projectRoles, roles, groupId, onClose }: FormProps) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
const [searchRoles, setSearchRoles] = useState(""); const [searchRoles, setSearchRoles] = useState("");
const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role);
const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole();
const { const {
handleSubmit, handleSubmit,
control, control,
reset,
setValue, setValue,
formState: { isSubmitting, isDirty } formState: { isSubmitting, isDirty }
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema) 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 handleRoleUpdate = async (data: TForm) => {
const selectedRoles = Object.keys(data) const selectedRoles = Object.keys(data)
.filter((el) => Boolean(data[el].isChecked)) .filter((el) => Boolean(data[el].isChecked))
@@ -253,7 +260,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
roles: selectedRoles roles: selectedRoles
}); });
createNotification({ text: "Successfully updated group role", type: "success" }); createNotification({ text: "Successfully updated group role", type: "success" });
handlePopUpToggle("editRole"); onClose();
setSearchRoles(""); setSearchRoles("");
} catch { } catch {
createNotification({ text: "Failed to update group role", type: "error" }); createNotification({ text: "Failed to update group role", type: "error" });
@@ -261,7 +268,120 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
}; };
return ( return (
<div className="flex items-center space-x-2"> <form onSubmit={handleSubmit(handleRoleUpdate)} id="role-update-form">
<div className="thin-scrollbar max-h-80 space-y-4 overflow-y-auto">
{projectRoles
?.filter(
({ name, slug }) =>
name.toLowerCase().includes(searchRoles.toLowerCase()) ||
slug.toLowerCase().includes(searchRoles.toLowerCase())
)
?.map(({ id, name, slug }) => {
const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0];
return (
<div key={id} className="flex items-center space-x-4">
<div className="flex-grow">
<Controller
control={control}
defaultValue={Boolean(userProjectRoleDetails?.id)}
name={`${slug}.isChecked`}
render={({ field }) => (
<Checkbox
id={slug}
isChecked={field.value}
onCheckedChange={(isChecked) => {
field.onChange(isChecked);
setValue(`${slug}.temporaryAccess`, false);
}}
>
{name}
</Checkbox>
)}
/>
</div>
<div>
<Controller
control={control}
name={`${slug}.temporaryAccess`}
defaultValue={
userProjectRoleDetails?.isTemporary
? {
isTemporary: true,
temporaryAccessStartTime:
userProjectRoleDetails.temporaryAccessStartTime as string,
temporaryRange: userProjectRoleDetails.temporaryRange as string,
temporaryAccessEndTime: userProjectRoleDetails.temporaryAccessEndTime
}
: false
}
render={({ field }) => (
<IdentityTemporaryRoleForm
temporaryConfig={
typeof field.value === "boolean"
? { isTemporary: field.value }
: field.value
}
onSetTemporary={(data) => {
setValue(`${slug}.isChecked`, true, { shouldDirty: true });
field.onChange({ isTemporary: true, ...data });
}}
onRemoveTemporary={() => {
setValue(`${slug}.isChecked`, false, { shouldDirty: true });
field.onChange(false);
}}
/>
)}
/>
</div>
</div>
);
})}
</div>
<div className="mt-3 flex items-center space-x-2 border-t border-t-gray-700 pt-3">
<div>
<Input
className="w-full p-1.5 pl-8"
size="xs"
value={searchRoles}
onChange={(el) => setSearchRoles(el.target.value)}
leftIcon={<FontAwesomeIcon icon={faSearch} />}
placeholder="Search roles.."
/>
</div>
<div>
<Button
size="xs"
type="submit"
form="role-update-form"
leftIcon={<FontAwesomeIcon icon={faCheck} />}
isDisabled={!isDirty || isSubmitting}
isLoading={isSubmitting}
>
Save
</Button>
</div>
</div>
</form>
);
};
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 (
<div className={twMerge("flex items-center space-x-1", className)}>
{roles {roles
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => { .map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => {
@@ -325,119 +445,32 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
open={popUp.editRole.isOpen} open={popUp.editRole.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("editRole", isOpen); handlePopUpToggle("editRole", isOpen);
reset();
}} }}
> >
{!disableEdit && ( {!disableEdit && (
<PopoverTrigger> <PopoverTrigger onClick={(e) => e.stopPropagation()}>
<IconButton size="sm" variant="plain" ariaLabel="update"> <IconButton size="sm" variant="plain" ariaLabel="update">
<FontAwesomeIcon icon={faEdit} /> <FontAwesomeIcon icon={faEdit} />
</IconButton> </IconButton>
</PopoverTrigger> </PopoverTrigger>
)} )}
<PopoverContent hideCloseBtn className="pt-4"> <PopoverContent
{...popperContentProps}
onClick={(e) => e.stopPropagation()}
hideCloseBtn
className="pt-4"
>
{isRolesLoading ? ( {isRolesLoading ? (
<div className="flex h-8 w-full items-center justify-center"> <div className="flex h-8 w-full items-center justify-center">
<Spinner /> <Spinner />
</div> </div>
) : ( ) : (
<form onSubmit={handleSubmit(handleRoleUpdate)} id="role-update-form"> <GroupRolesForm
<div className="thin-scrollbar max-h-80 space-y-4 overflow-y-auto"> projectRoles={projectRoles}
{projectRoles groupId={groupId}
?.filter( roles={roles}
({ name, slug }) => onClose={() => handlePopUpToggle("editRole")}
name.toLowerCase().includes(searchRoles.toLowerCase()) || />
slug.toLowerCase().includes(searchRoles.toLowerCase())
)
?.map(({ id, name, slug }) => {
const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0];
return (
<div key={id} className="flex items-center space-x-4">
<div className="flex-grow">
<Controller
control={control}
defaultValue={Boolean(userProjectRoleDetails?.id)}
name={`${slug}.isChecked`}
render={({ field }) => (
<Checkbox
id={slug}
isChecked={field.value}
onCheckedChange={(isChecked) => {
field.onChange(isChecked);
setValue(`${slug}.temporaryAccess`, false);
}}
>
{name}
</Checkbox>
)}
/>
</div>
<div>
<Controller
control={control}
name={`${slug}.temporaryAccess`}
defaultValue={
userProjectRoleDetails?.isTemporary
? {
isTemporary: true,
temporaryAccessStartTime:
userProjectRoleDetails.temporaryAccessStartTime as string,
temporaryRange:
userProjectRoleDetails.temporaryRange as string,
temporaryAccessEndTime:
userProjectRoleDetails.temporaryAccessEndTime
}
: false
}
render={({ field }) => (
<IdentityTemporaryRoleForm
temporaryConfig={
typeof field.value === "boolean"
? { isTemporary: field.value }
: field.value
}
onSetTemporary={(data) => {
setValue(`${slug}.isChecked`, true, { shouldDirty: true });
field.onChange({ isTemporary: true, ...data });
}}
onRemoveTemporary={() => {
setValue(`${slug}.isChecked`, false, { shouldDirty: true });
field.onChange(false);
}}
/>
)}
/>
</div>
</div>
);
})}
</div>
<div className="mt-3 flex items-center space-x-2 border-t border-t-gray-700 pt-3">
<div>
<Input
className="w-full p-1.5 pl-8"
size="xs"
value={searchRoles}
onChange={(el) => setSearchRoles(el.target.value)}
leftIcon={<FontAwesomeIcon icon={faSearch} />}
placeholder="Search roles.."
/>
</div>
<div>
<Button
size="xs"
type="submit"
form="role-update-form"
leftIcon={<FontAwesomeIcon icon={faCheck} />}
isDisabled={!isDirty || isSubmitting}
isLoading={isSubmitting}
>
Save
</Button>
</div>
</div>
</form>
)} )}
</PopoverContent> </PopoverContent>
</Popover> </Popover>
@@ -8,6 +8,7 @@ import {
faUsers faUsers
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { format } from "date-fns"; import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
@@ -55,6 +56,7 @@ enum GroupsOrderBy {
export const GroupTable = ({ handlePopUpOpen }: Props) => { export const GroupTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const navigate = useNavigate();
const { const {
search, search,
@@ -143,7 +145,32 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
.slice(offset, perPage * page) .slice(offset, perPage * page)
.map(({ group: { id, name }, roles, createdAt }) => { .map(({ group: { id, name }, roles, createdAt }) => {
return ( return (
<Tr className="group h-10" key={`st-v3-${id}`}> <Tr
className="group h-10 w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`st-v3-${id}`}
role="button"
tabIndex={0}
onKeyDown={(evt) => {
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
}
})
}
>
<Td>{name}</Td> <Td>{name}</Td>
<Td> <Td>
<ProjectPermissionCan <ProjectPermissionCan
@@ -165,7 +192,8 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100"> <div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content="Remove"> <Tooltip content="Remove">
<IconButton <IconButton
onClick={() => { onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteGroup", { handlePopUpOpen("deleteGroup", {
id, id,
name name
@@ -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 (
<div className="flex w-full items-center justify-center p-24">
<Spinner />
</div>
);
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{groupMembership ? (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={groupMembership.group.name} />
<div className="flex">
<div className="mr-4 w-96">
<GroupDetailsSection groupMembership={groupMembership} />
</div>
<GroupMembersSection groupMembership={groupMembership} />
</div>
</div>
) : (
<EmptyState title="Error: Unable to find the group." className="py-12" />
)}
</div>
);
};
export const GroupDetailsByIDPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "Project Group" })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Groups}
passThrough={false}
renderGuardBanner
>
<Page />
</ProjectPermissionCan>
</>
);
};
@@ -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 (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Group Details</h3>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton ariaLabel="Options" colorSchema="secondary" className="w-6" variant="plain">
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Groups}
>
{(isAllowed) => {
return (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => handlePopUpOpen("deleteGroup")}
isDisabled={!isAllowed}
>
Remove Group From Project
</DropdownMenuItem>
);
}}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Group ID</p>
<div className="group flex items-center gap-2">
<p className="text-sm text-mineshaft-300">{groupMembership.group.id}</p>
<CopyButton
value={groupMembership.group.id}
name="Group ID"
size="xs"
variant="plain"
/>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{groupMembership.group.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Slug</p>
<div className="group flex items-center gap-2">
<p className="text-sm text-mineshaft-300">{groupMembership.group.slug}</p>
<CopyButton value={groupMembership.group.slug} name="Slug" size="xs" variant="plain" />
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Project Role</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Groups}>
{(isAllowed) => (
<GroupRoles
className="mt-1"
popperContentProps={{ side: "right" }}
roles={groupMembership.roles}
groupId={groupMembership.group.id}
disableEdit={!isAllowed}
/>
)}
</ProjectPermissionCan>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Assigned to Project</p>
<p className="text-sm text-mineshaft-300">
{format(groupMembership.createdAt, "M/d/yyyy")}
</p>
</div>
</div>
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure you want to remove the group ${
groupMembership.group.name
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
deleteKey="confirm"
buttonText="Remove"
onDeleteApproved={onRemoveGroupSubmit}
/>
</div>
);
};
@@ -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 (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Group Members</h3>
</div>
<div className="py-4">
<GroupMembersTable groupMembership={groupMembership} />
</div>
</div>
);
};
@@ -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 (
<div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search users..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/3">
<div className="flex items-center">
Name
<IconButton
variant="plain"
className="ml-2"
ariaLabel="sort"
onClick={toggleOrderDirection}
>
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
/>
</IconButton>
</div>
</Th>
<Th>Email</Th>
<Th>Added On</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="group-user-memberships" />}
{!isPending &&
filteredGroupMemberships.slice(offset, perPage * page).map((userGroupMembership) => {
return (
<GroupMembershipRow
key={`user-group-membership-${userGroupMembership.id}`}
user={userGroupMembership}
onAssumePrivileges={(userId) => handlePopUpOpen("assumePrivileges", { userId })}
/>
);
})}
</TBody>
</Table>
{Boolean(filteredGroupMemberships.length) && (
<Pagination
count={filteredGroupMemberships.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
{!isPending && !filteredGroupMemberships?.length && (
<EmptyState
title={
groupMemberships?.users.length
? "No users match this search..."
: "This group does not have any members yet"
}
icon={groupMemberships?.users.length ? faSearch : faFolder}
/>
)}
</TableContainer>
<ConfirmActionModal
isOpen={popUp.assumePrivileges.isOpen}
confirmKey="assume"
title="Do you want to assume privileges of this user?"
subTitle="This will set your privileges to those of the user for the next hour."
onChange={(isOpen) => handlePopUpToggle("assumePrivileges", isOpen)}
onConfirmed={handleAssumePrivileges}
buttonText="Confirm"
/>
</div>
);
};
@@ -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 (
<Tr className="items-center" key={`group-user-${id}`}>
<Td>
<p>{`${firstName ?? "-"} ${lastName ?? ""}`}</p>
</Td>
<Td>
<p>{email}</p>
</Td>
<Td>
<Tooltip content={new Date(joinedGroupAt).toLocaleString()}>
<p>{new Date(joinedGroupAt).toLocaleDateString()}</p>
</Tooltip>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionMemberActions.AssumePrivileges}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => {
return (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faUser} />}
onClick={() => onAssumePrivileges(id)}
isDisabled={!isAllowed}
>
Assume Privileges
</DropdownMenuItem>
);
}}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);
};
@@ -0,0 +1 @@
export { GroupMembersSection } from "./GroupMembersSection";
@@ -0,0 +1 @@
export { GroupDetailsSection } from "./GroupDetailsSection";
@@ -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"
}
]
};
}
});
@@ -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"
}
]
};
}
});
@@ -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"
}
]
};
}
});
@@ -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"
}
]
};
}
});
@@ -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"
}
]
};
}
});
@@ -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: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "groups"
}
]
})
});
@@ -1,5 +1,6 @@
import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud"; import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud";
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
import { import {
GitHubSyncScope, GitHubSyncScope,
GitHubSyncVisibility GitHubSyncVisibility
@@ -47,7 +48,8 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
break; break;
case SecretSync.GCPSecretManager: case SecretSync.GCPSecretManager:
primaryText = destinationConfig.projectId; primaryText = destinationConfig.projectId;
secondaryText = "Global"; secondaryText =
destinationConfig.scope === GcpSyncScope.Global ? "Global" : destinationConfig.locationId;
break; break;
case SecretSync.AzureKeyVault: case SecretSync.AzureKeyVault:
primaryText = destinationConfig.vaultBaseUrl; primaryText = destinationConfig.vaultBaseUrl;
@@ -5,22 +5,46 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; 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 { 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"; import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({ const formSchema = z.object({
provider: z.object({ provider: z.discriminatedUnion("method", [
accessKey: z.string().trim().min(1), z.object({
secretAccessKey: z.string().trim().min(1), method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
region: z.string().trim().min(1), accessKey: z.string().trim().min(1),
awsPath: z.string().trim().optional(), secretAccessKey: z.string().trim().min(1),
permissionBoundaryPolicyArn: z.string().trim().optional(), region: z.string().trim().min(1),
policyDocument: z.string().trim().optional(), awsPath: z.string().trim().optional(),
userGroups: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(),
policyArns: 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) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
@@ -67,16 +91,21 @@ export const AwsIamInputForm = ({
const { const {
control, control,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit,
watch
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
environment: isSingleEnvironmentMode ? environments[0] : undefined, environment: isSingleEnvironmentMode ? environments[0] : undefined,
usernameTemplate: "{{randomUsername}}" usernameTemplate: "{{randomUsername}}",
provider: {
method: DynamicSecretAwsIamAuth.AssumeRole
}
} }
}); });
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
const isAccessKeyMethod = watch("provider.method") === DynamicSecretAwsIamAuth.AccessKey;
const handleCreateDynamicSecret = async ({ const handleCreateDynamicSecret = async ({
name, name,
@@ -127,7 +156,7 @@ export const AwsIamInputForm = ({
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} placeholder="dynamic-postgres" /> <Input {...field} placeholder="dynamic-aws-iam" />
</FormControl> </FormControl>
)} )}
/> />
@@ -170,38 +199,82 @@ export const AwsIamInputForm = ({
Configuration Configuration
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center space-x-2"> <Controller
<Controller name="provider.method"
control={control} control={control}
name="provider.accessKey" render={({ field: { value, onChange }, fieldState: { error } }) => (
defaultValue="" <FormControl
render={({ field, fieldState: { error } }) => ( errorText={error?.message}
<FormControl isError={Boolean(error?.message)}
label="AWS Access Key" label="Method"
className="flex-grow" >
isError={Boolean(error?.message)} <Select
errorText={error?.message} value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
> >
<Input {...field} /> <SelectItem value={DynamicSecretAwsIamAuth.AssumeRole}>
</FormControl> Assume Role (Recommended)
)} </SelectItem>
/> <SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
<Controller </Select>
control={control} </FormControl>
name="provider.secretAccessKey" )}
defaultValue="" />
render={({ field, fieldState: { error } }) => ( {isAccessKeyMethod ? (
<FormControl <div className="flex items-center space-x-2">
label="AWS Secret Key" <Controller
className="flex-grow" control={control}
isError={Boolean(error?.message)} name="provider.accessKey"
errorText={error?.message} defaultValue=""
> render={({ field, fieldState: { error } }) => (
<Input {...field} type="password" /> <FormControl
</FormControl> label="AWS Access Key"
)} className="flex-grow"
/> isError={Boolean(error?.message)}
</div> errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.secretAccessKey"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="AWS Secret Key"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} type="password" />
</FormControl>
)}
/>
</div>
) : (
<div className="flex items-center space-x-2">
<Controller
control={control}
name="provider.roleArn"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Assume Role ARN"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
)}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Controller <Controller
control={control} control={control}
@@ -5,14 +5,15 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, TextArea } from "@app/components/v2"; import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
import { useUpdateDynamicSecret } from "@app/hooks/api"; import { useUpdateDynamicSecret } from "@app/hooks/api";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { DynamicSecretAwsIamAuth, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({ const formSchema = z.object({
inputs: z inputs: z.discriminatedUnion("method", [
.object({ z.object({
method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
accessKey: z.string().trim().min(1), accessKey: z.string().trim().min(1),
secretAccessKey: z.string().trim().min(1), secretAccessKey: z.string().trim().min(1),
region: z.string().trim().min(1), region: z.string().trim().min(1),
@@ -21,8 +22,18 @@ const formSchema = z.object({
policyDocument: z.string().trim().optional(), policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(), userGroups: z.string().trim().optional(),
policyArns: 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()
}) })
.partial(), ]),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
@@ -66,6 +77,7 @@ export const EditDynamicSecretAwsIamForm = ({
}: Props) => { }: Props) => {
const { const {
control, control,
watch,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit
} = useForm<TForm>({ } = useForm<TForm>({
@@ -80,6 +92,7 @@ export const EditDynamicSecretAwsIamForm = ({
} }
} }
}); });
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
const updateDynamicSecret = useUpdateDynamicSecret(); const updateDynamicSecret = useUpdateDynamicSecret();
@@ -173,38 +186,82 @@ export const EditDynamicSecretAwsIamForm = ({
<div> <div>
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div> <div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center space-x-2"> <Controller
<Controller name="inputs.method"
control={control} control={control}
name="inputs.accessKey" render={({ field: { value, onChange }, fieldState: { error } }) => (
defaultValue="" <FormControl
render={({ field, fieldState: { error } }) => ( errorText={error?.message}
<FormControl isError={Boolean(error?.message)}
label="AWS Access Key" label="Method"
className="flex-grow" >
isError={Boolean(error?.message)} <Select
errorText={error?.message} value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
> >
<Input {...field} /> <SelectItem value={DynamicSecretAwsIamAuth.AssumeRole}>
</FormControl> Assume Role (Recommended)
)} </SelectItem>
/> <SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
<Controller </Select>
control={control} </FormControl>
name="inputs.secretAccessKey" )}
defaultValue="" />
render={({ field, fieldState: { error } }) => ( {isAccessKeyMethod ? (
<FormControl <div className="flex items-center space-x-2">
label="AWS Secret Key" <Controller
className="flex-grow" control={control}
isError={Boolean(error?.message)} name="inputs.accessKey"
errorText={error?.message} defaultValue=""
> render={({ field, fieldState: { error } }) => (
<Input {...field} type="password" /> <FormControl
</FormControl> label="AWS Access Key"
)} className="flex-grow"
/> isError={Boolean(error?.message)}
</div> errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.secretAccessKey"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="AWS Secret Key"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} type="password" />
</FormControl>
)}
/>
</div>
) : (
<div className="flex items-center space-x-2">
<Controller
control={control}
name="inputs.roleArn"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Assume Role ARN"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
)}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Controller <Controller
control={control} control={control}

Some files were not shown because too many files have changed in this diff Show More