Merge pull request #2199 from Infisical/secret-engine-v2-bridge

Secret engine v2 bridge
This commit is contained in:
Maidul Islam
2024-07-30 21:41:21 -04:00
committed by GitHub
192 changed files with 13069 additions and 2766 deletions
@@ -1,30 +1,4 @@
import crypto from "crypto";
import { SecretDataProps, Tag } from "public/data/frequentInterfaces";
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { SecretType } from "@app/hooks/api/types";
import { decryptAssymmetric, encryptSymmetric } from "../cryptography/crypto";
interface EncryptedSecretProps {
id: string;
createdAt: string;
environment: string;
secretName: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: SecretType;
tags: Tag[];
}
import { SecretDataProps } from "public/data/frequentInterfaces";
/**
* Encypt secrets before pushing the to the DB
* @param {object} obj
@@ -34,7 +8,6 @@ interface EncryptedSecretProps {
*/
const encryptSecrets = async ({
secretsToEncrypt,
workspaceId,
env
}: {
secretsToEncrypt: SecretDataProps[];
@@ -43,74 +16,14 @@ const encryptSecrets = async ({
}) => {
let secrets;
try {
// const sharedKey = await getLatestFileKey({ workspaceId });
const wsKey = await fetchUserWsKey(workspaceId);
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
let randomBytes: string;
if (wsKey) {
// case: a (shared) key exists for the workspace
randomBytes = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
} else {
// case: a (shared) key does not exist for the workspace
randomBytes = crypto.randomBytes(16).toString("hex");
}
secrets = secretsToEncrypt.map((secret) => {
// encrypt key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encryptSymmetric({
plaintext: secret.key,
key: randomBytes
});
// encrypt value
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encryptSymmetric({
plaintext: secret.value ?? "",
key: randomBytes
});
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encryptSymmetric({
plaintext: secret.comment ?? "",
key: randomBytes
});
const result: EncryptedSecretProps = {
const result = {
id: secret.id,
createdAt: "",
environment: env,
secretName: secret.key,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
type:
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? SecretType.Shared
: SecretType.Personal,
secretKey: secret.key,
secretValue: secret.value,
secretComment: secret.comment,
tags: secret.tags
};
@@ -14,6 +14,7 @@ export type CheckboxProps = Omit<
isChecked?: boolean;
isRequired?: boolean;
checkIndicatorBg?: string | undefined;
isError?: boolean;
};
export const Checkbox = ({
@@ -24,6 +25,7 @@ export const Checkbox = ({
isDisabled,
isRequired,
checkIndicatorBg,
isError,
...props
}: CheckboxProps): JSX.Element => {
return (
@@ -46,7 +48,10 @@ export const Checkbox = ({
<FontAwesomeIcon icon={faCheck} size="sm" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
<label className="truncate whitespace-nowrap text-sm" htmlFor={id}>
<label
className={twMerge("truncate whitespace-nowrap text-sm", isError && "text-red-400")}
htmlFor={id}
>
{children}
{isRequired && <span className="pl-1 text-red">*</span>}
</label>
@@ -5,7 +5,7 @@ import * as Popover from "@radix-ui/react-popover";
import { useWorkspace } from "@app/context";
import { useDebounce, useToggle } from "@app/hooks";
import { useGetProjectFolders, useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
import { useGetProjectFolders, useGetProjectSecrets } from "@app/hooks/api";
import { SecretInput } from "../SecretInput";
@@ -76,7 +76,6 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
) => {
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const debouncedValue = useDebounce(value, 500);
@@ -121,7 +120,6 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
const { data: secrets } = useGetProjectSecrets({
decryptFileKey: decryptFileKey!,
environment: suggestionSource.environment || "",
secretPath: suggestionSource.secretPath || "",
workspaceId,
@@ -193,8 +191,9 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
);
// mid will be computed value inside the interpolation
const mid = suggestionSource.isDeep
? `${suggestionSource.value.slice(0, -suggestionSource.predicate.length || undefined)}${selectedSuggestion.slug
}`
? `${suggestionSource.value.slice(0, -suggestionSource.predicate.length || undefined)}${
selectedSuggestion.slug
}`
: selectedSuggestion.slug;
// whether we should append . or closing bracket on selecting suggestion
const closingSymbol = getClosingSymbol(
@@ -328,8 +327,9 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
key={`secret-reference-secret-${i + 1}`}
>
<div
className={`${highlightedIndex === i ? "bg-gray-600" : ""
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-2 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
className={`${
highlightedIndex === i ? "bg-gray-600" : ""
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-2 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
>
<div className="flex w-full gap-2">
<div className="flex items-center text-yellow-700">
@@ -19,7 +19,8 @@ export enum OrgPermissionSubjects {
Groups = "groups",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
Identity = "identity",
Kms = "kms"
}
export type OrgPermissionSet =
@@ -35,6 +36,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
@@ -26,7 +26,8 @@ export enum ProjectPermissionSub {
SecretRotation = "secret-rotation",
Identity = "identity",
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates"
Certificates = "certificates",
Kms = "kms"
}
type SubjectFields = {
+23 -87
View File
@@ -1,81 +1,40 @@
import encryptSecrets from "@app/components/utilities/secrets/encryptSecrets";
import { createSecret } from "@app/hooks/api/secrets/mutations";
import { apiRequest } from "@app/config/request";
import { createWorkspace } from "@app/hooks/api/workspace/queries";
const secretsToBeAdded = [
{
pos: 0,
key: "DATABASE_URL",
secretKey: "DATABASE_URL",
// eslint-disable-next-line no-template-curly-in-string
value: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net",
valueOverride: undefined,
comment: "Secret referencing example",
id: "",
tags: []
secretValue: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net",
secretComment: "Secret referencing example"
},
{
pos: 1,
key: "DB_USERNAME",
value: "OVERRIDE_THIS",
valueOverride: undefined,
comment: "Override secrets with personal value",
id: "",
tags: []
secretKey: "DB_USERNAME",
secretValue: "OVERRIDE_THIS",
secretComment: "Override secrets with personal value"
},
{
pos: 2,
key: "DB_PASSWORD",
value: "OVERRIDE_THIS",
valueOverride: undefined,
comment: "Another secret override",
id: "",
tags: []
secretKey: "DB_PASSWORD",
secretValue: "OVERRIDE_THIS",
secretComment: "Another secret override"
},
{
pos: 3,
key: "DB_USERNAME",
value: "user1234",
valueOverride: "user1234",
comment: "",
id: "",
tags: []
secretKey: "DB_PASSWORD",
secretValue: "example_password"
},
{
pos: 4,
key: "DB_PASSWORD",
value: "example_password",
valueOverride: "example_password",
comment: "",
id: "",
tags: []
secretKey: "TWILIO_AUTH_TOKEN",
secretValue: "example_twillio_token"
},
{
pos: 5,
key: "TWILIO_AUTH_TOKEN",
value: "example_twillio_token",
valueOverride: undefined,
comment: "",
id: "",
tags: []
},
{
pos: 6,
key: "WEBSITE_URL",
value: "http://localhost:3000",
valueOverride: undefined,
comment: "",
id: "",
tags: []
secretKey: "WEBSITE_URL",
secretValue: "http://localhost:3000"
}
];
/**
* Create and initialize a new project in organization with id [organizationId]
* Note: current user should be a member of the organization
* @param {Object} obj
* @param {String} obj.organizationId - id of organization
* @param {String} obj.projectName - name of new project
* @returns {Project} project - new project
*/
const initProjectHelper = async ({ projectName }: { projectName: string }) => {
// create new project
@@ -85,37 +44,14 @@ const initProjectHelper = async ({ projectName }: { projectName: string }) => {
projectName
});
// encrypt and upload secrets to new project
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded,
workspaceId: project.id,
env: "dev"
});
try {
await Promise.allSettled(
(secrets || []).map((secret) =>
createSecret({
workspaceId: project.id,
environment: secret.environment,
type: secret.type,
secretKey: secret.secretName,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
secretCommentCiphertext: secret.secretCommentCiphertext,
secretCommentIV: secret.secretCommentIV,
secretCommentTag: secret.secretCommentTag,
secretPath: "/",
metadata: {
source: "signup"
}
})
)
);
const { data } = await apiRequest.post("/api/v3/secrets/batch/raw", {
workspaceId: project.id,
environment: "dev",
secretPath: "/",
secrets: secretsToBeAdded
});
return data;
} catch (err) {
console.error("Failed to upload secrets", err);
}
-175
View File
@@ -1,175 +0,0 @@
import path from "path";
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
import { fetchProjectEncryptedSecrets } from "@app/hooks/api/secrets/queries";
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
export const interpolateSecrets = ({
projectId,
secretEncKey
}: {
projectId: string;
secretEncKey: string;
}) => {
const fetchSecretsCrossEnv = () => {
const fetchCache: Record<string, Record<string, string>> = {};
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
const secRefPathUrl = path.join("/", ...secRefPath);
const uniqKey = `${secRefEnv}-${secRefPathUrl}`;
if (fetchCache?.[uniqKey]) {
return fetchCache[uniqKey][secRefKey];
}
// get secrets by projectId, env, path
const encryptedSecrets = await fetchProjectEncryptedSecrets({
workspaceId: projectId,
environment: secRefEnv,
secretPath: secRefPathUrl
});
const decryptedSec = encryptedSecrets.reduce<Record<string, string>>((prev, secret) => {
const secretKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: secretEncKey
});
const secretValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: secretEncKey
});
// eslint-disable-next-line
prev[secretKey] = secretValue;
return prev;
}, {});
fetchCache[uniqKey] = decryptedSec;
return fetchCache[uniqKey][secRefKey];
};
};
const recursivelyExpandSecret = async (
expandedSec: Record<string, string>,
interpolatedSec: Record<string, string>,
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
key: string
) => {
if (expandedSec?.[key] !== undefined) {
return expandedSec[key];
}
if (recursionChainBreaker?.[key]) {
return "";
}
// eslint-disable-next-line
recursionChainBreaker[key] = true;
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`);
return "";
}
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
if (refs) {
await Promise.all(
refs.map(async (interpolationSyntax) => {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
if (entities.length === 1) {
const val = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
fetchCrossEnv,
recursionChainBreaker,
interpolationKey
);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
return;
}
if (entities.length > 1) {
const secRefEnv = entities[0];
const secRefPath = entities.slice(1, entities.length - 1);
const secRefKey = entities[entities.length - 1];
const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
}
})
);
}
// eslint-disable-next-line
expandedSec[key] = interpolatedValue;
return interpolatedValue;
};
// used to convert multi line ones to quotes ones with \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
};
const expandSecrets = async (
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
const crossSecEnvFetch = fetchSecretsCrossEnv();
Object.keys(secrets).forEach((key) => {
if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) {
interpolatedSec[key] = secrets[key].value;
} else {
expandedSec[key] = secrets[key].value;
}
});
await Promise.all(
Object.keys(secrets).map(async (key) => {
if (expandedSec?.[key]) {
// should not do multi line encoding if user has set it to skip
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedSec[key]
: formatMultiValueEnv(expandedSec[key]);
return;
}
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
// so for any recursion building if there is an entity two times same key meaning it will be looped
const recursionChainBreaker: Record<string, boolean> = {};
const expandedVal = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
crossSecEnvFetch,
recursionChainBreaker,
key
);
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedVal
: formatMultiValueEnv(expandedVal);
})
);
return secrets;
};
return expandSecrets;
};
+1
View File
@@ -16,6 +16,7 @@ export * from "./incidentContacts";
export * from "./integrationAuth";
export * from "./integrations";
export * from "./keys";
export * from "./kms";
export * from "./ldapConfig";
export * from "./oidcConfig";
export * from "./organization";
+8
View File
@@ -0,0 +1,8 @@
export {
useAddExternalKms,
useLoadProjectKmsBackup,
useRemoveExternalKms,
useUpdateExternalKms,
useUpdateProjectKms
} from "./mutations";
export { useGetActiveProjectKms, useGetExternalKmsById, useGetExternalKmsList } from "./queries";
+98
View File
@@ -0,0 +1,98 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { kmsKeys } from "./queries";
import { AddExternalKmsType, KmsType } from "./types";
export const useAddExternalKms = (orgId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ slug, description, provider }: AddExternalKmsType) => {
const { data } = await apiRequest.post("/api/v1/external-kms", {
slug,
description,
provider
});
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId));
}
});
};
export const useUpdateExternalKms = (orgId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
kmsId,
slug,
description,
provider
}: {
kmsId: string;
} & AddExternalKmsType) => {
const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, {
slug,
description,
provider
});
return data;
},
onSuccess: (_, { kmsId }) => {
queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId));
queryClient.invalidateQueries(kmsKeys.getExternalKmsById(kmsId));
}
});
};
export const useRemoveExternalKms = (orgId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (kmsId: string) => {
const { data } = await apiRequest.delete(`/api/v1/external-kms/${kmsId}`);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId));
}
});
};
export const useUpdateProjectKms = (projectId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (
updatedData: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string }
) => {
const { data } = await apiRequest.patch(`/api/v1/workspace/${projectId}/kms`, {
kms: updatedData
});
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(kmsKeys.getActiveProjectKms(projectId));
}
});
};
export const useLoadProjectKmsBackup = (projectId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (backup: string) => {
const { data } = await apiRequest.post(`/api/v1/workspace/${projectId}/kms/backup`, {
backup
});
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(kmsKeys.getActiveProjectKms(projectId));
}
});
};
+63
View File
@@ -0,0 +1,63 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { Kms, KmsListEntry } from "./types";
export const kmsKeys = {
getExternalKmsList: (orgId: string) => ["get-all-external-kms", { orgId }],
getExternalKmsById: (id: string) => ["get-external-kms", { id }],
getActiveProjectKms: (projectId: string) => ["get-active-project-kms", { projectId }]
};
export const useGetExternalKmsList = (orgId: string) => {
return useQuery({
queryKey: kmsKeys.getExternalKmsList(orgId),
queryFn: async () => {
const {
data: { externalKmsList }
} = await apiRequest.get<{ externalKmsList: KmsListEntry[] }>("/api/v1/external-kms");
return externalKmsList;
}
});
};
export const useGetExternalKmsById = (kmsId: string) => {
return useQuery({
queryKey: kmsKeys.getExternalKmsById(kmsId),
enabled: Boolean(kmsId),
queryFn: async () => {
const {
data: { externalKms }
} = await apiRequest.get<{ externalKms: Kms }>(`/api/v1/external-kms/${kmsId}`);
return externalKms;
}
});
};
export const useGetActiveProjectKms = (projectId: string) => {
return useQuery({
queryKey: kmsKeys.getActiveProjectKms(projectId),
enabled: Boolean(projectId),
queryFn: async () => {
const {
data: { secretManagerKmsKey }
} = await apiRequest.get<{
secretManagerKmsKey: {
id: string;
slug: string;
isExternal: string;
};
}>(`/api/v1/workspace/${projectId}/kms`);
return secretManagerKmsKey;
}
});
};
export const fetchProjectKmsBackup = async (projectId: string) => {
const { data } = await apiRequest.get<{
secretManager: string;
}>(`/api/v1/workspace/${projectId}/kms/backup`);
return data;
};
+102
View File
@@ -0,0 +1,102 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
export type Kms = {
id: string;
description: string;
orgId: string;
slug: string;
external: {
id: string;
status: string;
statusDetails: string;
provider: string;
providerInput: Record<string, any>;
};
};
export type KmsListEntry = {
id: string;
description: string;
isDisabled: boolean;
createdAt: string;
updatedAt: string;
slug: string;
externalKms: {
provider: string;
status: string;
statusDetails: string;
};
};
export enum KmsType {
Internal = "internal",
External = "external"
}
export enum ExternalKmsProvider {
AWS = "aws"
}
export const INTERNAL_KMS_KEY_ID = "internal";
export enum KmsAwsCredentialType {
AssumeRole = "assume-role",
AccessKey = "access-key"
}
export const ExternalKmsAwsSchema = z.object({
credential: z
.discriminatedUnion("type", [
z.object({
type: z.literal(KmsAwsCredentialType.AccessKey),
data: z.object({
accessKey: z.string().trim().min(1).describe("AWS user account access key"),
secretKey: z.string().trim().min(1).describe("AWS user account secret key")
})
}),
z.object({
type: z.literal(KmsAwsCredentialType.AssumeRole),
data: z.object({
assumeRoleArn: z
.string()
.trim()
.min(1)
.describe("AWS user role to be assumed by infisical"),
externalId: z
.string()
.trim()
.min(1)
.optional()
.describe("AWS assume role external id for furthur security in authentication")
})
})
])
.describe("AWS credential information to connect"),
awsRegion: z.string().min(1).trim().describe("AWS region to connect"),
kmsKeyId: z
.string()
.trim()
.optional()
.describe(
"A pre existing AWS KMS key id to be used for encryption. If not provided a kms key will be generated."
)
});
export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(ExternalKmsProvider.AWS), inputs: ExternalKmsAwsSchema })
]);
export const AddExternalKmsSchema = z.object({
slug: z
.string()
.trim()
.min(1)
.refine((v) => slugify(v) === v, {
message: "Alias must be a valid slug"
}),
description: z.string().trim().optional(),
provider: ExternalKmsInputSchema
});
export type AddExternalKmsType = z.infer<typeof AddExternalKmsSchema>;
@@ -1,3 +1,4 @@
/* eslint-disable no-param-reassign */
import {
useInfiniteQuery,
UseInfiniteQueryOptions,
@@ -12,16 +13,13 @@ import {
import { apiRequest } from "@app/config/request";
import { UserWsKeyPair } from "../keys/types";
import { decryptSecrets } from "../secrets/queries";
import { DecryptedSecret } from "../secrets/types";
import { EncryptedSecret, SecretType, SecretV3RawSanitized } from "../secrets/types";
import {
CommitType,
TGetSecretApprovalRequestCount,
TGetSecretApprovalRequestDetails,
TGetSecretApprovalRequestList,
TSecretApprovalRequest,
TSecretApprovalRequestCount,
TSecretApprovalSecChangeData
TSecretApprovalRequestCount
} from "./types";
export const secretApprovalRequestKeys = {
@@ -45,8 +43,8 @@ export const secretApprovalRequestKeys = {
]
};
export const decryptSecretApprovalSecret = (
encSecret: TSecretApprovalSecChangeData,
export const decryptSecrets = (
encryptedSecrets: EncryptedSecret[],
decryptFileKey: UserWsKeyPair
) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
@@ -57,34 +55,64 @@ export const decryptSecretApprovalSecret = (
privateKey: PRIVATE_KEY
});
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
const personalSecrets: Record<string, { id: string; value: string }> = {};
const secrets: SecretV3RawSanitized[] = [];
encryptedSecrets.forEach((encSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const decryptedSecret: SecretV3RawSanitized = {
id: encSecret.id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
reminderRepeatDays: encSecret.secretReminderRepeatDays,
reminderNote: encSecret.secretReminderNote,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version,
skipMultilineEncoding: encSecret.skipMultilineEncoding
};
if (encSecret.type === SecretType.Personal) {
personalSecrets[decryptedSecret.key] = {
id: encSecret.id,
value: secretValue
};
} else {
secrets.push(decryptedSecret);
}
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
secrets.forEach((sec) => {
if (personalSecrets?.[sec.key]) {
sec.idOverride = personalSecrets[sec.key].id;
sec.valueOverride = personalSecrets[sec.key].value;
sec.overrideAction = "modified";
}
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
return {
id: encSecret.id,
version: encSecret.version,
secretKey,
secretValue,
secretComment,
tags: encSecret.tags
};
return secrets;
};
const fetchSecretApprovalRequestList = async ({
@@ -173,7 +201,7 @@ export const useGetSecretApprovalRequestDetails = ({
UseQueryOptions<
TSecretApprovalRequest,
unknown,
TSecretApprovalRequest<DecryptedSecret>,
TSecretApprovalRequest,
ReturnType<typeof secretApprovalRequestKeys.detail>
>,
"queryKey" | "queryFn"
@@ -182,16 +210,6 @@ export const useGetSecretApprovalRequestDetails = ({
useQuery({
queryKey: secretApprovalRequestKeys.detail({ id }),
queryFn: () => fetchSecretApprovalRequestDetails({ id }),
select: (data) => ({
...data,
commits: data.commits.map(({ secretVersion, op, secret, ...newVersion }) => ({
op,
secret,
secretVersion: secretVersion ? decryptSecrets([secretVersion], decryptKey)[0] : undefined,
newVersion:
op !== CommitType.DELETE ? decryptSecretApprovalSecret(newVersion, decryptKey) : undefined
}))
}),
enabled: Boolean(id && decryptKey) && (options?.enabled ?? true)
});
@@ -1,6 +1,6 @@
import { UserWsKeyPair } from "../keys/types";
import { TSecretApprovalPolicy } from "../secretApproval/types";
import { EncryptedSecret } from "../secrets/types";
import { SecretV3Raw } from "../secrets/types";
import { WsTag } from "../tags/types";
export enum ApprovalStatus {
@@ -17,15 +17,9 @@ export enum CommitType {
export type TSecretApprovalSecChangeData = {
id: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentIV: string;
secretCommentTag: string;
secretCommentCiphertext: string;
secretKey: string;
secretValue?: string;
secretComment?: string;
skipMultilineEncoding?: boolean;
algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64";
@@ -37,12 +31,12 @@ export type TSecretApprovalSecChange = {
id: string;
version: number;
secretKey: string;
secretValue: string;
secretComment: string;
secretValue?: string;
secretComment?: string;
tags?: string[];
};
export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
export type TSecretApprovalRequest = {
id: string;
isReplicated?: boolean;
slug: string;
@@ -90,7 +84,7 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
commits: ({
// if there is no secret means it was creation
secret?: { version: number };
secretVersion: J;
secretVersion: SecretV3Raw;
// if there is no new version its for Delete
op: CommitType;
} & TSecretApprovalSecChangeData)[];
+45 -121
View File
@@ -1,10 +1,6 @@
import { useCallback } from "react";
import { useQueries, useQuery, UseQueryOptions } from "@tanstack/react-query";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import {
@@ -75,7 +71,7 @@ const fetchImportedSecrets = async (
directory?: string
) => {
const { data } = await apiRequest.get<{ secrets: TImportedSecrets[] }>(
"/api/v1/secret-imports/secrets",
"/api/v1/secret-imports/secrets/raw",
{
params: {
workspaceId,
@@ -107,7 +103,6 @@ const fetchImportedFolders = async ({
export const useGetImportedSecretsSingleEnv = ({
environment,
decryptFileKey,
path,
projectId,
options = {}
@@ -123,78 +118,40 @@ export const useGetImportedSecretsSingleEnv = ({
>;
}) =>
useQuery({
enabled:
Boolean(projectId) &&
Boolean(environment) &&
Boolean(decryptFileKey) &&
(options?.enabled ?? true),
enabled: Boolean(projectId) && Boolean(environment) && (options?.enabled ?? true),
queryKey: secretImportKeys.getSecretImportSecrets({
environment,
path,
projectId
}),
queryFn: () => fetchImportedSecrets(projectId, environment, path),
select: useCallback(
(data: TImportedSecrets[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
return data.map((el) => ({
environment: el.environment,
secretPath: el.secretPath,
environmentInfo: el.environmentInfo,
folderId: el.folderId,
secrets: el.secrets.map((encSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
return {
id: encSecret.id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version
};
})
}));
},
[decryptFileKey]
)
select: (data: TImportedSecrets[]) => {
return data.map((el) => ({
environment: el.environment,
secretPath: el.secretPath,
environmentInfo: el.environmentInfo,
folderId: el.folderId,
secrets: el.secrets.map((encSecret) => {
return {
id: encSecret.id,
env: encSecret.environment,
key: encSecret.secretKey,
value: encSecret.secretValue,
tags: encSecret.tags,
comment: encSecret.secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version
};
})
}));
}
});
export const useGetImportedSecretsAllEnvs = ({
projectId,
environments,
path = "/",
decryptFileKey
path = "/"
}: TGetSecretImportsAllEnvs) => {
const secretImports = useQueries({
queries: environments.map((env) => ({
@@ -206,60 +163,27 @@ export const useGetImportedSecretsAllEnvs = ({
queryFn: () => fetchImportedSecrets(projectId, env, path).catch(() => []),
enabled: Boolean(projectId) && Boolean(env),
// eslint-disable-next-line react-hooks/rules-of-hooks
select: useCallback(
(data: TImportedSecrets[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
return data.map((el) => ({
environment: el.environment,
secretPath: el.secretPath,
environmentInfo: el.environmentInfo,
folderId: el.folderId,
secrets: el.secrets.map((encSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
return {
id: encSecret.id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version
};
})
}));
},
[decryptFileKey]
)
select: (data: TImportedSecrets[]) => {
return data.map((el) => ({
environment: el.environment,
secretPath: el.secretPath,
environmentInfo: el.environmentInfo,
folderId: el.folderId,
secrets: el.secrets.map((encSecret) => {
return {
id: encSecret.id,
env: encSecret.environment,
key: encSecret.secretKey,
value: encSecret.secretValue,
tags: encSecret.tags,
comment: encSecret.secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version
};
})
}));
}
}))
});
@@ -1,5 +1,4 @@
import { UserWsKeyPair } from "../keys/types";
import { EncryptedSecret } from "../secrets/types";
import { SecretV3Raw } from "../secrets/types";
import { WorkspaceEnv } from "../workspace/types";
export type TSecretImport = {
@@ -28,7 +27,7 @@ export type TImportedSecrets = {
environmentInfo: WorkspaceEnv;
secretPath: string;
folderId: string;
secrets: EncryptedSecret[];
secrets: SecretV3Raw[];
};
export type TGetSecretImports = {
@@ -39,7 +38,6 @@ export type TGetSecretImports = {
export type TGetSecretImportsAllEnvs = {
projectId: string;
decryptFileKey: UserWsKeyPair;
path?: string;
environments: string[];
};
@@ -48,7 +46,6 @@ export type TGetImportedSecrets = {
projectId: string;
environment: string;
path?: string;
decryptFileKey: UserWsKeyPair;
};
export type TuseGetImportedFoldersByEnv = {
@@ -1,14 +1,9 @@
import { useCallback } from "react";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import {
TGetSecretRotationList,
TGetSecretRotationListDTO,
TGetSecretRotationProviders,
TSecretRotation,
TSecretRotationProviderList
@@ -19,7 +14,7 @@ export const secretRotationKeys = {
{ workspaceId },
"secret-rotation-providers"
],
list: ({ workspaceId }: Omit<TGetSecretRotationList, "decryptFileKey">) =>
list: ({ workspaceId }: Omit<TGetSecretRotationListDTO, "decryptFileKey">) =>
[{ workspaceId }, "secret-rotations"] as const
};
@@ -53,7 +48,7 @@ export const useGetSecretRotationProviders = ({
const fetchSecretRotations = async ({
workspaceId
}: Omit<TGetSecretRotationList, "decryptFileKey">) => {
}: Omit<TGetSecretRotationListDTO, "decryptFileKey">) => {
const { data } = await apiRequest.get<{ secretRotations: TSecretRotation[] }>(
"/api/v1/secret-rotations",
{ params: { workspaceId } }
@@ -63,14 +58,13 @@ const fetchSecretRotations = async ({
export const useGetSecretRotations = ({
workspaceId,
decryptFileKey,
options = {}
}: TGetSecretRotationList & {
}: TGetSecretRotationListDTO & {
options?: Omit<
UseQueryOptions<
TSecretRotation[],
unknown,
TSecretRotation<{ key: string }>[],
TSecretRotation[],
ReturnType<typeof secretRotationKeys.list>
>,
"queryKey" | "queryFn"
@@ -80,31 +74,5 @@ export const useGetSecretRotations = ({
...options,
queryKey: secretRotationKeys.list({ workspaceId }),
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
queryFn: async () => fetchSecretRotations({ workspaceId }),
select: useCallback(
(data: TSecretRotation[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const decryptKey = decryptAssymmetric({
ciphertext: decryptFileKey.encryptedKey,
nonce: decryptFileKey.nonce,
publicKey: decryptFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
return data.map((el) => ({
...el,
outputs: el.outputs.map(({ key, secret }) => ({
key,
secret: {
key: decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: decryptKey
})
}
}))
}));
},
[decryptFileKey]
)
queryFn: async () => fetchSecretRotations({ workspaceId })
});
@@ -1,5 +1,3 @@
import { UserWsKeyPair } from "../keys/types";
import { EncryptedSecret } from "../secrets/types";
import { WorkspaceEnv } from "../workspace/types";
export enum TProviderFunctionTypes {
@@ -74,7 +72,7 @@ export type TDbProviderTemplate = {
outputs: Record<string, unknown>;
};
export type TSecretRotation<T extends unknown = EncryptedSecret> = {
export type TSecretRotation = {
id: string;
interval: number;
provider: string;
@@ -85,7 +83,11 @@ export type TSecretRotation<T extends unknown = EncryptedSecret> = {
secretPath: string;
outputs: Array<{
key: string;
secret: T;
secret: {
version: number;
id: string;
secretKey: string;
};
}>;
status?: "success" | "failed";
lastRotatedAt?: string;
@@ -103,9 +105,8 @@ export type TGetSecretRotationProviders = {
workspaceId: string;
};
export type TGetSecretRotationList = {
export type TGetSecretRotationListDTO = {
workspaceId: string;
decryptFileKey: UserWsKeyPair;
};
export type TCreateSecretRotationDTO = {
@@ -1,13 +1,9 @@
/* eslint-disable no-param-reassign */
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { DecryptedSecret, SecretType } from "../secrets/types";
import { SecretType, SecretV3RawSanitized } from "../secrets/types";
import {
TGetSecretSnapshotsDTO,
TSecretRollbackDTO,
@@ -65,55 +61,33 @@ const fetchSnapshotEncSecrets = async (snapshotId: string) => {
return res.data.secretSnapshot;
};
export const useGetSnapshotSecrets = ({ decryptFileKey, snapshotId }: TSnapshotDataProps) =>
export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) =>
useQuery({
queryKey: secretSnapshotKeys.snapshotData(snapshotId),
enabled: Boolean(snapshotId && decryptFileKey),
enabled: Boolean(snapshotId),
queryFn: () => fetchSnapshotEncSecrets(snapshotId),
select: (data) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const sharedSecrets: DecryptedSecret[] = [];
const sharedSecrets: SecretV3RawSanitized[] = [];
const personalSecrets: Record<string, { id: string; value: string }> = {};
data.secretVersions.forEach((encSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = "";
data.secretVersions.forEach((secretVersion) => {
const decryptedSecret = {
id: encSecret.secretId,
id: secretVersion.secretId,
env: data.environment.slug,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
key: secretVersion.secretKey,
value: secretVersion.secretValue || "",
tags: secretVersion.tags,
comment: secretVersion.secretComment,
createdAt: secretVersion.createdAt,
updatedAt: secretVersion.updatedAt,
type: "modified",
version: encSecret.version
version: secretVersion.version
};
if (encSecret.type === SecretType.Personal) {
personalSecrets[decryptedSecret.key] = { id: encSecret.secretId, value: secretValue };
if (secretVersion.type === SecretType.Personal) {
personalSecrets[decryptedSecret.key] = {
id: secretVersion.secretId,
value: secretVersion.secretValue || ""
};
} else {
sharedSecrets.push(decryptedSecret);
}
@@ -1,5 +1,4 @@
import { UserWsKeyPair } from "../keys/types";
import { EncryptedSecretVersion } from "../secrets/types";
import { SecretVersions } from "../secrets/types";
import { WorkspaceEnv } from "../types";
export type TSecretSnapshot = {
@@ -12,7 +11,7 @@ export type TSecretSnapshot = {
export type TSnapshotData = Omit<TSecretSnapshot, "secretVersions"> & {
id: string;
secretVersions: EncryptedSecretVersion[];
secretVersions: SecretVersions[];
folderVersion: Array<{ name: string; id: string }>;
environment: WorkspaceEnv;
};
@@ -20,7 +19,6 @@ export type TSnapshotData = Omit<TSecretSnapshot, "secretVersions"> & {
export type TSnapshotDataProps = {
snapshotId: string;
env: string;
decryptFileKey: UserWsKeyPair;
};
export type TGetSecretSnapshotsDTO = {
+41 -162
View File
@@ -1,18 +1,11 @@
import crypto from "crypto";
import { MutationOptions, useMutation, useQueryClient } from "@tanstack/react-query";
import {
decryptAssymmetric,
encryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { secretApprovalRequestKeys } from "../secretApprovalRequest/queries";
import { secretSnapshotKeys } from "../secretSnapshots/queries";
import { secretKeys } from "./queries";
import {
CreateSecretDTO,
TCreateSecretBatchDTO,
TCreateSecretsV3DTO,
TDeleteSecretBatchDTO,
@@ -22,50 +15,6 @@ import {
TUpdateSecretsV3DTO
} from "./types";
const encryptSecret = (randomBytes: string, key: string, value?: string, comment?: string) => {
// encrypt key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encryptSymmetric({
plaintext: key,
key: randomBytes
});
// encrypt value
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encryptSymmetric({
plaintext: value ?? "",
key: randomBytes
});
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encryptSymmetric({
plaintext: comment ?? "",
key: randomBytes
});
return {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
};
};
export const useCreateSecretV3 = ({
options
}: {
@@ -78,32 +27,20 @@ export const useCreateSecretV3 = ({
type,
environment,
workspaceId,
secretName,
secretKey,
secretValue,
latestFileKey,
secretComment,
skipMultilineEncoding
}) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
workspaceId,
environment,
type,
const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, {
secretPath,
...encryptSecret(randomBytes, secretName, secretValue, secretComment),
type,
environment,
workspaceId,
secretValue,
secretComment,
skipMultilineEncoding
};
const { data } = await apiRequest.post(`/api/v3/secrets/${secretName}`, reqBody);
});
return data;
},
onSuccess: (_, { workspaceId, environment, secretPath }) => {
@@ -132,44 +69,30 @@ export const useUpdateSecretV3 = ({
mutationFn: async ({
secretPath = "/",
type,
secretId,
environment,
workspaceId,
secretName,
secretKey,
secretValue,
latestFileKey,
tags,
tagIds,
secretComment,
secretReminderRepeatDays,
secretReminderNote,
newSecretName,
skipMultilineEncoding
}) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
const { data } = await apiRequest.patch(`/api/v3/secrets/raw/${secretKey}`, {
workspaceId,
environment,
type,
secretReminderNote,
secretReminderRepeatDays,
secretPath,
secretId,
...encryptSecret(randomBytes, newSecretName ?? secretName, secretValue, secretComment),
tags,
skipMultilineEncoding,
secretName: newSecretName
};
const { data } = await apiRequest.patch(`/api/v3/secrets/${secretName}`, reqBody);
newSecretName,
secretComment,
tagIds,
secretValue
});
return data;
},
onSuccess: (_, { workspaceId, environment, secretPath }) => {
@@ -201,19 +124,17 @@ export const useDeleteSecretV3 = ({
type,
environment,
workspaceId,
secretName,
secretKey,
secretId
}) => {
const reqBody = {
workspaceId,
environment,
type,
secretPath,
secretId
};
const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, {
data: reqBody
const { data } = await apiRequest.delete(`/api/v3/secrets/raw/${secretKey}`, {
data: {
workspaceId,
environment,
type,
secretPath,
secretId
}
});
return data;
},
@@ -241,33 +162,13 @@ export const useCreateSecretBatch = ({
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretBatchDTO>({
mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets, latestFileKey }) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets }) => {
const { data } = await apiRequest.post("/api/v3/secrets/batch/raw", {
workspaceId,
environment,
secretPath,
secrets: secrets.map(
({ secretName, secretValue, secretComment, metadata, type, skipMultilineEncoding }) => ({
secretName,
...encryptSecret(randomBytes, secretName, secretValue, secretComment),
type,
metadata,
skipMultilineEncoding
})
)
};
const { data } = await apiRequest.post("/api/v3/secrets/batch", reqBody);
secrets
});
return data;
},
onSuccess: (_, { workspaceId, environment, secretPath }) => {
@@ -294,33 +195,13 @@ export const useUpdateSecretBatch = ({
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretBatchDTO>({
mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets, latestFileKey }) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets }) => {
const { data } = await apiRequest.patch("/api/v3/secrets/batch/raw", {
workspaceId,
environment,
secretPath,
secrets: secrets.map(
({ secretName, secretValue, secretComment, type, tags, skipMultilineEncoding }) => ({
secretName,
...encryptSecret(randomBytes, secretName, secretValue, secretComment),
type,
tags,
skipMultilineEncoding
})
)
};
const { data } = await apiRequest.patch("/api/v3/secrets/batch", reqBody);
secrets
});
return data;
},
onSuccess: (_, { workspaceId, environment, secretPath }) => {
@@ -348,15 +229,13 @@ export const useDeleteSecretBatch = ({
return useMutation<{}, {}, TDeleteSecretBatchDTO>({
mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets }) => {
const reqBody = {
workspaceId,
environment,
secretPath,
secrets
};
const { data } = await apiRequest.delete("/api/v3/secrets/batch", {
data: reqBody
const { data } = await apiRequest.delete("/api/v3/secrets/batch/raw", {
data: {
workspaceId,
environment,
secretPath,
secrets
}
});
return data;
},
@@ -443,7 +322,7 @@ export const useMoveSecrets = ({
});
};
export const createSecret = async (dto: CreateSecretDTO) => {
export const createSecret = async (dto: TCreateSecretsV3DTO) => {
const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto);
return data;
};
+55 -111
View File
@@ -2,19 +2,15 @@
import { useCallback, useMemo } from "react";
import { useQueries, useQuery, UseQueryOptions } from "@tanstack/react-query";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { UserWsKeyPair } from "../keys/types";
import {
DecryptedSecret,
EncryptedSecret,
EncryptedSecretVersion,
GetSecretVersionsDTO,
SecretType,
SecretV3Raw,
SecretV3RawResponse,
SecretV3RawSanitized,
SecretVersions,
TGetProjectSecretsAllEnvDTO,
TGetProjectSecretsDTO,
TGetProjectSecretsKey
@@ -27,61 +23,49 @@ export const secretKeys = {
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const
};
export const decryptSecrets = (
encryptedSecrets: EncryptedSecret[],
decryptFileKey: UserWsKeyPair
) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const key = decryptAssymmetric({
ciphertext: decryptFileKey.encryptedKey,
nonce: decryptFileKey.nonce,
publicKey: decryptFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
export const fetchProjectSecrets = async ({
workspaceId,
environment,
secretPath,
includeImports,
expandSecretReferences
}: TGetProjectSecretsKey) => {
const { data } = await apiRequest.get<SecretV3RawResponse>("/api/v3/secrets/raw", {
params: {
environment,
workspaceId,
secretPath,
expandSecretReferences,
include_imports: includeImports
}
});
const personalSecrets: Record<string, { id: string; value: string }> = {};
const secrets: DecryptedSecret[] = [];
encryptedSecrets.forEach((encSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
return data;
};
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const decryptedSecret: DecryptedSecret = {
id: encSecret.id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
reminderRepeatDays: encSecret.secretReminderRepeatDays,
reminderNote: encSecret.secretReminderNote,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version,
skipMultilineEncoding: encSecret.skipMultilineEncoding
export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
const personalSecrets: Record<string, { id: string; value?: string }> = {};
const secrets: SecretV3RawSanitized[] = [];
rawSecrets.forEach((el) => {
const decryptedSecret: SecretV3RawSanitized = {
id: el.id,
env: el.environment,
key: el.secretKey,
value: el.secretValue,
tags: el.tags || [],
comment: el.secretComment || "",
reminderRepeatDays: el.secretReminderRepeatDays,
reminderNote: el.secretReminderNote,
createdAt: el.createdAt,
updatedAt: el.updatedAt,
version: el.version,
skipMultilineEncoding: el.skipMultilineEncoding
};
if (encSecret.type === SecretType.Personal) {
if (el.type === SecretType.Personal) {
personalSecrets[decryptedSecret.key] = {
id: encSecret.id,
value: secretValue
id: el.id,
value: el.secretValue
};
} else {
secrets.push(decryptedSecret);
@@ -99,33 +83,17 @@ export const decryptSecrets = (
return secrets;
};
export const fetchProjectEncryptedSecrets = async ({
workspaceId,
environment,
secretPath
}: TGetProjectSecretsKey) => {
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v3/secrets", {
params: {
environment,
workspaceId,
secretPath
}
});
return data.secrets;
};
export const useGetProjectSecrets = ({
workspaceId,
environment,
decryptFileKey,
secretPath,
options
}: TGetProjectSecretsDTO & {
options?: Omit<
UseQueryOptions<
EncryptedSecret[],
SecretV3RawResponse,
unknown,
DecryptedSecret[],
SecretV3RawSanitized[],
ReturnType<typeof secretKeys.getProjectSecret>
>,
"queryKey" | "queryFn"
@@ -134,25 +102,24 @@ export const useGetProjectSecrets = ({
useQuery({
...options,
// wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && environment) && (options?.enabled ?? true),
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
queryFn: async () => fetchProjectEncryptedSecrets({ workspaceId, environment, secretPath }),
select: (secrets: EncryptedSecret[]) => decryptSecrets(secrets, decryptFileKey)
queryFn: async () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
select: ({ secrets }) => mergePersonalSecrets(secrets)
});
export const useGetProjectSecretsAllEnv = ({
workspaceId,
envs,
decryptFileKey,
secretPath
}: TGetProjectSecretsAllEnvDTO) => {
const secrets = useQueries({
queries: envs.map((environment) => ({
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
enabled: Boolean(decryptFileKey && workspaceId && environment),
queryFn: async () => fetchProjectEncryptedSecrets({ workspaceId, environment, secretPath }),
select: (secs: EncryptedSecret[]) =>
decryptSecrets(secs, decryptFileKey).reduce<Record<string, DecryptedSecret>>(
enabled: Boolean(workspaceId && environment),
queryFn: async () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
select: (el: SecretV3RawResponse) =>
mergePersonalSecrets(el.secrets).reduce<Record<string, SecretV3RawSanitized>>(
(prev, curr) => ({ ...prev, [curr.key]: curr }),
{}
)
@@ -195,7 +162,7 @@ export const useGetProjectSecretsAllEnv = ({
};
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>(
const { data } = await apiRequest.get<{ secretVersions: SecretVersions[] }>(
`/api/v1/secret/${secretId}/secret-versions`,
{
params: {
@@ -209,33 +176,10 @@ const fetchEncryptedSecretVersion = async (secretId: string, offset: number, lim
export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
useQuery({
enabled: Boolean(dto.secretId && dto.decryptFileKey),
enabled: Boolean(dto.secretId),
queryKey: secretKeys.getSecretVersion(dto.secretId),
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
select: useCallback(
(data: EncryptedSecretVersion[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = dto.decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
return data
.map((el) => ({
createdAt: el.createdAt,
id: el.id,
value: decryptSymmetric({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key
})
}))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
},
[dto.decryptFileKey]
)
select: useCallback((data: SecretVersions[]) => {
return data.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}, [])
});
+56 -53
View File
@@ -1,4 +1,3 @@
import type { UserWsKeyPair } from "../keys/types";
import type { WsTag } from "../tags/types";
export enum SecretType {
@@ -30,15 +29,16 @@ export type EncryptedSecret = {
tags: WsTag[];
};
export type DecryptedSecret = {
// both personal and shared secret stitiched together for dashboard
export type SecretV3RawSanitized = {
id: string;
version: number;
key: string;
value: string;
comment: string;
value?: string;
comment?: string;
reminderRepeatDays?: number | null;
reminderNote?: string | null;
tags: WsTag[];
tags?: WsTag[];
createdAt: string;
updatedAt: string;
env: string;
@@ -49,7 +49,36 @@ export type DecryptedSecret = {
skipMultilineEncoding?: boolean;
};
export type EncryptedSecretVersion = {
export type SecretV3Raw = {
id: string;
_id: string;
workspace: string;
environment: string;
version: number;
type: string;
secretKey: string;
secretValue?: string;
secretComment?: string;
secretReminderNote?: string;
secretReminderRepeatDays?: number;
skipMultilineEncoding?: boolean;
metadata?: Record<string, string>;
tags?: WsTag[];
createdAt: string;
updatedAt: string;
};
export type SecretV3RawResponse = {
secrets: SecretV3Raw[];
imports: {
secretPath: string;
environment: string;
folderId: string;
secrets: SecretV3Raw[];
}[];
};
export type SecretVersions = {
id: string;
secretId: string;
version: number;
@@ -57,12 +86,9 @@ export type EncryptedSecretVersion = {
type: SecretType;
isDeleted: boolean;
envId: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretKey: string;
secretValue?: string;
secretComment?: string;
tags: WsTag[];
__v: number;
skipMultilineEncoding?: boolean;
@@ -75,16 +101,15 @@ export type TGetProjectSecretsKey = {
workspaceId: string;
environment: string;
secretPath?: string;
includeImports?: boolean;
expandSecretReferences?: boolean;
};
export type TGetProjectSecretsDTO = {
decryptFileKey: UserWsKeyPair;
} & TGetProjectSecretsKey;
export type TGetProjectSecretsDTO = TGetProjectSecretsKey;
export type TGetProjectSecretsAllEnvDTO = {
workspaceId: string;
envs: string[];
decryptFileKey: UserWsKeyPair;
folderId?: string;
secretPath?: string;
isPaused?: boolean;
@@ -94,12 +119,10 @@ export type GetSecretVersionsDTO = {
secretId: string;
limit: number;
offset: number;
decryptFileKey: UserWsKeyPair;
};
export type TCreateSecretsV3DTO = {
latestFileKey: UserWsKeyPair;
secretName: string;
secretKey: string;
secretValue: string;
secretComment: string;
skipMultilineEncoding?: boolean;
@@ -110,20 +133,18 @@ export type TCreateSecretsV3DTO = {
};
export type TUpdateSecretsV3DTO = {
latestFileKey: UserWsKeyPair;
workspaceId: string;
environment: string;
type: SecretType;
secretPath: string;
type: SecretType;
skipMultilineEncoding?: boolean;
newSecretName?: string;
secretName: string;
secretId?: string;
secretKey: string;
secretValue: string;
secretComment?: string;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
tags?: string[];
tagIds?: string[];
};
export type TDeleteSecretsV3DTO = {
@@ -131,7 +152,7 @@ export type TDeleteSecretsV3DTO = {
environment: string;
type: SecretType;
secretPath: string;
secretName: string;
secretKey: string;
secretId?: string;
};
@@ -139,13 +160,13 @@ export type TCreateSecretBatchDTO = {
workspaceId: string;
environment: string;
secretPath: string;
latestFileKey: UserWsKeyPair;
secrets: Array<{
secretName: string;
secretKey: string;
secretValue: string;
secretComment: string;
skipMultilineEncoding?: boolean;
type: SecretType;
tagIds?: string[];
metadata?: {
source?: string;
};
@@ -156,14 +177,16 @@ export type TUpdateSecretBatchDTO = {
workspaceId: string;
environment: string;
secretPath: string;
latestFileKey: UserWsKeyPair;
secrets: Array<{
type: SecretType;
secretName: string;
skipMultilineEncoding?: boolean;
secretKey: string;
secretValue: string;
secretComment: string;
tags?: string[];
secretComment?: string;
skipMultilineEncoding?: boolean;
tagIds?: string[];
metadata?: {
source?: string;
};
}>;
};
@@ -172,7 +195,7 @@ export type TDeleteSecretBatchDTO = {
environment: string;
secretPath: string;
secrets: Array<{
secretName: string;
secretKey: string;
type: SecretType;
}>;
};
@@ -187,23 +210,3 @@ export type TMoveSecretsDTO = {
secretIds: string[];
shouldOverwrite: boolean;
};
export type CreateSecretDTO = {
workspaceId: string;
environment: string;
type: SecretType;
secretKey: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretPath: string;
metadata?: {
source?: string;
};
};
@@ -40,4 +40,5 @@ export type SubscriptionPlan = {
has_used_trial: boolean;
caCrl: boolean;
instanceUserManagement: boolean;
externalKms: boolean;
};
-1
View File
@@ -66,7 +66,6 @@ export type OrgUser = {
export type TProjectMembership = {
id: string;
role: string;
createdAt: string;
updatedAt: string;
projectId: string;
+2 -2
View File
@@ -2,8 +2,8 @@ export {
useAddGroupToWorkspace,
useDeleteGroupFromWorkspace,
useLeaveProject,
useUpdateGroupWorkspaceRole
} from "./mutations";
useMigrateProjectToV3,
useUpdateGroupWorkspaceRole} from "./mutations";
export {
useAddIdentityToWorkspace,
useCreateWorkspace,
@@ -74,3 +74,15 @@ export const useLeaveProject = () => {
}
});
};
export const useMigrateProjectToV3 = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, { workspaceId: string }>({
mutationFn: ({ workspaceId }) => {
return apiRequest.post(`/api/v1/workspace/${workspaceId}/migrate-v3`);
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
});
};
+12 -6
View File
@@ -150,11 +150,15 @@ export const useGetWorkspaceSecrets = (workspaceId: string) => {
});
};
export const useGetWorkspaceById = (workspaceId: string) => {
export const useGetWorkspaceById = (
workspaceId: string,
dto?: { refetchInterval?: number | false }
) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceById(workspaceId),
queryFn: () => fetchWorkspaceById(workspaceId),
enabled: Boolean(workspaceId)
enabled: Boolean(workspaceId),
refetchInterval: dto?.refetchInterval
});
};
@@ -226,18 +230,20 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
});
export const createWorkspace = ({
projectName
projectName,
kmsKeyId
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
return apiRequest.post("/api/v2/workspace", { projectName });
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId });
};
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ projectName }) =>
mutationFn: async ({ projectName, kmsKeyId }) =>
createWorkspace({
projectName
projectName,
kmsKeyId
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
+3 -1
View File
@@ -1,6 +1,7 @@
export enum ProjectVersion {
V1 = 1,
V2 = 2
V2 = 2,
V3 = 3
}
export enum ProjectUserMembershipTemporaryMode {
@@ -48,6 +49,7 @@ export type TGetUpgradeProjectStatusDTO = {
// mutation dto
export type CreateWorkspaceDTO = {
projectName: string;
kmsKeyId?: string;
};
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
+77 -24
View File
@@ -36,6 +36,10 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Checkbox,
DropdownMenu,
@@ -51,7 +55,6 @@ import {
SelectItem,
UpgradePlanModal
} from "@app/components/v2";
import { UpgradeOverlay } from "@app/components/v2/UpgradeOverlay";
import {
OrgPermissionActions,
OrgPermissionSubjects,
@@ -66,11 +69,13 @@ import {
useAddUserToWsNonE2EE,
useCreateWorkspace,
useGetAccessRequestsCount,
useGetExternalKmsList,
useGetOrgTrialUrl,
useGetSecretApprovalRequestCount,
useLogoutUser,
useSelectOrganization
} from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { Workspace } from "@app/hooks/api/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
@@ -113,7 +118,8 @@ const formSchema = yup.object({
.label("Project Name")
.trim()
.max(64, "Too long, maximum length is 64 characters"),
addMembers: yup.bool().required().label("Add Members")
addMembers: yup.bool().required().label("Add Members"),
kmsKeyId: yup.string().label("KMS Key ID")
});
type TAddProjectFormData = yup.InferType<typeof formSchema>;
@@ -147,6 +153,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
const pendingRequestsCount = useMemo(() => {
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
@@ -172,7 +179,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
reset,
handleSubmit
} = useForm<TAddProjectFormData>({
resolver: yupResolver(formSchema)
resolver: yupResolver(formSchema),
defaultValues: {
kmsKeyId: INTERNAL_KMS_KEY_ID
}
});
const { t } = useTranslation();
@@ -245,7 +255,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
putUserInOrg();
}, [router.query.id]);
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
const onCreateProject = async ({ name, addMembers, kmsKeyId }: TAddProjectFormData) => {
// type check
if (!currentOrg) return;
if (!user) return;
@@ -255,7 +265,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
project: { id: newProjectId }
}
} = await createWs.mutateAsync({
projectName: name
projectName: name,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined
});
if (addMembers) {
@@ -323,7 +334,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
<UpgradeOverlay />
{!router.asPath.includes("personal") && (
<div className="flex h-12 cursor-default items-center px-3 pt-6">
{(router.asPath.includes("project") ||
@@ -889,24 +899,67 @@ export const AppLayout = ({ children }: LayoutProps) => {
)}
/>
</div>
<div className="mt-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="mr-4"
type="submit"
>
Create Project
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose("addNewWs")}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
<div className="mt-14 flex">
<Accordion type="single" collapsible className="w-full">
<AccordionItem
value="advance-settings"
className="data-[state=open]:border-none"
>
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Advanced Settings</div>
</AccordionTrigger>
<AccordionContent>
<Controller
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="KMS"
>
<Select
{...field}
onValueChange={(e) => {
onChange(e);
}}
className="mb-12 w-full bg-mineshaft-600"
>
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
Default Infisical KMS
</SelectItem>
{externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="kmsKeyId"
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose("addNewWs")}
colorSchema="secondary"
variant="plain"
className="py-2"
>
Cancel
</Button>
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="ml-4"
type="submit"
>
Create Project
</Button>
</div>
</div>
</form>
</ModalContent>
+77 -22
View File
@@ -36,6 +36,10 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Checkbox,
FormControl,
@@ -43,6 +47,8 @@ import {
Input,
Modal,
ModalContent,
Select,
SelectItem,
Skeleton,
UpgradePlanModal
} from "@app/components/v2";
@@ -59,8 +65,10 @@ import {
fetchOrgUsers,
useAddUserToWsNonE2EE,
useCreateWorkspace,
useGetExternalKmsList,
useRegisterUserAction
} from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { Workspace } from "@app/hooks/api/types";
@@ -473,7 +481,8 @@ const formSchema = yup.object({
.label("Project Name")
.trim()
.max(64, "Too long, maximum length is 64 characters"),
addMembers: yup.bool().required().label("Add Members")
addMembers: yup.bool().required().label("Add Members"),
kmsKeyId: yup.string().label("KMS Key ID")
});
type TAddProjectFormData = yup.InferType<typeof formSchema>;
@@ -506,7 +515,10 @@ const OrganizationPage = withPermission(
reset,
handleSubmit
} = useForm<TAddProjectFormData>({
resolver: yupResolver(formSchema)
resolver: yupResolver(formSchema),
defaultValues: {
kmsKeyId: INTERNAL_KMS_KEY_ID
}
});
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
@@ -521,7 +533,9 @@ const OrganizationPage = withPermission(
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
);
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
const onCreateProject = async ({ name, addMembers, kmsKeyId }: TAddProjectFormData) => {
// type check
if (!currentOrg) return;
if (!user) return;
@@ -531,7 +545,8 @@ const OrganizationPage = withPermission(
project: { id: newProjectId }
}
} = await createWs.mutateAsync({
projectName: name
projectName: name,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined
});
if (addMembers) {
@@ -1064,24 +1079,64 @@ const OrganizationPage = withPermission(
)}
/>
</div>
<div className="mt-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="mr-4"
type="submit"
>
Create Project
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose("addNewWs")}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
<div className="mt-14 flex">
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Advanced Settings</div>
</AccordionTrigger>
<AccordionContent>
<Controller
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="KMS"
>
<Select
{...field}
onValueChange={(e) => {
onChange(e);
}}
className="mb-12 w-full bg-mineshaft-600"
>
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
Default Infisical KMS
</SelectItem>
{externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="kmsKeyId"
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose("addNewWs")}
colorSchema="secondary"
variant="plain"
className="py-2"
>
Cancel
</Button>
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="ml-4"
type="submit"
>
Create Project
</Button>
</div>
</div>
</form>
</ModalContent>
@@ -1,50 +1,39 @@
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { createNotification } from "@app/components/notifications";
import { Button, Modal, ModalContent } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import {
useDeleteIntegration,
useDeleteIntegrationAuths,
useGetCloudIntegrations,
useGetUserWsKey,
useGetWorkspaceAuthorizations,
useGetWorkspaceBot,
useGetWorkspaceIntegrations,
useUpdateBotActiveStatus
useGetWorkspaceIntegrations
} from "@app/hooks/api";
import { IntegrationAuth } from "@app/hooks/api/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
import { InfrastructureIntegrationSection } from "./components/InfrastructureIntegrationSection/InfrastructureIntegrationSection";
import { IntegrationsSection } from "./components/IntegrationsSection";
import { generateBotKey, redirectForProviderAuth } from "./IntegrationPage.utils";
import { redirectForProviderAuth } from "./IntegrationPage.utils";
type Props = {
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
infrastructureIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
infrastructureIntegrations: Array<{
name: string;
slug: string;
image: string;
docsLink: string;
}>;
};
export const IntegrationsPage = withProjectPermission(
({ frameworkIntegrations, infrastructureIntegrations }: Props) => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const environments = currentWorkspace?.environments || [];
const { data: latestWsKey } = useGetUserWsKey(workspaceId);
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"activeBot"
] as const);
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
useGetCloudIntegrations();
@@ -70,11 +59,6 @@ export const IntegrationsPage = withProjectPermission(
isFetching: isIntegrationFetching
} = useGetWorkspaceIntegrations(workspaceId);
const { data: bot } = useGetWorkspaceBot(workspaceId);
// mutation
const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
useUpdateBotActiveStatus();
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const {
mutateAsync: deleteIntegrationAuths,
@@ -95,12 +79,6 @@ export const IntegrationsPage = withProjectPermission(
isIntegrationsAuthorizedEmpty &&
isIntegrationsEmpty
) {
if (bot?.id && currentWorkspace?.version === ProjectVersion.V1)
updateBotActiveStatusSync({
isActive: false,
botId: bot.id,
workspaceId
});
resetDeleteIntegrationAuths();
}
}, [
@@ -116,16 +94,6 @@ export const IntegrationsPage = withProjectPermission(
if (!selectedCloudIntegration) return;
try {
if (bot && !bot.isActive && currentWorkspace?.version === ProjectVersion.V1) {
const botKey = generateBotKey(bot.publicKey, latestWsKey!);
await updateBotActiveStatus({
workspaceId,
botKey,
isActive: true,
botId: bot.id
});
}
redirectForProviderAuth(selectedCloudIntegration);
} catch (error) {
console.error(error);
@@ -135,19 +103,9 @@ export const IntegrationsPage = withProjectPermission(
// function to strat integration for a provider
// confirmation to user passing the bot key for provider to get secret access
const handleProviderIntegrationStart = (provider: string) => {
if (!bot?.isActive) {
handlePopUpOpen("activeBot", { provider });
return;
}
handleProviderIntegration(provider);
};
const handleUserAcceptBotCondition = () => {
const { provider } = popUp.activeBot?.data as { provider: string };
handleProviderIntegration(provider);
handlePopUpClose("activeBot");
};
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
try {
await deleteIntegration({ id: integrationId, workspaceId });
@@ -195,7 +153,6 @@ export const IntegrationsPage = withProjectPermission(
integrations={integrations}
environments={environments}
onIntegrationDelete={({ id }, cb) => handleIntegrationDelete(id, cb)}
isBotActive={bot?.isActive}
workspaceId={workspaceId}
/>
<CloudIntegrationSection
@@ -205,30 +162,6 @@ export const IntegrationsPage = withProjectPermission(
onIntegrationStart={handleProviderIntegrationStart}
onIntegrationRevoke={handleIntegrationAuthRevoke}
/>
<Modal
isOpen={popUp.activeBot?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
>
<ModalContent
title={t("integrations.grant-access-to-secrets") as string}
footerContent={
<div className="flex items-center space-x-2">
<Button onClick={() => handleUserAcceptBotCondition()}>
{t("integrations.grant-access-button") as string}
</Button>
<Button
onClick={() => handlePopUpClose("activeBot")}
variant="outline_bg"
colorSchema="secondary"
>
Cancel
</Button>
</div>
}
>
{t("integrations.why-infisical-needs-access")}
</ModalContent>
</Modal>
<FrameworkIntegrationSection frameworks={frameworkIntegrations} />
<InfrastructureIntegrationSection integrations={infrastructureIntegrations} />
</div>
@@ -1,4 +1,3 @@
import Link from "next/link";
import { faCalendarCheck } from "@fortawesome/free-regular-svg-icons";
import { faArrowRight, faRefresh, faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -7,8 +6,6 @@ import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Alert,
AlertDescription,
Button,
DeleteActionModal,
EmptyState,
@@ -29,7 +26,6 @@ type Props = {
integrations?: TIntegration[];
isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
isBotActive: boolean | undefined;
workspaceId: string;
};
@@ -38,7 +34,6 @@ export const IntegrationsSection = ({
environments = [],
isLoading,
onIntegrationDelete,
isBotActive,
workspaceId
}: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -59,21 +54,7 @@ export const IntegrationsSection = ({
</div>
)}
{!isBotActive && Boolean(integrations.length) && (
<div className="px-6 py-4">
<Alert hideTitle variant="warning">
<AlertDescription>
All the active integrations will be disabled. Disable End-to-End Encryption in{" "}
<Link href={`/project/${workspaceId}/settings`} passHref>
<a className="underline underline-offset-2">project settings </a>
</Link>
to re-enable it.
</AlertDescription>
</Alert>
</div>
)}
{!isLoading && !integrations.length && isBotActive && (
{!isLoading && !integrations.length && (
<div className="mx-6">
<EmptyState
className="rounded-md border border-mineshaft-700 pt-8 pb-4"
@@ -81,7 +62,7 @@ export const IntegrationsSection = ({
/>
</div>
)}
{!isLoading && isBotActive && (
{!isLoading && (
<div className="flex min-w-max flex-col space-y-4 p-6 pt-0">
{integrations?.map((integration) => (
<div
@@ -13,11 +13,11 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { CommitType, DecryptedSecret, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
export type Props = {
op: CommitType;
secretVersion?: DecryptedSecret;
secretVersion?: SecretV3Raw;
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & { tags?: WsTag[] };
presentSecretVersionNumber: number;
hasMerged?: Boolean;
@@ -91,11 +91,11 @@ export const SecretApprovalRequestChangeItem = ({
<TBody>
<Tr>
<Td className="text-red-600">OLD</Td>
<Td>{secretVersion?.key}</Td>
<Td>{secretVersion?.secretKey}</Td>
<Td>
<SecretInput isReadOnly value={secretVersion?.value} />
<SecretInput isReadOnly value={secretVersion?.secretValue} />
</Td>
<Td>{secretVersion?.comment}</Td>
<Td>{secretVersion?.secretComment}</Td>
<Td>
{secretVersion?.tags?.map(({ name, id: tagId, color }) => (
<Tag
@@ -137,17 +137,23 @@ export const SecretApprovalRequestChangeItem = ({
) : (
<TBody>
<Tr>
<Td>{op === CommitType.CREATE ? newVersion?.secretKey : secretVersion?.key}</Td>
<Td>
{op === CommitType.CREATE ? newVersion?.secretKey : secretVersion?.secretKey}
</Td>
<Td>
<SecretInput
isReadOnly
value={
op === CommitType.CREATE ? newVersion?.secretValue : secretVersion?.value
op === CommitType.CREATE
? newVersion?.secretValue
: secretVersion?.secretValue
}
/>
</Td>
<Td>
{op === CommitType.CREATE ? newVersion?.secretComment : secretVersion?.comment}
{op === CommitType.CREATE
? newVersion?.secretComment
: secretVersion?.secretComment}
</Td>
<Td>
{(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map(
@@ -230,7 +230,7 @@ export const SecretApprovalRequestChanges = ({
</div>
<div className="flex flex-col space-y-4">
{secretApprovalRequestDetails.commits.map(
({ op, secretVersion, secret, newVersion }, index) => (
({ op, secretVersion, secret, ...newVersion }, index) => (
<SecretApprovalRequestChangeItem
op={op}
conflicts={secretApprovalRequestDetails.conflicts}
@@ -269,8 +269,9 @@ export const SecretApprovalRequestChanges = ({
>
<div className="flex-grow text-sm">
<Tooltip
content={`${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""
}`}
content={`${requiredApprover.firstName || ""} ${
requiredApprover.lastName || ""
}`}
>
<span>{requiredApprover?.email} </span>
</Tooltip>
@@ -23,13 +23,12 @@ import {
useGetProjectSecrets,
useGetSecretApprovalPolicyOfABoard,
useGetSecretImports,
useGetUserWsKey,
useGetWorkspaceSnapshotList,
useGetWsSnapshotCount,
useGetWsTags
} from "@app/hooks/api";
import { ProjectIndexSecretsSection } from "../SecretOverviewPage/components/ProjectIndexSecretsSection";
import { SecretV2MigrationSection } from "../SecretOverviewPage/components/SecretV2MigrationSection";
import { ActionBar } from "./components/ActionBar";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { DynamicSecretListView } from "./components/DynamicSecretListView";
@@ -93,14 +92,11 @@ export const SecretMainPage = () => {
}
}, [isWorkspaceLoading, currentWorkspace, environment, router.isReady]);
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
// fetch secrets
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
environment,
workspaceId,
secretPath,
decryptFileKey: decryptFileKey!,
options: {
enabled: canReadSecret
}
@@ -131,7 +127,6 @@ export const SecretMainPage = () => {
const { data: importedSecrets } = useGetImportedSecretsSingleEnv({
projectId: workspaceId,
environment,
decryptFileKey: decryptFileKey!,
path: secretPath,
options: {
enabled: canReadSecret
@@ -237,6 +232,7 @@ export const SecretMainPage = () => {
return (
<StoreProvider>
<div className="container mx-auto flex h-full flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<SecretV2MigrationSection />
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
@@ -250,12 +246,10 @@ export const SecretMainPage = () => {
protectionPolicyName={boardPolicy?.name}
/>
</div>
<ProjectIndexSecretsSection decryptFileKey={decryptFileKey!} />
{!isRollbackMode ? (
<>
<ActionBar
secrets={secrets}
importedSecrets={importedSecrets}
environment={environment}
workspaceId={workspaceId}
projectSlug={projectSlug}
@@ -333,7 +327,6 @@ export const SecretMainPage = () => {
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
decryptFileKey={decryptFileKey!}
isProtectedBranch={isProtectedBranch}
/>
)}
@@ -343,7 +336,6 @@ export const SecretMainPage = () => {
<CreateSecretForm
environment={environment}
workspaceId={workspaceId}
decryptFileKey={decryptFileKey!}
secretPath={secretPath}
autoCapitalize={currentWorkspace?.autoCapitalization}
isProtectedBranch={isProtectedBranch}
@@ -352,7 +344,6 @@ export const SecretMainPage = () => {
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
decryptFileKey={decryptFileKey!}
secretPath={secretPath}
isSmaller={isNotEmtpy}
environments={currentWorkspace?.environments}
@@ -372,7 +363,6 @@ export const SecretMainPage = () => {
) : (
<SnapshotView
snapshotId={snapshotId || ""}
decryptFileKey={decryptFileKey!}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
@@ -25,7 +25,6 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { decryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
import {
Button,
DeleteActionModal,
@@ -46,15 +45,10 @@ import {
UpgradePlanModal
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
import { interpolateSecrets } from "@app/helpers/secret";
import { usePopUp } from "@app/hooks";
import {
useCreateFolder,
useDeleteSecretBatch,
useGetUserWsKey,
useMoveSecrets
} from "@app/hooks/api";
import { DecryptedSecret, SecretType, TImportedSecrets, WsTag } from "@app/hooks/api/types";
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
import { SecretType, SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { debounce } from "@app/lib/fn/debounce";
import {
@@ -70,9 +64,8 @@ import { FolderForm } from "./FolderForm";
import { MoveSecretsModal } from "./MoveSecretsModal";
type Props = {
secrets?: DecryptedSecret[];
secrets?: SecretV3RawSanitized[];
// swtich the secrets type as it gets decrypted after api call
importedSecrets?: Array<Omit<TImportedSecrets, "secrets"> & { secrets: DecryptedSecret[] }>;
environment: string;
// @depreciated will be moving all these details to zustand
workspaceId: string;
@@ -92,7 +85,6 @@ type Props = {
export const ActionBar = ({
secrets = [],
importedSecrets = [],
environment,
workspaceId,
projectSlug,
@@ -124,7 +116,6 @@ export const ActionBar = ({
const { mutateAsync: createFolder } = useCreateFolder();
const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch();
const { mutateAsync: moveSecrets } = useMoveSecrets();
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const selectedSecrets = useSelectedSecrets();
const { reset: resetSelectedSecret } = useSelectedSecretActions();
@@ -155,58 +146,46 @@ export const ActionBar = ({
};
const handleSecretDownload = async () => {
const secPriority: Record<string, boolean> = {};
const downloadedSecrets: Array<{ key: string; value: string; comment?: string }> = [];
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const workspaceKey = decryptAssymmetric({
ciphertext: decryptFileKey!.encryptedKey,
nonce: decryptFileKey!.nonce,
publicKey: decryptFileKey!.sender.publicKey,
privateKey: PRIVATE_KEY
const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({
workspaceId,
expandSecretReferences: true,
includeImports: true,
environment,
secretPath
});
const expandSecrets = interpolateSecrets({
projectId: workspaceId,
secretEncKey: workspaceKey
});
const secretRecord: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean }
> = {};
// load up secrets in dashboard
secrets?.forEach(({ key, value, valueOverride, comment }) => {
secPriority[key] = true;
downloadedSecrets.push({ key, value: valueOverride || value, comment });
});
// now load imported secrets with secPriority
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
importedSecrets[i].secrets.forEach(({ key, value, valueOverride, comment }) => {
if (secPriority?.[key]) return;
downloadedSecrets.unshift({ key, value: valueOverride || value, comment });
secPriority[key] = true;
const secretsPicked = new Set<string>();
const secretsToDownload: { key: string; value?: string; comment?: string }[] = [];
localSecrets.forEach((el) => {
secretsPicked.add(el.secretKey);
secretsToDownload.push({
key: el.secretKey,
value: el.secretValue,
comment: el.secretComment
});
});
for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) {
const secret = localImportedSecrets[i].secrets[j];
if (!secretsPicked.has(secret.secretKey)) {
secretsToDownload.push({
key: secret.secretKey,
value: secret.secretValue,
comment: secret.secretComment
});
}
secretsPicked.add(secret.secretKey);
}
}
downloadedSecrets.forEach((secret) => {
secretRecord[secret.key] = {
value: secret.value,
comment: secret.comment
};
});
await expandSecrets(secretRecord);
const file = downloadedSecrets
const file = secretsToDownload
.sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
.reduce(
(prev, { key, comment }, index) =>
(prev, { key, comment, value }, index) =>
prev +
(comment
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${secretRecord[key].value}\n`
: `${key}=${secretRecord[key].value}\n`),
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
: `${key}=${value}\n`),
""
);
@@ -221,7 +200,7 @@ export const ActionBar = ({
secretPath,
workspaceId,
environment,
secrets: bulkDeletedSecrets.map(({ key }) => ({ secretName: key, type: SecretType.Shared }))
secrets: bulkDeletedSecrets.map(({ key }) => ({ secretKey: key, type: SecretType.Shared }))
});
resetSelectedSecret();
handlePopUpClose("bulkDeleteSecrets");
@@ -6,7 +6,7 @@ import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { useCreateSecretV3 } from "@app/hooks/api";
import { SecretType, UserWsKeyPair } from "@app/hooks/api/types";
import { SecretType } from "@app/hooks/api/types";
import { PopUpNames, usePopUpAction, usePopUpState } from "../../SecretMainPage.store";
@@ -20,7 +20,6 @@ type TFormSchema = z.infer<typeof typeSchema>;
type Props = {
environment: string;
workspaceId: string;
decryptFileKey: UserWsKeyPair;
secretPath?: string;
// modal props
autoCapitalize?: boolean;
@@ -30,7 +29,6 @@ type Props = {
export const CreateSecretForm = ({
environment,
workspaceId,
decryptFileKey,
secretPath = "/",
autoCapitalize = true,
isProtectedBranch = false
@@ -53,11 +51,10 @@ export const CreateSecretForm = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretValue: value || "",
secretComment: "",
type: SecretType.Shared,
latestFileKey: decryptFileKey
type: SecretType.Shared
});
closePopUp(PopUpNames.CreateSecretForm);
reset();
@@ -32,7 +32,6 @@ import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api";
import { UserWsKeyPair } from "@app/hooks/api/types";
const formSchema = z.object({
environment: z.string().trim(),
@@ -54,7 +53,6 @@ type Props = {
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
environments?: { name: string; slug: string }[];
workspaceId: string;
decryptFileKey: UserWsKeyPair;
environment: string;
secretPath: string;
};
@@ -62,7 +60,6 @@ type Props = {
export const CopySecretsFromBoard = ({
environments = [],
workspaceId,
decryptFileKey,
environment,
secretPath,
isOpen,
@@ -93,7 +90,6 @@ export const CopySecretsFromBoard = ({
workspaceId,
environment: selectedEnvSlug,
secretPath: debouncedEnvCopySecretPath,
decryptFileKey,
options: {
enabled:
Boolean(workspaceId) &&
@@ -16,7 +16,7 @@ import { usePopUp, useToggle } from "@app/hooks";
import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { DecryptedSecret, SecretType, UserWsKeyPair } from "@app/hooks/api/types";
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types";
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
@@ -40,10 +40,9 @@ type Props = {
isSmaller: boolean;
environments?: { name: string; slug: string }[];
workspaceId: string;
decryptFileKey: UserWsKeyPair;
environment: string;
secretPath: string;
secrets?: DecryptedSecret[];
secrets?: SecretV3RawSanitized[];
isProtectedBranch?: boolean;
};
@@ -51,7 +50,6 @@ export const SecretDropzone = ({
isSmaller,
environments = [],
workspaceId,
decryptFileKey,
environment,
secretPath,
secrets = [],
@@ -165,29 +163,27 @@ export const SecretDropzone = ({
try {
if (Object.keys(create || {}).length) {
await createSecretBatch({
latestFileKey: decryptFileKey!,
secretPath,
workspaceId,
environment,
secrets: Object.entries(create).map(([secretName, secData]) => ({
secrets: Object.entries(create).map(([secretKey, secData]) => ({
type: SecretType.Shared,
secretComment: secData.comments.join("\n"),
secretValue: secData.value,
secretName
secretKey
}))
});
}
if (Object.keys(update || {}).length) {
await updateSecretBatch({
latestFileKey: decryptFileKey!,
secretPath,
workspaceId,
environment,
secrets: Object.entries(update).map(([secretName, secData]) => ({
secrets: Object.entries(update).map(([secretKey, secData]) => ({
type: SecretType.Shared,
secretComment: secData.comments.join("\n"),
secretValue: secData.value,
secretName
secretKey
}))
});
}
@@ -281,7 +277,6 @@ export const SecretDropzone = ({
environment={environment}
environments={environments}
workspaceId={workspaceId}
decryptFileKey={decryptFileKey}
secretPath={secretPath}
isSmaller={isSmaller}
/>
@@ -19,13 +19,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
SecretInput,
TableContainer,
Tooltip
} from "@app/components/v2";
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { useResyncSecretReplication } from "@app/hooks/api";
@@ -37,7 +31,11 @@ type Props = {
secretPath?: string;
secretImport?: TSecretImport;
isReplicationExpand?: boolean;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
importedSecrets: {
key: string;
value?: string;
overriden: { env: string; secretPath: string };
}[];
searchTerm: string;
onExpandReplicateSecrets: (id: string) => void;
};
@@ -45,9 +43,9 @@ type Props = {
// to show the environment and folder icon
export const EnvFolderIcon = ({
env,
secretPath,
// isReplication
}: {
secretPath
}: // isReplication
{
env: string;
secretPath: string;
// isReplication?: boolean;
@@ -18,7 +18,7 @@ import { usePopUp } from "@app/hooks";
import { useDeleteSecretImport, useUpdateSecretImport } from "@app/hooks/api";
import { ReservedFolders } from "@app/hooks/api/secretFolders/types";
import { TSecretImport } from "@app/hooks/api/secretImports/types";
import { DecryptedSecret, WorkspaceEnv } from "@app/hooks/api/types";
import { SecretV3RawSanitized, WorkspaceEnv } from "@app/hooks/api/types";
import { formatReservedPaths } from "@app/lib/fn/string";
import { SecretImportItem } from "./SecretImportItem";
@@ -29,14 +29,14 @@ type TImportedSecrets = Array<{
environmentInfo: WorkspaceEnv;
secretPath: string;
folderId: string;
secrets: DecryptedSecret[];
secrets: SecretV3RawSanitized[];
}>;
export const computeImportedSecretRows = (
importedSecEnv: string,
importedSecPath: string,
importSecrets: TImportedSecrets = [],
secrets: DecryptedSecret[] = []
secrets: SecretV3RawSanitized[] = []
) => {
const importedSecIndex = importSecrets.findIndex(
({ secretPath, environmentInfo }) =>
@@ -63,7 +63,7 @@ export const computeImportedSecretRows = (
const importedEntry: Record<string, boolean> = {};
const importedSecretEntries: {
key: string;
value: string;
value?: string;
overriden: {
env: string;
secretPath: string;
@@ -90,7 +90,7 @@ type Props = {
secretPath?: string;
secretImports?: TSecretImport[];
isFetching?: boolean;
secrets?: DecryptedSecret[];
secrets?: SecretV3RawSanitized[];
importedSecrets?: TImportedSecrets;
searchTerm: string;
};
@@ -37,7 +37,7 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api";
import { DecryptedSecret, UserWsKeyPair, WsTag } from "@app/hooks/api/types";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@@ -48,12 +48,11 @@ type Props = {
secretPath: string;
onToggle: (isOpen: boolean) => void;
onClose: () => void;
secret: DecryptedSecret;
decryptFileKey: UserWsKeyPair;
secret: SecretV3RawSanitized;
onDeleteSecret: () => void;
onSaveSecret: (
orgSec: DecryptedSecret,
modSec: Omit<DecryptedSecret, "tags"> & { tags: { id: string }[] },
orgSec: SecretV3RawSanitized,
modSec: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
cb?: () => void
) => Promise<void>;
tags: WsTag[];
@@ -64,7 +63,6 @@ type Props = {
export const SecretDetailSidebar = ({
isOpen,
onToggle,
decryptFileKey,
secret,
onDeleteSecret,
onSaveSecret,
@@ -101,7 +99,7 @@ export const SecretDetailSidebar = ({
control,
name: "tags"
});
const selectedTags = watch("tags", []);
const selectedTags = watch("tags", []) || [];
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.id]: true }),
{}
@@ -114,8 +112,7 @@ export const SecretDetailSidebar = ({
const { data: secretVersion } = useGetSecretVersion({
limit: 10,
offset: 0,
secretId: secret?.id,
decryptFileKey
secretId: secret?.id
});
const handleOverrideClick = () => {
@@ -420,7 +417,12 @@ export const SecretDetailSidebar = ({
className="px-2 py-1"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => handleSecretShare(secret.valueOverride ?? secret.value)}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share Secret
</Button>
@@ -428,7 +430,7 @@ export const SecretDetailSidebar = ({
<div className="dark mt-4 mb-4 flex-grow text-sm text-bunker-300">
<div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, value, id }, i) => (
{secretVersion?.map(({ createdAt, secretValue, id }, i) => (
<div key={id} className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<div>
@@ -438,7 +440,7 @@ export const SecretDetailSidebar = ({
</div>
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
<div className="break-all font-mono">{value}</div>
<div className="break-all font-mono">{secretValue}</div>
</div>
</div>
))}
@@ -28,7 +28,7 @@ import {
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WsTag } from "@app/hooks/api/types";
import { subject } from "@casl/ability";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -46,14 +46,14 @@ import {
} from "./SecretListView.utils";
type Props = {
secret: DecryptedSecret;
secret: SecretV3RawSanitized;
onSaveSecret: (
orgSec: DecryptedSecret,
modSec: Omit<DecryptedSecret, "tags"> & { tags: { id: string }[] },
orgSec: SecretV3RawSanitized,
modSec: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
cb?: () => void
) => Promise<void>;
onDeleteSecret: (sec: DecryptedSecret) => void;
onDetailViewSecret: (sec: DecryptedSecret) => void;
onDeleteSecret: (sec: SecretV3RawSanitized) => void;
onDetailViewSecret: (sec: SecretV3RawSanitized) => void;
isVisible?: boolean;
isSelected?: boolean;
onToggleSecretSelect: (id: string) => void;
@@ -113,7 +113,7 @@ export const SecretItem = memo(
const overrideAction = watch("overrideAction");
const hasComment = Boolean(watch("comment"));
const selectedTags = watch("tags", []);
const selectedTags = watch("tags", []) || [];
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.id]: true }),
{}
@@ -10,9 +10,9 @@ import { usePopUp } from "@app/hooks";
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { DecryptedSecret, SecretType } from "@app/hooks/api/secrets/types";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { secretSnapshotKeys } from "@app/hooks/api/secretSnapshots/queries";
import { UserWsKeyPair, WsTag } from "@app/hooks/api/types";
import { WsTag } from "@app/hooks/api/types";
import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal";
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
@@ -22,10 +22,9 @@ import { SecretItem } from "./SecretItem";
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
type Props = {
secrets?: DecryptedSecret[];
secrets?: SecretV3RawSanitized[];
environment: string;
workspaceId: string;
decryptFileKey: UserWsKeyPair;
secretPath?: string;
filter: Filter;
sortDir?: SortDir;
@@ -34,8 +33,8 @@ type Props = {
isProtectedBranch?: boolean;
};
const reorderSecretGroupByUnderscore = (secrets: DecryptedSecret[], sortDir: SortDir) => {
const groupedSecrets: Record<string, DecryptedSecret[]> = {};
const reorderSecretGroupByUnderscore = (secrets: SecretV3RawSanitized[], sortDir: SortDir) => {
const groupedSecrets: Record<string, SecretV3RawSanitized[]> = {};
secrets.forEach((secret) => {
const lastSeperatorIndex = secret.key.lastIndexOf("_");
const namespace =
@@ -53,7 +52,11 @@ const reorderSecretGroupByUnderscore = (secrets: DecryptedSecret[], sortDir: Sor
.map((namespace) => ({ namespace, secrets: groupedSecrets[namespace] }));
};
const reorderSecret = (secrets: DecryptedSecret[], sortDir: SortDir, filter?: GroupBy | null) => {
const reorderSecret = (
secrets: SecretV3RawSanitized[],
sortDir: SortDir,
filter?: GroupBy | null
) => {
if (filter === GroupBy.PREFIX) {
return reorderSecretGroupByUnderscore(secrets, sortDir);
}
@@ -70,13 +73,13 @@ const reorderSecret = (secrets: DecryptedSecret[], sortDir: SortDir, filter?: Gr
];
};
export const filterSecrets = (secrets: DecryptedSecret[], filter: Filter) =>
export const filterSecrets = (secrets: SecretV3RawSanitized[], filter: Filter) =>
secrets.filter(({ key, value, tags }) => {
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
const searchTerm = filter.searchFilter.toLowerCase();
return (
(!isTagFilterActive || tags.some(({ id }) => filter.tags?.[id])) &&
(key.toLowerCase().includes(searchTerm) || value.toLowerCase().includes(searchTerm))
(!isTagFilterActive || tags?.some(({ id }) => filter.tags?.[id])) &&
(key.toLowerCase().includes(searchTerm) || value?.toLowerCase().includes(searchTerm))
);
});
@@ -84,7 +87,6 @@ export const SecretListView = ({
secrets = [],
environment,
workspaceId,
decryptFileKey,
secretPath = "/",
filter,
sortDir = SortDir.ASC,
@@ -148,7 +150,7 @@ export const SecretListView = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
type,
secretId
});
@@ -160,12 +162,10 @@ export const SecretListView = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretId,
secretKey: key,
secretValue: value || "",
type,
latestFileKey: decryptFileKey,
tags,
tagIds: tags,
secretComment: comment,
secretReminderRepeatDays: reminderRepeatDays,
secretReminderNote: reminderNote,
@@ -180,12 +180,11 @@ export const SecretListView = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretValue: value || "",
secretComment: "",
skipMultilineEncoding,
type,
latestFileKey: decryptFileKey
type
},
{}
);
@@ -193,8 +192,8 @@ export const SecretListView = ({
const handleSaveSecret = useCallback(
async (
orgSecret: DecryptedSecret,
modSecret: Omit<DecryptedSecret, "tags"> & { tags: { id: string }[] },
orgSecret: SecretV3RawSanitized,
modSecret: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
cb?: () => void
) => {
const { key: oldKey } = orgSecret;
@@ -288,7 +287,7 @@ export const SecretListView = ({
);
const handleSecretDelete = useCallback(async () => {
const { key, id: secretId } = popUp.deleteSecret?.data as DecryptedSecret;
const { key, id: secretId } = popUp.deleteSecret?.data as SecretV3RawSanitized;
try {
await handleSecretOperation("delete", SecretType.Shared, key, { secretId });
// wrap this in another function and then reuse
@@ -317,16 +316,16 @@ export const SecretListView = ({
text: "Failed to delete secret"
});
}
}, [(popUp.deleteSecret?.data as DecryptedSecret)?.key, environment, secretPath]);
}, [(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key, environment, secretPath]);
// for optimization on minimise re-rendering of secret items
const onCreateTag = useCallback(() => handlePopUpOpen("createTag"), []);
const onDeleteSecret = useCallback(
(sec: DecryptedSecret) => handlePopUpOpen("deleteSecret", sec),
(sec: SecretV3RawSanitized) => handlePopUpOpen("deleteSecret", sec),
[]
);
const onDetailViewSecret = useCallback(
(sec: DecryptedSecret) => handlePopUpOpen("secretDetail", sec),
(sec: SecretV3RawSanitized) => handlePopUpOpen("secretDetail", sec),
[]
);
@@ -380,7 +379,7 @@ export const SecretListView = ({
)}
<DeleteActionModal
isOpen={popUp.deleteSecret.isOpen}
deleteKey={(popUp.deleteSecret?.data as DecryptedSecret)?.key}
deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
title="Do you want to delete this secret?"
onChange={(isOpen) => handlePopUpToggle("deleteSecret", isOpen)}
onDeleteApproved={handleSecretDelete}
@@ -391,8 +390,7 @@ export const SecretListView = ({
secretPath={secretPath}
isOpen={popUp.secretDetail.isOpen}
onToggle={(isOpen) => handlePopUpToggle("secretDetail", isOpen)}
decryptFileKey={decryptFileKey}
secret={popUp.secretDetail.data as DecryptedSecret}
secret={popUp.secretDetail.data as SecretV3RawSanitized}
onDeleteSecret={() => handlePopUpOpen("deleteSecret", popUp.secretDetail.data)}
onClose={() => handlePopUpClose("secretDetail")}
onSaveSecret={handleSaveSecret}
@@ -23,7 +23,10 @@ export enum SecretActionType {
export const formSchema = z.object({
key: z.string().trim().min(1, { message: "Secret key is required" }),
value: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
value: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.optional(),
idOverride: z.string().trim().optional(),
valueOverride: z
.string()
@@ -52,6 +55,7 @@ export const formSchema = z.object({
})
.array()
.default([])
.optional()
});
export type TFormSchema = z.infer<typeof formSchema>;
@@ -21,7 +21,7 @@ import {
Tr
} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { DecryptedSecret } from "@app/hooks/api/types";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
export enum TDiffModes {
NoChange = "no change",
@@ -32,8 +32,8 @@ export enum TDiffModes {
type Props = {
mode: TDiffModes;
preSecret?: DecryptedSecret;
postSecret: DecryptedSecret;
preSecret?: SecretV3RawSanitized;
postSecret: SecretV3RawSanitized;
};
export type TDiffView<T> = {
mode: TDiffModes;
@@ -120,9 +120,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<Td className="border-r border-mineshaft-600">Value</Td>
{isModified && (
<Td className="border-r border-mineshaft-600">
<SecretInput
value={preSecret?.value}
/>
<SecretInput value={preSecret?.value} />
</Td>
)}
<Td>
@@ -14,7 +14,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, ContentLoader, Input, Tag, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useGetSnapshotSecrets, usePerformSecretRollback } from "@app/hooks/api";
import { DecryptedSecret, TSecretFolder, UserWsKeyPair } from "@app/hooks/api/types";
import { SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
import { renderIcon, SecretItem, TDiffModes, TDiffView } from "./SecretItem";
@@ -23,8 +23,7 @@ type Props = {
environment: string;
workspaceId: string;
secretPath?: string;
decryptFileKey: UserWsKeyPair;
secrets?: DecryptedSecret[];
secrets?: SecretV3RawSanitized[];
folders?: TSecretFolder[];
snapshotCount?: number;
onGoBack: () => void;
@@ -33,7 +32,7 @@ type Props = {
const LOADER_TEXT = ["Fetching your snapshot", "Creating the difference view"];
const deepCompareSecrets = (lhs: DecryptedSecret, rhs: DecryptedSecret) =>
const deepCompareSecrets = (lhs: SecretV3RawSanitized, rhs: SecretV3RawSanitized) =>
lhs.key === rhs.key &&
lhs.value === rhs.value &&
lhs.comment === rhs.comment &&
@@ -45,21 +44,18 @@ export const SnapshotView = ({
environment,
workspaceId,
secretPath,
decryptFileKey,
secrets = [],
folders = [],
onGoBack,
snapshotCount,
onClickListSnapshot
}: Props) => {
const [search, setSearch] = useState("");
const { mutateAsync: performRollback, isLoading: isRollingBack } = usePerformSecretRollback();
const { data: snapshotData, isLoading: isSnapshotLoading } = useGetSnapshotSecrets({
snapshotId,
env: environment,
decryptFileKey
env: environment
});
const rollingFolder = snapshotData?.folders || [];
@@ -92,11 +88,11 @@ export const SnapshotView = ({
}, [folders, rollingFolder]);
const secretDiffView = useMemo(() => {
const secretGroupById = secrets.reduce<Record<string, DecryptedSecret>>(
const secretGroupById = secrets.reduce<Record<string, SecretV3RawSanitized>>(
(prev, curr) => ({ ...prev, [curr.id]: curr }),
{}
);
const diffView: Array<TDiffView<DecryptedSecret>> = [];
const diffView: Array<TDiffView<SecretV3RawSanitized>> = [];
rollingSecrets.forEach((rollSecret) => {
const { id } = rollSecret;
const doesExist = Boolean(secretGroupById?.[id]);
@@ -59,7 +59,6 @@ import {
useGetFoldersByEnv,
useGetImportedSecretsAllEnvs,
useGetProjectSecretsAllEnv,
useGetUserWsKey,
useUpdateSecretV3
} from "@app/hooks/api";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
@@ -70,10 +69,10 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
import { SecretV2MigrationSection } from "./components/SecretV2MigrationSection";
import { SelectionPanel } from "./components/SelectionPanel/SelectionPanel";
export enum EntryType {
@@ -103,7 +102,6 @@ export const SecretOverviewPage = () => {
const { currentOrg } = useOrganization();
const workspaceId = currentWorkspace?.id as string;
const projectSlug = currentWorkspace?.slug as string;
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
const [searchFilter, setSearchFilter] = useState("");
const secretPath = (router.query?.secretPath as string) || "/";
@@ -178,8 +176,7 @@ export const SecretOverviewPage = () => {
} = useGetProjectSecretsAllEnv({
workspaceId,
envs: userAvailableEnvs.map(({ slug }) => slug),
secretPath,
decryptFileKey: latestFileKey!
secretPath
});
const { folders, folderNames, isFolderPresentInEnv, getFolderByNameAndEnv } = useGetFoldersByEnv({
@@ -190,7 +187,6 @@ export const SecretOverviewPage = () => {
const { isImportedSecretPresentInEnv, getImportedSecretByKey } = useGetImportedSecretsAllEnvs({
projectId: workspaceId,
decryptFileKey: latestFileKey!,
path: secretPath,
environments: userAvailableEnvs.map(({ slug }) => slug)
});
@@ -317,11 +313,10 @@ export const SecretOverviewPage = () => {
environment: env,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretValue: value,
secretComment: "",
type: SecretType.Shared,
latestFileKey: latestFileKey!
type: SecretType.Shared
});
createNotification({
type: "success",
@@ -348,19 +343,16 @@ export const SecretOverviewPage = () => {
env: string,
key: string,
value: string,
type = SecretType.Shared,
secretId?: string
type = SecretType.Shared
) => {
try {
await updateSecretV3({
environment: env,
workspaceId,
secretPath,
secretId,
secretName: key,
secretKey: key,
secretValue: value,
type,
latestFileKey: latestFileKey!
type
});
createNotification({
type: "success",
@@ -381,7 +373,7 @@ export const SecretOverviewPage = () => {
environment: env,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretId,
type: SecretType.Shared
});
@@ -481,7 +473,7 @@ export const SecretOverviewPage = () => {
return (
<>
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
<SecretV2MigrationSection />
<div className="relative right-5 ml-4">
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
</div>
@@ -861,7 +853,6 @@ export const SecretOverviewPage = () => {
getSecretByKey={getSecretByKey}
onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
decryptFileKey={latestFileKey!}
/>
<Modal
isOpen={popUp.addFolder.isOpen}
@@ -18,7 +18,7 @@ import {
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { useWorkspace } from "@app/context";
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { DecryptedSecret, SecretType, UserWsKeyPair } from "@app/hooks/api/types";
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types";
const typeSchema = z
.object({
@@ -34,8 +34,7 @@ type TFormSchema = z.infer<typeof typeSchema>;
type Props = {
secretPath?: string;
decryptFileKey: UserWsKeyPair;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
// modal props
isOpen?: boolean;
onClose: () => void;
@@ -44,7 +43,6 @@ type Props = {
export const CreateSecretForm = ({
secretPath = "/",
decryptFileKey,
isOpen,
getSecretByKey,
onClose,
@@ -101,10 +99,9 @@ export const CreateSecretForm = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretValue: value || "",
type: SecretType.Shared,
latestFileKey: decryptFileKey
type: SecretType.Shared
});
}
@@ -112,11 +109,10 @@ export const CreateSecretForm = ({
environment,
workspaceId,
secretPath,
secretName: key,
secretKey: key,
secretValue: value || "",
secretComment: "",
type: SecretType.Shared,
latestFileKey: decryptFileKey
type: SecretType.Shared
});
});
@@ -1,109 +0,0 @@
import { ProjectPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Button, Spinner } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetWorkspaceIndexStatus, useNameWorkspaceSecrets } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { UserWsKeyPair } from "@app/hooks/api/types";
import { fetchWorkspaceSecrets } from "@app/hooks/api/workspace/queries";
// TODO: add check so that this only shows up if user is
// an admin in the workspace
type Props = {
decryptFileKey: UserWsKeyPair;
};
export const ProjectIndexSecretsSection = ({ decryptFileKey }: Props) => {
const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission();
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
currentWorkspace?.id ?? ""
);
const [isIndexing, setIsIndexing] = useToggle();
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
const onEnableBlindIndices = async () => {
if (!currentWorkspace?.id) return;
setIsIndexing.on();
try {
const encryptedSecrets = await fetchWorkspaceSecrets(currentWorkspace.id);
const key = decryptAssymmetric({
ciphertext: decryptFileKey.encryptedKey,
nonce: decryptFileKey.nonce,
publicKey: decryptFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
const secretName = decryptSymmetric({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag,
key
});
return {
secretName,
secretId: encryptedSecret.id
};
});
await nameWorkspaceSecrets.mutateAsync({
workspaceId: currentWorkspace.id,
secretsToUpdate
});
} catch (err) {
console.log(err);
} finally {
setIsIndexing.off();
}
};
// for non admin this would throw an error
// so no need to render
return !isBlindIndexedLoading && typeof isBlindIndexed === "boolean" && !isBlindIndexed ? (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
{isIndexing && (
<div className="absolute top-0 left-0 z-50 flex h-screen w-screen items-center justify-center bg-bunker-500 bg-opacity-80">
<Spinner size="lg" className="text-primary" />
<div className="ml-4 flex flex-col space-y-1">
<div className="text-3xl font-medium">Please wait</div>
<span className="inline-block">Re-indexing your secrets...</span>
</div>
</div>
)}
<p className="mb-2 text-lg font-semibold">Action Required</p>
<p className="mb-4 leading-7 text-gray-400">
Your project was created before the introduction of blind indexing. To continue accessing
secrets by name through the SDK, public API and web dashboard, please enable blind indexing.{" "}
<b>
{membership.role !== ProjectMembershipRole.Admin && "This is an admin only operation."}
</b>
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Button
onClick={onEnableBlindIndices}
isDisabled={!isAllowed || membership.role !== ProjectMembershipRole.Admin}
color="mineshaft"
type="submit"
isLoading={isIndexing}
>
Enable Blind Indexing
</Button>
)}
</ProjectPermissionCan>
</div>
) : (
<div />
);
};
@@ -1 +0,0 @@
export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection";
@@ -13,7 +13,7 @@ import { twMerge } from "tailwind-merge";
import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { DecryptedSecret, SecretType } from "@app/hooks/api/secrets/types";
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
import { SecretEditRow } from "./SecretEditRow";
@@ -26,7 +26,7 @@ type Props = {
expandableColWidth: number;
isSelected: boolean;
onToggleSecretSelect: (key: string) => void;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (
env: string,
@@ -40,7 +40,7 @@ type Props = {
getImportedSecretByKey: (
env: string,
secretName: string
) => { secret?: DecryptedSecret; environmentInfo?: WorkspaceEnv } | undefined;
) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined;
};
export const SecretOverviewTableRow = ({
@@ -17,15 +17,15 @@ import {
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetUserWsKey, useUpdateSecretV3 } from "@app/hooks/api";
import { DecryptedSecret, SecretType } from "@app/hooks/api/types";
import { useUpdateSecretV3 } from "@app/hooks/api";
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types";
import { SecretActionType } from "@app/views/SecretMainPage/components/SecretListView/SecretListView.utils";
type Props = {
secretKey: string;
secretPath: string;
environments: { name: string; slug: string }[];
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
};
export const formSchema = z.object({
@@ -64,8 +64,6 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
);
const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const [isSecNameCopied, setIsSecNameCopied] = useToggle(false);
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
@@ -109,12 +107,10 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
environment: secret?.env,
workspaceId,
secretPath,
secretName: secret.key,
secretId: secret.id,
secretKey: secret.key,
secretValue: secret.value || "",
type: SecretType.Shared,
latestFileKey: decryptFileKey!,
tags: secret.tags.map((tag) => tag.id),
tagIds: secret.tags?.map((tag) => tag.id),
secretComment: secret.comment,
secretReminderRepeatDays: secret.reminderRepeatDays,
secretReminderNote: secret.reminderNote,
@@ -0,0 +1,183 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, Checkbox, Modal, ModalContent, Spinner } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useGetWorkspaceById, useMigrateProjectToV3 } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
enum ProjectUpgradeStatus {
InProgress = "IN_PROGRESS",
// Completed -> Will be null if completed. So a completed status is not needed
Failed = "FAILED"
}
const formSchema = z.object({
isCLIChecked: z.literal(true),
isOperatorChecked: z.literal(true),
shouldCloseOpenApprovals: z.literal(true)
});
export const SecretV2MigrationSection = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["migrationInfo"] as const);
const { currentWorkspace } = useWorkspace();
const { data: workspaceDetails, refetch } = useGetWorkspaceById(
// if v3 no need to fetch
currentWorkspace?.version === ProjectVersion.V3 ? "" : currentWorkspace?.id || "",
{
refetchInterval:
currentWorkspace?.upgradeStatus === ProjectUpgradeStatus.InProgress ? 3000 : false
}
);
const { membership } = useProjectPermission();
const migrateProjectToV3 = useMigrateProjectToV3();
const { handleSubmit, control, reset } = useForm({ resolver: zodResolver(formSchema) });
useEffect(() => {
if (!popUp.migrationInfo.isOpen) {
reset();
}
}, [popUp.migrationInfo.isOpen]);
const isProjectUpgraded = workspaceDetails?.version === ProjectVersion.V3;
useEffect(() => {
if (isProjectUpgraded && migrateProjectToV3.data) {
createNotification({ type: "success", text: "Project upgrade completed successfully" });
migrateProjectToV3.reset();
}
}, [isProjectUpgraded, Boolean(migrateProjectToV3.data)]);
if (isProjectUpgraded || currentWorkspace?.version === ProjectVersion.V3) return null;
const isUpgrading = workspaceDetails?.upgradeStatus === ProjectUpgradeStatus.InProgress;
const didProjectUpgradeFailed = workspaceDetails?.upgradeStatus === ProjectUpgradeStatus.Failed;
const handleMigrationSecretV2 = async () => {
try {
handlePopUpToggle("migrationInfo");
await migrateProjectToV3.mutateAsync({ workspaceId: currentWorkspace?.id || "" });
refetch();
createNotification({
text: "Project upgrade started",
type: "success"
});
} catch {
createNotification({
text: "Failed to upgrade project",
type: "error"
});
}
};
const isAdmin = membership?.roles.includes(ProjectMembershipRole.Admin);
return (
<div className="mt-4 rounded-lg border border-primary-600 bg-mineshaft-900 p-4">
{isUpgrading && (
<div className="absolute top-0 left-0 z-50 flex h-screen w-screen items-center justify-center bg-bunker-500 bg-opacity-80">
<Spinner size="lg" className="text-primary" />
<div className="ml-4 flex flex-col space-y-1">
<div className="text-3xl font-medium">Please wait</div>
<span className="inline-block">Upgrading your project...</span>
</div>
</div>
)}
<p className="mb-2 text-lg font-semibold">Action Required</p>
<p className="mb-4 leading-7 text-gray-400">
Infisical secrets engine is now 10x faster and allows you to encrypt secrets with your own
KMS. Upgrade your project to receive these improvements.
<b>{!isAdmin && "This is an admin only operation."}</b>
</p>
<Button
onClick={() => handlePopUpOpen("migrationInfo")}
isDisabled={!isAdmin || isUpgrading}
color="mineshaft"
isLoading={migrateProjectToV3.isLoading}
>
Upgrade Project
</Button>
{didProjectUpgradeFailed && (
<p className="mt-2 text-sm leading-7 text-red-400">
<FontAwesomeIcon icon={faTriangleExclamation} className="mr-2" />
Project upgrade unsuccessful. For assistance, please contact the Infisical support team.
</p>
)}
<Modal
isOpen={popUp.migrationInfo.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("migrationInfo", isOpen)}
>
<ModalContent
title="Upgrade Checklist"
subTitle="To ensure smooth transition, please ensure the following requirements are met before upgrading this project."
>
<div>
<form onSubmit={handleSubmit(handleMigrationSecretV2)}>
<div className="flex flex-col space-y-4">
<Controller
control={control}
name="isCLIChecked"
defaultValue={false}
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
<Checkbox
id="is-cli-checked"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
isError={Boolean(error?.message)}
>
Infisical CLI version is v0.25.0 or above.
</Checkbox>
)}
/>
<Controller
control={control}
name="isOperatorChecked"
defaultValue={false}
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
<Checkbox
id="is-operator-checked"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
isError={Boolean(error?.message)}
>
Infisical Kubernetes Operator version is v0.7.0 or above.
</Checkbox>
)}
/>
<Controller
control={control}
name="shouldCloseOpenApprovals"
defaultValue={false}
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
<Checkbox
id="is-approvals-checked"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
isError={Boolean(error?.message)}
>
Close/merge all open approval/access requests.
</Checkbox>
)}
/>
</div>
<div className="mt-8 flex space-x-4">
<Button type="submit">Confirm Upgrade</Button>
<Button variant="outline_bg" onClick={() => handlePopUpToggle("migrationInfo")}>
Cancel
</Button>
</div>
</form>
</div>
</ModalContent>
</Modal>
</div>
);
};
@@ -0,0 +1 @@
export { SecretV2MigrationSection } from "./SecretV2MigrationSection";
@@ -14,8 +14,8 @@ import {
import { usePopUp } from "@app/hooks";
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
import {
DecryptedSecret,
SecretType,
SecretV3RawSanitized,
TDeleteSecretBatchDTO,
TSecretFolder
} from "@app/hooks/api/types";
@@ -27,7 +27,7 @@ export enum EntryType {
type Props = {
secretPath: string;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
getFolderByNameAndEnv: (name: string, env: string) => TSecretFolder | undefined;
resetSelectedEntries: () => void;
selectedEntries: {
@@ -115,7 +115,7 @@ export const SelectionPanel = ({
return [
...accum,
{
secretName: entry.key,
secretKey: entry.key,
type: SecretType.Shared
}
];
@@ -1,4 +1,3 @@
import { useTranslation } from "react-i18next";
import Link from "next/link";
import {
faArrowsSpin,
@@ -16,12 +15,9 @@ import { formatDistance } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
IconButton,
Modal,
ModalContent,
Skeleton,
Spinner,
Table,
@@ -48,22 +44,17 @@ import {
useDeleteSecretRotation,
useGetSecretRotationProviders,
useGetSecretRotations,
useGetUserWsKey,
useGetWorkspaceBot,
useRestartSecretRotation,
useUpdateBotActiveStatus
useRestartSecretRotation
} from "@app/hooks/api";
import { TSecretRotationProviderTemplate } from "@app/hooks/api/types";
import { CreateRotationForm } from "./components/CreateRotationForm";
import { generateBotKey } from "./SecretRotationPage.utils";
export const SecretRotationPage = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
const { permission } = useProjectPermission();
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"createRotation",
"activeBot",
@@ -77,13 +68,10 @@ export const SecretRotationPage = withProjectPermission(
);
const { subscription } = useSubscription();
const { data: userWsKey } = useGetUserWsKey(workspaceId);
const { data: secretRotationProviders, isLoading: isRotationProviderLoading } =
useGetSecretRotationProviders({ workspaceId });
const { data: secretRotations, isLoading: isRotationLoading } = useGetSecretRotations({
workspaceId,
decryptFileKey: userWsKey!
workspaceId
});
const {
@@ -97,11 +85,6 @@ export const SecretRotationPage = withProjectPermission(
isLoading: isRestartingRotation
} = useRestartSecretRotation();
const { data: bot } = useGetWorkspaceBot(workspaceId);
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
const isBotActive = Boolean(bot?.isActive);
const handleDeleteRotation = async () => {
const { id } = popUp.deleteRotation.data as { id: string };
try {
@@ -142,29 +125,6 @@ export const SecretRotationPage = withProjectPermission(
}
};
const handleUserAcceptBotCondition = async () => {
const provider = popUp.activeBot?.data as TSecretRotationProviderTemplate;
try {
if (bot?.id) {
const botKey = generateBotKey(bot.publicKey, userWsKey!);
await updateBotActiveStatus({
isActive: true,
botId: bot.id,
workspaceId,
botKey
});
}
handlePopUpOpen("createRotation", provider);
handlePopUpClose("activeBot");
} catch (error) {
console.log(error);
createNotification({
type: "error",
text: "Failed to create bot"
});
}
};
const handleCreateRotation = async (provider: TSecretRotationProviderTemplate) => {
if (subscription && !subscription?.secretRotation) {
handlePopUpOpen("upgradePlan");
@@ -174,11 +134,7 @@ export const SecretRotationPage = withProjectPermission(
createNotification({ type: "error", text: "Access permission denied!!" });
return;
}
if (isBotActive) {
handlePopUpOpen("createRotation", provider);
} else {
handlePopUpOpen("activeBot", provider);
}
handlePopUpOpen("createRotation", provider);
};
return (
@@ -391,30 +347,6 @@ export const SecretRotationPage = withProjectPermission(
onToggle={(isOpen) => handlePopUpToggle("createRotation", isOpen)}
provider={(popUp.createRotation.data as TSecretRotationProviderTemplate) || {}}
/>
<Modal
isOpen={popUp.activeBot?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
>
<ModalContent
title={t("integrations.grant-access-to-secrets") as string}
footerContent={
<div className="flex items-center space-x-2">
<Button onClick={() => handleUserAcceptBotCondition()}>
{t("integrations.grant-access-button") as string}
</Button>
<Button
onClick={() => handlePopUpClose("activeBot")}
variant="outline_bg"
colorSchema="secondary"
>
Cancel
</Button>
</div>
}
>
{t("integrations.why-infisical-needs-access")}
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.deleteRotation.isOpen}
title="Are you sure want to delete this rotation?"
@@ -5,7 +5,7 @@ import { z } from "zod";
import { Button, FormControl, Input, Select, SelectItem, Spinner } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useWorkspace } from "@app/context";
import { useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
import { useGetProjectSecrets } from "@app/hooks/api";
const formSchema = z.object({
environment: z.string().trim(),
@@ -38,12 +38,10 @@ export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Pr
const secretPath = watch("secretPath");
const selectedSecrets = watch("secrets");
const { data: userWsKey } = useGetUserWsKey(workspaceId);
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
environment,
secretPath,
decryptFileKey: userWsKey!
secretPath
});
return (
@@ -0,0 +1,97 @@
import { useState } from "react";
import { faAws } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { Modal, ModalContent } from "@app/components/v2";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AwsKmsForm } from "./AwsKmsForm";
type Props = {
isOpen?: boolean;
onToggle: (isOpen: boolean) => void;
};
enum WizardSteps {
SelectProvider = "select-provider",
ProviderInputs = "provider-inputs"
}
const EXTERNAL_KMS_LIST = [
{
icon: faAws,
provider: ExternalKmsProvider.AWS,
title: "AWS KMS"
}
];
export const AddExternalKmsForm = ({ isOpen, onToggle }: Props) => {
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider);
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const handleFormReset = (state: boolean = false) => {
onToggle(state);
setWizardStep(WizardSteps.SelectProvider);
setSelectedProvider(null);
};
return (
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
<ModalContent
title="Add a Key Management System"
subTitle="Configure an external key management system (KMS)"
className="my-4"
>
<AnimatePresence exitBeforeEnter>
{wizardStep === WizardSteps.SelectProvider && (
<motion.div
key="select-type-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<div className="mb-4 text-mineshaft-300">Select a KMS Provider</div>
<div className="flex items-center space-x-4">
{EXTERNAL_KMS_LIST.map(({ icon, provider, title }) => (
<div
key={`kms-${provider}`}
className="flex h-28 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
role="button"
tabIndex={0}
onClick={() => {
setSelectedProvider(provider);
setWizardStep(WizardSteps.ProviderInputs);
}}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
setSelectedProvider(provider);
setWizardStep(WizardSteps.ProviderInputs);
}
}}
>
<FontAwesomeIcon icon={icon} size="lg" />
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
</div>
))}
</div>
</motion.div>
)}
{wizardStep === WizardSteps.ProviderInputs &&
selectedProvider === ExternalKmsProvider.AWS && (
<motion.div
key="kms-aws"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<AwsKmsForm onCancel={() => onToggle(false)} onCompleted={() => onToggle(false)} />
</motion.div>
)}
</AnimatePresence>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,273 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useAddExternalKms, useUpdateExternalKms } from "@app/hooks/api";
import {
AddExternalKmsSchema,
AddExternalKmsType,
ExternalKmsProvider,
Kms,
KmsAwsCredentialType
} from "@app/hooks/api/kms/types";
const AWS_REGIONS = [
{ name: "US East (Ohio)", slug: "us-east-2" },
{ name: "US East (N. Virginia)", slug: "us-east-1" },
{ name: "US West (N. California)", slug: "us-west-1" },
{ name: "US West (Oregon)", slug: "us-west-2" },
{ name: "Africa (Cape Town)", slug: "af-south-1" },
{ name: "Asia Pacific (Hong Kong)", slug: "ap-east-1" },
{ name: "Asia Pacific (Hyderabad)", slug: "ap-south-2" },
{ name: "Asia Pacific (Jakarta)", slug: "ap-southeast-3" },
{ name: "Asia Pacific (Melbourne)", slug: "ap-southeast-4" },
{ name: "Asia Pacific (Mumbai)", slug: "ap-south-1" },
{ name: "Asia Pacific (Osaka)", slug: "ap-northeast-3" },
{ name: "Asia Pacific (Seoul)", slug: "ap-northeast-2" },
{ name: "Asia Pacific (Singapore)", slug: "ap-southeast-1" },
{ name: "Asia Pacific (Sydney)", slug: "ap-southeast-2" },
{ name: "Asia Pacific (Tokyo)", slug: "ap-northeast-1" },
{ name: "Canada (Central)", slug: "ca-central-1" },
{ name: "Europe (Frankfurt)", slug: "eu-central-1" },
{ name: "Europe (Ireland)", slug: "eu-west-1" },
{ name: "Europe (London)", slug: "eu-west-2" },
{ name: "Europe (Milan)", slug: "eu-south-1" },
{ name: "Europe (Paris)", slug: "eu-west-3" },
{ name: "Europe (Spain)", slug: "eu-south-2" },
{ name: "Europe (Stockholm)", slug: "eu-north-1" },
{ name: "Europe (Zurich)", slug: "eu-central-2" },
{ name: "Middle East (Bahrain)", slug: "me-south-1" },
{ name: "Middle East (UAE)", slug: "me-central-1" },
{ name: "South America (Sao Paulo)", slug: "sa-east-1" },
{ name: "AWS GovCloud (US-East)", slug: "us-gov-east-1" },
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" }
];
type Props = {
onCompleted: () => void;
onCancel: () => void;
kms?: Kms;
};
export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
const {
control,
handleSubmit,
watch,
setValue,
formState: { isSubmitting }
} = useForm<AddExternalKmsType>({
resolver: zodResolver(AddExternalKmsSchema),
defaultValues: {
slug: kms?.slug,
description: kms?.description ?? "",
provider: {
type: ExternalKmsProvider.AWS,
inputs: {
credential: {
type: kms?.external?.providerInput?.credential?.type,
data: {
accessKey: kms?.external?.providerInput?.credential?.data?.accessKey,
secretKey: kms?.external?.providerInput?.credential?.data?.secretKey,
assumeRoleArn: kms?.external?.providerInput?.credential?.data?.assumeRoleArn,
externalId: kms?.external?.providerInput?.credential?.data?.externalId
}
},
awsRegion: kms?.external?.providerInput?.awsRegion,
kmsKeyId: kms?.external?.providerInput?.kmsKeyId
}
}
}
});
const { currentOrg } = useOrganization();
const { mutateAsync: addAwsExternalKms } = useAddExternalKms(currentOrg?.id!);
const { mutateAsync: updateAwsExternalKms } = useUpdateExternalKms(currentOrg?.id!);
const selectedAwsAuthType = watch("provider.inputs.credential.type");
const handleAddAwsKms = async (data: AddExternalKmsType) => {
const { slug, description, provider } = data;
try {
if (kms) {
await updateAwsExternalKms({
kmsId: kms.id,
slug,
description,
provider
});
createNotification({
text: "Successfully updated AWS External KMS",
type: "success"
});
} else {
await addAwsExternalKms({
slug,
description,
provider
});
createNotification({
text: "Successfully added AWS External KMS",
type: "success"
});
}
onCompleted();
} catch (err) {
console.error(err);
}
};
return (
<form onSubmit={handleSubmit(handleAddAwsKms)} autoComplete="off">
<Controller
control={control}
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.inputs.credential.type"
defaultValue={KmsAwsCredentialType.AssumeRole}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Authentication Mode"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue("provider.inputs.credential.data.accessKey", "");
setValue("provider.inputs.credential.data.secretKey", "");
setValue("provider.inputs.credential.data.assumeRoleArn", "");
setValue("provider.inputs.credential.data.externalId", "");
onChange(e);
}}
className="w-full"
>
<SelectItem value={KmsAwsCredentialType.AssumeRole}>AWS Assume Role</SelectItem>
<SelectItem value={KmsAwsCredentialType.AccessKey}>Access Key</SelectItem>
</Select>
</FormControl>
)}
/>
{selectedAwsAuthType === KmsAwsCredentialType.AccessKey ? (
<>
<Controller
control={control}
name="provider.inputs.credential.data.accessKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Key ID"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.inputs.credential.data.secretKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Access Key"
errorText={error?.message}
isError={Boolean(error)}
>
<Input type="password" autoComplete="new-password" placeholder="" {...field} />
</FormControl>
)}
/>
</>
) : (
<>
<Controller
control={control}
name="provider.inputs.credential.data.assumeRoleArn"
render={({ field, fieldState: { error } }) => (
<FormControl
label="IAM Role ARN For Role Assumption"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.inputs.credential.data.externalId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Assume Role External ID"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
name="provider.inputs.awsRegion"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="AWS Region" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500"
>
{AWS_REGIONS.map((awsRegion) => (
<SelectItem value={awsRegion.slug} key={`kms-aws-region-${awsRegion.slug}`}>
{awsRegion.name} ({awsRegion.slug})
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.inputs.kmsKeyId"
render={({ field, fieldState: { error } }) => (
<FormControl label="AWS KMS Key ID" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<div className="mt-6 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Save
</Button>
<Button variant="outline_bg" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
);
};
@@ -0,0 +1,218 @@
import { faAws } from "@fortawesome/free-brands-svg-icons";
import { faEllipsis, faLock, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
THead,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useGetExternalKmsList, useRemoveExternalKms } from "@app/hooks/api";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AddExternalKmsForm } from "./AddExternalKmsForm";
import { UpdateExternalKmsForm } from "./UpdateExternalKmsForm";
export const OrgEncryptionTab = withPermission(
() => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upgradePlan",
"addExternalKms",
"editExternalKms",
"removeExternalKms"
] as const);
const { data: externalKmsList, isLoading: isExternalKmsListLoading } =
useGetExternalKmsList(orgId);
const { mutateAsync: removeExternalKms } = useRemoveExternalKms(currentOrg?.id!);
const handleRemoveExternalKms = async () => {
const { kmsId } = popUp?.removeExternalKms?.data as {
kmsId: string;
};
try {
await removeExternalKms(kmsId);
createNotification({
text: "Successfully deleted external KMS",
type: "success"
});
handlePopUpToggle("removeExternalKms", false);
} catch (err) {
console.error(err);
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Key Management System (KMS)</p>
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Kms}>
{(isAllowed) => (
<Button
onClick={() => {
if (subscription && !subscription?.externalKms) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("addExternalKms");
}}
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add
</Button>
)}
</OrgPermissionCan>
</div>
<p className="mb-4 text-gray-400">
Encrypt your organization&apos;s data with external KMS.
</p>
<TableContainer>
<Table>
<THead>
<Tr>
<Td>Provider</Td>
<Td>Alias</Td>
</Tr>
</THead>
<TBody>
{isExternalKmsListLoading && <TableSkeleton columns={2} innerKey="kms-loading" />}
{!isExternalKmsListLoading && externalKmsList && externalKmsList?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No external KMS found" icon={faLock} />
</Td>
</Tr>
)}
{!isExternalKmsListLoading &&
externalKmsList?.map((kms) => (
<Tr key={kms.id}>
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{kms.externalKms.provider === ExternalKmsProvider.AWS && (
<FontAwesomeIcon icon={faAws} />
)}
<div className="ml-2">{kms.externalKms.provider.toUpperCase()}</div>
</Td>
<Td>{kms.slug}</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
an={OrgPermissionSubjects.Kms}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
if (subscription && !subscription?.externalKms) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("editExternalKms", {
kmsId: kms.id
});
}}
>
Edit
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
an={OrgPermissionSubjects.Kms}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeExternalKms", {
slug: kms.slug,
kmsId: kms.id,
provider: kms.externalKms.provider
});
}}
>
Delete
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can configure external KMS if you switch to Infisical's Enterprise plan."
/>
<AddExternalKmsForm
isOpen={popUp.addExternalKms.isOpen}
onToggle={(state) => handlePopUpToggle("addExternalKms", state)}
/>
<UpdateExternalKmsForm
isOpen={popUp.editExternalKms.isOpen}
kmsId={(popUp.editExternalKms.data as { kmsId: string })?.kmsId}
onOpenChange={(state) => handlePopUpToggle("editExternalKms", state)}
/>
<DeleteActionModal
isOpen={popUp.removeExternalKms.isOpen}
title={`Are you sure want to remove ${
(popUp?.removeExternalKms?.data as { slug: string })?.slug || ""
} from ${(popUp?.removeExternalKms?.data as { provider: string })?.provider || ""}?`}
onChange={(isOpen) => handlePopUpToggle("removeExternalKms", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleRemoveExternalKms}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Kms }
);
@@ -0,0 +1,29 @@
import { ContentLoader, Modal, ModalContent } from "@app/components/v2";
import { useGetExternalKmsById } from "@app/hooks/api";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AwsKmsForm } from "./AwsKmsForm";
type Props = {
isOpen: boolean;
kmsId: string;
onOpenChange: (state: boolean) => void;
};
export const UpdateExternalKmsForm = ({ isOpen, kmsId, onOpenChange }: Props) => {
const { data: externalKms, isLoading } = useGetExternalKmsById(kmsId);
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Edit configuration">
{isLoading && <ContentLoader />}
{externalKms?.external?.provider === ExternalKmsProvider.AWS && (
<AwsKmsForm
kms={externalKms}
onCancel={() => onOpenChange(false)}
onCompleted={() => onOpenChange(false)}
/>
)}
</ModalContent>
</Modal>
);
};
@@ -0,0 +1 @@
export { OrgEncryptionTab } from "./OrgEncryptionTab";
@@ -3,11 +3,13 @@ import { Tab } from "@headlessui/react";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
import { OrgAuthTab } from "../OrgAuthTab";
import { OrgEncryptionTab } from "../OrgEncryptionTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
const tabs = [
{ name: "General", key: "tab-org-general" },
{ name: "Security", key: "tab-org-security" },
{ name: "Encryption", key: "tab-org-encryption" },
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
];
export const OrgTabGroup = () => {
@@ -19,8 +21,9 @@ export const OrgTabGroup = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
@@ -35,6 +38,9 @@ export const OrgTabGroup = () => {
<Tab.Panel>
<OrgAuthTab />
</Tab.Panel>
<Tab.Panel>
<OrgEncryptionTab />
</Tab.Panel>
<Tab.Panel>
<AuditLogStreamsTab />
</Tab.Panel>
@@ -2,16 +2,26 @@ import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { Tab } from "@headlessui/react";
import { useWorkspace } from "@app/context";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { EncryptionTab } from "./components/EncryptionTab";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { WebhooksTab } from "./components/WebhooksTab";
const tabs = [
{ name: "General", key: "tab-project-general" },
{ name: "Webhooks", key: "tab-project-webhooks" },
];
export const ProjectSettingsPage = () => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const tabs = [
{ name: "General", key: "tab-project-general" },
{
name: "Encryption",
key: "tab-project-encryption",
isHidden: currentWorkspace?.version !== ProjectVersion.V3
},
{ name: "Webhooks", key: "tab-project-webhooks" }
];
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6">
@@ -20,24 +30,32 @@ export const ProjectSettingsPage = () => {
</div>
<Tab.Group>
<Tab.List className="mb-4 w-full border-b-2 border-mineshaft-800">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
{tabs
.filter((el) => !el.isHidden)
.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
)}
</Tab>
))}
>
{tab.name}
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<ProjectGeneralTab />
</Tab.Panel>
{currentWorkspace?.version === ProjectVersion.V3 && (
<Tab.Panel>
<EncryptionTab />
</Tab.Panel>
)}
<Tab.Panel>
<WebhooksTab />
</Tab.Panel>
@@ -1,38 +0,0 @@
import Link from "next/link";
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
import { useWorkspace } from "@app/context";
import { useGetWorkspaceBot } from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
export const E2EESection = () => {
const { currentWorkspace } = useWorkspace();
const { data: bot } = useGetWorkspaceBot(currentWorkspace?.id ?? "");
if (!currentWorkspace) return null;
return bot && currentWorkspace.version === ProjectVersion.V1 ? (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">End-to-End Encryption</p>
<UpgradeProjectAlert transparent project={currentWorkspace} />
</div>
<p className="mt-5 max-w-2xl text-sm text-gray-400">
We are updating our encryption logic to make sure that Infisical can be the most versatile
secret management platform. <br />
<br />
Upgrading the project version is required to continue receiving the latest improvements and
patches.
</p>
<Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
<a target="_blank" className="text-sm text-primary-400">
Learn more about project upgrades
</a>
</Link>
</div>
) : (
<div />
);
};
@@ -1 +0,0 @@
export { E2EESection } from "./E2EESection";
@@ -0,0 +1,352 @@
import { ChangeEvent, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import FileSaver from "file-saver";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
FormControl,
IconButton,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace
} from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useGetActiveProjectKms,
useGetExternalKmsList,
useLoadProjectKmsBackup,
useUpdateProjectKms
} from "@app/hooks/api";
import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries";
import { INTERNAL_KMS_KEY_ID, KmsType } from "@app/hooks/api/kms/types";
import { Organization, Workspace } from "@app/hooks/api/types";
const formSchema = z.object({
kmsKeyId: z.string()
});
type TForm = z.infer<typeof formSchema>;
const BackupConfirmationModal = ({
isOpen,
onOpenChange,
org,
workspace
}: {
isOpen: boolean;
onOpenChange: (state: boolean) => void;
org?: Organization;
workspace?: Workspace;
}) => {
const [isGeneratingBackup, setGeneratingBackup] = useToggle();
const downloadKmsBackup = async () => {
if (!workspace || !org) {
return;
}
setGeneratingBackup.on();
try {
const { secretManager } = await fetchProjectKmsBackup(workspace.id);
const [, , kmsFunction] = secretManager.split(".");
const file = secretManager;
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(
blob,
`kms-backup-${org.slug}-${workspace.slug}-${kmsFunction}.infisical.txt`
);
onOpenChange(false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create KMS backup",
type: "error"
});
}
setGeneratingBackup.off();
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Create KMS backup">
<p className="mb-6 text-bunker-300">
In case of interruptions with your configured external KMS, you can load a backup to set
the project&apos;s KMS back to the default Infisical KMS.
</p>
<Button onClick={downloadKmsBackup} isLoading={isGeneratingBackup}>
Generate
</Button>
<Button
onClick={() => onOpenChange(false)}
colorSchema="secondary"
variant="plain"
className="ml-4"
>
Cancel
</Button>
</ModalContent>
</Modal>
);
};
const LoadBackupModal = ({
isOpen,
onOpenChange,
org,
workspace
}: {
isOpen: boolean;
onOpenChange: (state: boolean) => void;
org?: Organization;
workspace?: Workspace;
}) => {
const fileUploadRef = useRef<HTMLInputElement>(null);
const { mutateAsync: loadKmsBackup, isLoading } = useLoadProjectKmsBackup(workspace?.id!);
const [backupContent, setBackupContent] = useState("");
const [backupFileName, setBackupFileName] = useState("");
const uploadKmsBackup = async () => {
if (!workspace || !org) {
return;
}
try {
await loadKmsBackup(backupContent);
createNotification({
text: "Successfully loaded KMS backup",
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
}
};
const parseFile = (file?: File) => {
if (!file) {
createNotification({
text: "Failed to parse uploaded file.",
type: "error"
});
return;
}
const reader = new FileReader();
reader.onload = (event) => {
if (!event?.target?.result) return;
const data = event.target.result.toString();
setBackupContent(data);
};
try {
reader.readAsText(file);
} catch (error) {
console.log(error);
}
};
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
parseFile(e.target?.files?.[0]);
setBackupFileName(e.target?.files?.[0]?.name || "");
};
return (
<Modal
isOpen={isOpen}
onOpenChange={(state: boolean) => {
setBackupContent("");
setBackupFileName("");
onOpenChange(state);
}}
>
<ModalContent title="Load KMS backup">
<p className="mb-6 text-bunker-300">
By loading a backup, the project&apos;s KMS will be switched to the default Infisical KMS.
</p>
<div className="flex justify-center">
<input
id="fileSelect"
className="hidden"
type="file"
accept=".txt"
onChange={handleFileUpload}
ref={fileUploadRef}
/>
<IconButton
className="p-10"
ariaLabel="upload-backup"
colorSchema="secondary"
onClick={() => {
fileUploadRef?.current?.click();
}}
>
<FontAwesomeIcon icon={faUpload} size="3x" />
</IconButton>
</div>
{backupFileName && (
<div className="mt-2 flex justify-center px-4 text-center">{backupFileName}</div>
)}
{backupContent && (
<Button
onClick={uploadKmsBackup}
className="mt-10 w-fit"
disabled={isLoading}
isLoading={isLoading}
>
Continue
</Button>
)}
</ModalContent>
</Modal>
);
};
export const EncryptionTab = () => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
const { data: activeKms } = useGetActiveProjectKms(currentWorkspace?.id!);
const { mutateAsync: updateProjectKms, isLoading: isUpdatingProjectKms } = useUpdateProjectKms(
currentWorkspace?.id!
);
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([
"createBackupConfirmation",
"loadBackup"
] as const);
const kmsKeyId = activeKms?.isExternal ? activeKms.id : INTERNAL_KMS_KEY_ID;
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = useForm<TForm>({
resolver: zodResolver(formSchema),
values: {
kmsKeyId
}
});
const onUpdateProjectKms = async (data: TForm) => {
try {
await updateProjectKms(
data.kmsKeyId === INTERNAL_KMS_KEY_ID
? { type: KmsType.Internal }
: { type: KmsType.External, kmsId: data.kmsKeyId }
);
createNotification({
text: "Successfully updated project KMS",
type: "success"
});
} catch (err) {
console.error(err);
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Key Management</h2>
{kmsKeyId !== INTERNAL_KMS_KEY_ID && (
<div className="space-x-2">
<Button colorSchema="secondary" onClick={() => handlePopUpOpen("loadBackup")}>
Load Backup
</Button>
<Button
colorSchema="secondary"
onClick={() => {
handlePopUpOpen("createBackupConfirmation");
}}
>
Create Backup
</Button>
</div>
)}
</div>
<p className="mb-4 text-gray-400">
Select which Key Management System to use for encrypting your project data
</p>
<div className="mb-6 max-w-md">
<form onSubmit={handleSubmit(onUpdateProjectKms)}>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Kms}>
{(isAllowed) => (
<Controller
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<Select
{...field}
isDisabled={!isAllowed || isUpdatingProjectKms}
onValueChange={onChange}
isLoading={isUpdatingProjectKms}
className="w-3/4 bg-mineshaft-600"
>
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
Default Infisical KMS
</SelectItem>
{externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="kmsKeyId"
/>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Workspace}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
isDisabled={!isAllowed || isSubmitting || !isDirty}
isLoading={isSubmitting}
>
Save
</Button>
)}
</ProjectPermissionCan>
</form>
</div>
<BackupConfirmationModal
isOpen={popUp.createBackupConfirmation.isOpen}
onOpenChange={(state: boolean) => handlePopUpToggle("createBackupConfirmation", state)}
org={currentOrg}
workspace={currentWorkspace}
/>
<LoadBackupModal
isOpen={popUp.loadBackup.isOpen}
onOpenChange={(state: boolean) => handlePopUpToggle("loadBackup", state)}
org={currentOrg}
workspace={currentWorkspace}
/>
</div>
);
};
@@ -0,0 +1 @@
export { EncryptionTab } from "./EncryptionTab";
@@ -2,7 +2,6 @@ import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { DeleteProjectSection } from "../DeleteProjectSection";
import { E2EESection } from "../E2EESection";
import { EnvironmentSection } from "../EnvironmentSection";
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
import { ProjectNameChangeSection } from "../ProjectNameChangeSection";
@@ -16,7 +15,6 @@ export const ProjectGeneralTab = () => {
<EnvironmentSection />
<SecretTagsSection />
<AutoCapitalizationSection />
<E2EESection />
<PointInTimeVersionLimitSection />
<AuditLogsRetentionSection />
<BackfillSecretReferenceSecretion />
@@ -1,7 +1,6 @@
export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
export { DeleteProjectSection } from "./DeleteProjectSection";
export { E2EESection } from "./E2EESection";
export { EnvironmentSection } from "./EnvironmentSection";
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
export { SecretTagsSection } from "./SecretTagsSection";