mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 21:28:59 +00:00
Merge branch 'heads/main' into daniel/gateway-docs
This commit is contained in:
Vendored
+4
@@ -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,7 +173,17 @@ export const DynamicSecretSapAseSchema = z.object({
|
|||||||
revocationStatement: z.string().trim()
|
revocationStatement: z.string().trim()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DynamicSecretAwsIamSchema = z.object({
|
export const DynamicSecretAwsIamSchema = z.preprocess(
|
||||||
|
(val) => {
|
||||||
|
if (typeof val === "object" && val !== null && !Object.hasOwn(val, "method")) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
(val as { method: string }).method = AwsIamAuthType.AccessKey;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AwsIamAuthType.AccessKey),
|
||||||
accessKey: z.string().trim().min(1),
|
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),
|
||||||
@@ -177,7 +192,19 @@ export const DynamicSecretAwsIamSchema = 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(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;
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Accept-Encoding": "application/json"
|
"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:
|
||||||
|
destinationConfig.scope === GcpSyncScope.Global
|
||||||
|
? {
|
||||||
automatic: {}
|
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: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Accept-Encoding": "application/json"
|
"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: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Accept-Encoding": "application/json"
|
"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", [
|
||||||
|
z
|
||||||
|
.object({
|
||||||
scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope),
|
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)
|
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),
|
||||||
|
|||||||
@@ -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,30 +183,39 @@ 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
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop to handle multiple HTTP requests on the same stream
|
||||||
|
for {
|
||||||
req, err := http.ReadRequest(reader)
|
req, err := http.ReadRequest(reader)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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")
|
actionHeader := req.Header.Get("x-infisical-action")
|
||||||
if actionHeader != "" {
|
if actionHeader != "" {
|
||||||
|
|
||||||
if actionHeader == "inject-k8s-sa-auth-token" {
|
if actionHeader == "inject-k8s-sa-auth-token" {
|
||||||
token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
||||||
return fmt.Errorf("failed to read k8s sa auth token: %v", err)
|
continue // Continue to next request instead of returning
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
||||||
log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL)
|
log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Del("x-infisical-action")
|
req.Header.Del("x-infisical-action")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build full target URL
|
||||||
var targetFullURL string
|
var targetFullURL string
|
||||||
if strings.HasPrefix(targetURL, "http://") || strings.HasPrefix(targetURL, "https://") {
|
if strings.HasPrefix(targetURL, "http://") || strings.HasPrefix(targetURL, "https://") {
|
||||||
baseURL := strings.TrimSuffix(targetURL, "/")
|
baseURL := strings.TrimSuffix(targetURL, "/")
|
||||||
@@ -216,25 +233,21 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string,
|
|||||||
|
|
||||||
// create the request to the target
|
// create the request to the target
|
||||||
proxyReq, err := http.NewRequest(req.Method, targetFullURL, req.Body)
|
proxyReq, err := http.NewRequest(req.Method, targetFullURL, req.Body)
|
||||||
proxyReq.Header = req.Header.Clone()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create proxy request: %v", err)
|
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)
|
log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL)
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// make the request to the target
|
|
||||||
resp, err := client.Do(proxyReq)
|
resp, err := client.Do(proxyReq)
|
||||||
if err != nil {
|
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()))))
|
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)
|
continue // Continue to next request
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Write the entire response (status line, headers, body) to the stream
|
// Write the entire response (status line, headers, body) to the stream
|
||||||
// http.Response.Write handles this for "Connection: close" correctly.
|
// http.Response.Write handles this for "Connection: close" correctly.
|
||||||
@@ -243,15 +256,22 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string,
|
|||||||
resp.Header.Del("Connection") // Good practice for proxies
|
resp.Header.Del("Connection") // Good practice for proxies
|
||||||
|
|
||||||
log.Info().Msgf("Writing response to stream: %s", resp.Status)
|
log.Info().Msgf("Writing response to stream: %s", resp.Status)
|
||||||
|
|
||||||
if err := resp.Write(stream); err != nil {
|
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")
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func buildHttpInternalServerError(message string) string {
|
func buildHttpInternalServerError(message string) string {
|
||||||
return fmt.Sprintf("HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\n\r\n{\"message\": \"gateway: %s\"}", message)
|
return fmt.Sprintf("HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\n\r\n{\"message\": \"gateway: %s\"}", message)
|
||||||
|
|||||||
+12
@@ -61,6 +61,18 @@ For methods like OIDC, these come as claims in the token and can be made availab
|
|||||||
```
|
```
|
||||||
|
|
||||||
<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.
|
||||||
|
|||||||
@@ -101,6 +101,22 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
|
|||||||
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,6 +50,189 @@ 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
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Assume Role (Recommended)">
|
||||||
|
Infisical will assume the provided role in your AWS account securely, without the need to share any credentials.
|
||||||
|
<Accordion title="Self-Hosted Instance">
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The following steps are for instances not deployed on AWS:
|
||||||
|
<Steps>
|
||||||
|
<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**.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
</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>
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create the Managing User IAM Role for Infisical">
|
||||||
|
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||||
|
3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
||||||
|
4. (Recommended) <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.
|
||||||
|
|
||||||
|
<Warning type="warning" title="Security Best Practice: Use External ID to Prevent Confused Deputy Attacks">
|
||||||
|
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**.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
<strong>Always enable "Require external ID" and use your Project ID when setting up the IAM Role.</strong>
|
||||||
|
</Warning>
|
||||||
|
</Step>
|
||||||
|
<Step title="Copy the AWS IAM Role ARN">
|
||||||
|

|
||||||
|
</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">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select AWS IAM">
|
||||||
|

|
||||||
|
</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>
|
||||||
|
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="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
|
||||||
|
- `{{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="Method" type="string" required>
|
||||||
|
Select *Assume Role* method.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Aws Role ARN" type="string" required>
|
||||||
|
The ARN of the AWS Role to assume.
|
||||||
|
</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.
|
||||||
|

|
||||||
|
</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.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<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.
|
||||||
|
|
||||||
|

|
||||||
|
</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>
|
<Steps>
|
||||||
<Step title="Secret Overview Dashboard">
|
<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.
|
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
|
||||||
@@ -61,6 +244,7 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
|
|||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Provide the inputs for dynamic secret parameters">
|
<Step title="Provide the inputs for dynamic secret parameters">
|
||||||
|

|
||||||
<ParamField path="Secret Name" type="string" required>
|
<ParamField path="Secret Name" type="string" required>
|
||||||
Name by which you want the secret to be referenced
|
Name by which you want the secret to be referenced
|
||||||
</ParamField>
|
</ParamField>
|
||||||
@@ -73,6 +257,10 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
|
|||||||
Maximum time-to-live for a generated secret
|
Maximum time-to-live for a generated secret
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Method" type="string" required>
|
||||||
|
Select *Access Key* method.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="AWS Access Key" type="string" required>
|
<ParamField path="AWS Access Key" type="string" required>
|
||||||
The managing AWS IAM User Access Key
|
The managing AWS IAM User Access Key
|
||||||
</ParamField>
|
</ParamField>
|
||||||
@@ -102,25 +290,26 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="AWS IAM Policy Document" type="string">
|
<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
|
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>
|
||||||
|
|
||||||
<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>
|
</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.
|
||||||
|
|
||||||

|

|
||||||
</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.
|
||||||
@@ -134,26 +323,31 @@ Allowed template variables are
|
|||||||

|

|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4.
|
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4.
|
||||||
</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.
|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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.
|
||||||

|

|
||||||
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -85,6 +85,22 @@ The above configuration allows user creation and granting permissions.
|
|||||||
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).
|
||||||
|
|||||||
@@ -93,6 +93,22 @@ The port that your Elasticsearch instance is running on. _(Example: 9200)_
|
|||||||
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>
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -129,6 +129,22 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
|
|||||||
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>
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,22 @@ Create a project scoped API Key with the required permission in your Mongo Atlas
|
|||||||
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 Scope" type="string">
|
<ParamField path="Customize Scope" type="string">
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,22 @@ Create a user with the required permission in your MongoDB instance. This user w
|
|||||||
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>
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -78,10 +77,24 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
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">
|
|
||||||
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.
|
||||||
@@ -91,6 +104,7 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
</Note>
|
</Note>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</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.
|
||||||
@@ -112,19 +126,23 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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.
|
||||||

|

|
||||||
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -75,9 +75,22 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
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`">
|
||||||
|
|||||||
@@ -77,9 +77,22 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
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'">
|
||||||
|
|||||||
@@ -78,6 +78,22 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
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).
|
||||||
|
|||||||
@@ -71,6 +71,22 @@ Specifies a template for generating usernames. This field allows customization o
|
|||||||
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="CA(SSL)" type="string">
|
<ParamField path="CA(SSL)" type="string">
|
||||||
|
|||||||
@@ -63,6 +63,22 @@ Create a user with the required permission in your Redis instance. This user wil
|
|||||||
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).
|
||||||
|
|||||||
@@ -70,6 +70,22 @@ The Infisical SAP ASE dynamic secret allows you to generate SAP ASE database cre
|
|||||||
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">
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,22 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c
|
|||||||
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">
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,22 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede
|
|||||||
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">
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 538 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 526 KiB |
BIN
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**.
|
||||||

|

|
||||||
|
|||||||
+108
-7
@@ -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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-2
@@ -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", [
|
||||||
|
z.object({
|
||||||
scope: z.literal(GcpSyncScope.Global),
|
scope: z.literal(GcpSyncScope.Global),
|
||||||
projectId: z.string().min(1, "Project ID required")
|
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")
|
||||||
})
|
})
|
||||||
|
])
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,7 +83,9 @@ export type TDynamicSecretProvider =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: DynamicSecretProviders.AwsIam;
|
type: DynamicSecretProviders.AwsIam;
|
||||||
inputs: {
|
inputs:
|
||||||
|
| {
|
||||||
|
method: DynamicSecretAwsIamAuth.AccessKey;
|
||||||
accessKey: string;
|
accessKey: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
region: string;
|
region: string;
|
||||||
@@ -86,6 +93,15 @@ export type TDynamicSecretProvider =
|
|||||||
policyDocument?: string;
|
policyDocument?: string;
|
||||||
userGroups?: string;
|
userGroups?: string;
|
||||||
policyArns?: string;
|
policyArns?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: DynamicSecretAwsIamAuth.AssumeRole;
|
||||||
|
roleArn: string;
|
||||||
|
region: string;
|
||||||
|
awsPath?: string;
|
||||||
|
policyDocument?: string;
|
||||||
|
userGroups?: string;
|
||||||
|
policyArns?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -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,14 +3,21 @@ 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;
|
scope: GcpSyncScope.Global;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
scope: GcpSyncScope.Region;
|
||||||
|
projectId: string;
|
||||||
|
locationId: string;
|
||||||
};
|
};
|
||||||
connection: {
|
connection: {
|
||||||
app: AppConnection.GCP;
|
app: AppConnection.GCP;
|
||||||
|
|||||||
@@ -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,30 +243,11 @@ 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 () => {
|
|
||||||
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" });
|
navigate({ to: "/login" });
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
await redirectExistingUser();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(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"))
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
|
|||||||
+37
-11
@@ -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,7 +47,20 @@ export const GroupMembershipRow = ({
|
|||||||
<p>{new Date(joinedGroupAt).toLocaleDateString()}</p>
|
<p>{new Date(joinedGroupAt).toLocaleDateString()}</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="justify-end">
|
<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">
|
||||||
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
|
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||||
{(isAllowed) => {
|
{(isAllowed) => {
|
||||||
return (
|
return (
|
||||||
@@ -46,22 +68,26 @@ export const GroupMembershipRow = ({
|
|||||||
content={
|
content={
|
||||||
isOidcManageGroupMembershipsEnabled
|
isOidcManageGroupMembershipsEnabled
|
||||||
? "OIDC Group Membership Mapping Enabled. Remove user from this group in your OIDC provider."
|
? "OIDC Group Membership Mapping Enabled. Remove user from this group in your OIDC provider."
|
||||||
: "Remove user from group"
|
: undefined
|
||||||
}
|
}
|
||||||
|
position="left"
|
||||||
>
|
>
|
||||||
<IconButton
|
<div>
|
||||||
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
|
<DropdownMenuItem
|
||||||
ariaLabel="Remove user from group"
|
icon={<FontAwesomeIcon icon={faUserMinus} />}
|
||||||
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })}
|
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })}
|
||||||
variant="plain"
|
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
|
||||||
colorSchema="danger"
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faUserMinus} />
|
Remove User From Group
|
||||||
</IconButton>
|
</DropdownMenuItem>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Tooltip>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
|
|||||||
+144
-111
@@ -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>
|
||||||
|
|||||||
+30
-2
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+20
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+235
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+76
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
+3
-1
@@ -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;
|
||||||
|
|||||||
+79
-6
@@ -5,13 +5,26 @@ 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", [
|
||||||
|
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,6 +34,17 @@ const formSchema = z.object({
|
|||||||
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()
|
||||||
|
})
|
||||||
|
]),
|
||||||
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,6 +199,31 @@ export const AwsIamInputForm = ({
|
|||||||
Configuration
|
Configuration
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
<Controller
|
||||||
|
name="provider.method"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Method"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
>
|
||||||
|
<SelectItem value={DynamicSecretAwsIamAuth.AssumeRole}>
|
||||||
|
Assume Role (Recommended)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{isAccessKeyMethod ? (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -202,6 +256,25 @@ export const AwsIamInputForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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}
|
||||||
|
|||||||
+62
-5
@@ -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,6 +186,31 @@ 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">
|
||||||
|
<Controller
|
||||||
|
name="inputs.method"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Method"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
>
|
||||||
|
<SelectItem value={DynamicSecretAwsIamAuth.AssumeRole}>
|
||||||
|
Assume Role (Recommended)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{isAccessKeyMethod ? (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -205,6 +243,25 @@ export const EditDynamicSecretAwsIamForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
Reference in New Issue
Block a user