Merge branch 'Infisical:main' into feat/create-multiple-orgs-under-same-account

This commit is contained in:
Ronit Panda
2023-10-24 05:42:31 +05:30
committed by GitHub
126 changed files with 2887 additions and 1714 deletions
@@ -1,6 +1,6 @@
import crypto from "crypto";
import { TCloudIntegration,UserWsKeyPair } from "@app/hooks/api/types";
import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types";
import {
decryptAssymmetric,
@@ -32,11 +32,10 @@ export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) =
export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
let link = "";
switch (integrationOption.slug) {
case "gcp-secret-manager":
@@ -123,6 +122,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "teamcity":
link = `${window.location.origin}/integrations/teamcity/authorize`;
break;
case "hasura-cloud":
link = `${window.location.origin}/integrations/hasura-cloud/authorize`;
break;
default:
break;
}
@@ -130,7 +132,6 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
@@ -202,6 +202,8 @@ export const IntegrationsPage = withProjectPermission(
integrations={integrations}
environments={environments}
onIntegrationDelete={({ _id: id }, cb) => handleIntegrationDelete(id, cb)}
isBotActive={bot?.isActive}
workspaceId={workspaceId}
/>
<CloudIntegrationSection
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
@@ -1,9 +1,12 @@
import Link from "next/link";
import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Alert,
AlertDescription,
DeleteActionModal,
EmptyState,
FormControl,
@@ -23,17 +26,22 @@ type Props = {
integrations?: TIntegration[];
isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
isBotActive: boolean | undefined;
workspaceId: string;
};
export const IntegrationsSection = ({
integrations = [],
environments = [],
isLoading,
onIntegrationDelete
onIntegrationDelete,
isBotActive,
workspaceId
}: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
return (
<div className="mb-8">
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
@@ -45,7 +53,22 @@ export const IntegrationsSection = ({
<Skeleton className="h-28" />
</div>
)}
{!isLoading && !integrations.length && (
{!isBotActive && (
<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 && (
<div className="mx-6">
<EmptyState
className="rounded-md border border-mineshaft-700 pt-8 pb-4"
@@ -53,7 +76,7 @@ export const IntegrationsSection = ({
/>
</div>
)}
{!isLoading && (
{!isLoading && isBotActive && (
<div className="flex flex-col space-y-4 p-6 pt-0">
{integrations?.map((integration) => (
<div
@@ -153,6 +153,7 @@ export const SecretListView = ({
workspaceId,
secretPath,
secretName: key,
secretId,
secretValue: value || "",
type,
latestFileKey: decryptFileKey,
@@ -201,11 +202,14 @@ export const SecretListView = ({
try {
// personal secret change
if (overrideAction === "deleted") {
await handleSecretOperation("delete", "personal", oldKey);
await handleSecretOperation("delete", "personal", oldKey, {
secretId: orgSecret.idOverride
});
} else if (overrideAction && idOverride) {
await handleSecretOperation("update", "personal", oldKey, {
value: valueOverride,
newKey: hasKeyChanged ? key : undefined,
secretId: orgSecret.idOverride,
skipMultilineEncoding: modSecret.skipMultilineEncoding
});
} else if (overrideAction) {
@@ -218,6 +222,7 @@ export const SecretListView = ({
value,
tags: tagIds,
comment,
secretId: orgSecret._id,
newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding
});
@@ -308,7 +313,6 @@ export const SecretListView = ({
>
{namespace}
</div>
{filteredSecrets.map((secret) => (
<SecretItem
environment={environment}
@@ -146,12 +146,13 @@ export const SecretOverviewPage = () => {
}
};
const handleSecretUpdate = async (env: string, key: string, value: string) => {
const handleSecretUpdate = async (env: string, key: string, value: string, secretId?: string) => {
try {
await updateSecretV3({
environment: env,
workspaceId,
secretPath,
secretId,
secretName: key,
secretValue: value,
type: "shared",
@@ -242,7 +243,6 @@ export const SecretOverviewPage = () => {
);
const canViewOverviewPage = Boolean(userAvailableEnvs.length);
const filteredSecretNames = secKeys
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
@@ -18,7 +18,7 @@ type Props = {
environment: string;
secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
};
@@ -42,7 +42,7 @@ export const SecretEditRow = ({
formState: { isDirty, isSubmitting }
} = useForm({
values: {
value: defaultValue
value: defaultValue || null
}
});
const [isDeleting, setIsDeleting] = useToggle();
@@ -70,7 +70,7 @@ export const SecretEditRow = ({
if (isCreatable) {
await onSecretCreate(environment, secretName, value);
} else {
await onSecretUpdate(environment, secretName, value);
await onSecretUpdate(environment, secretName, value, secretId);
}
}
reset({ value });
@@ -80,7 +80,7 @@ export const SecretEditRow = ({
setIsDeleting.on();
try {
await onSecretDelete(environment, secretName, secretId);
reset({ value: undefined });
reset({ value: null });
} finally {
setIsDeleting.off();
}
@@ -23,7 +23,7 @@ type Props = {
expandableColWidth: number;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
};
@@ -143,8 +143,8 @@ export const SSOModal = ({
entityId: "Identifier (Entity ID)",
entryPoint: "Login URL",
entryPointPlaceholder: "https://login.microsoftonline.com/xxx/saml2",
issuer: "Azure AD Identifier",
issuerPlaceholder: "https://sts.windows.net/xxx/"
issuer: "Azure Application ID",
issuerPlaceholder: "abc-def-ghi-jkl-mno"
});
case AuthProvider.JUMPCLOUD_SAML:
return ({
@@ -0,0 +1,199 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
IconButton,
Input,
Modal,
ModalContent} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import {
useCreateAPIKeyV2,
useUpdateAPIKeyV2
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({
name: yup.string().required("API Key V2 name is required")
}).required();
export type FormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["apiKeyV2"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["apiKeyV2"]>, state?: boolean) => void;
};
export const APIKeyV2Modal = ({
popUp,
handlePopUpToggle
}: Props) => {
const [newAPIKey, setNewAPIKey] = useState("");
const [isAPIKeyCopied, setIsAPIKeyCopied] = useToggle(false);
const { createNotification } = useNotificationContext();
const { mutateAsync: createMutateAsync } = useCreateAPIKeyV2();
const { mutateAsync: updateMutateAsync } = useUpdateAPIKeyV2();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
name: ""
}
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isAPIKeyCopied) {
timer = setTimeout(() => setIsAPIKeyCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [setIsAPIKeyCopied]);
useEffect(() => {
const apiKeyData = popUp?.apiKeyV2?.data as {
apiKeyDataId: string;
name: string;
};
if (apiKeyData) {
reset({
name: apiKeyData.name
});
} else {
reset({
name: ""
});
}
}, [popUp?.apiKeyV2?.data]);
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(newAPIKey);
setIsAPIKeyCopied.on();
};
const onFormSubmit = async ({
name
}: FormData) => {
try {
const apiKeyData = popUp?.apiKeyV2?.data as {
apiKeyDataId: string;
name: string;
};
if (apiKeyData) {
// update
await updateMutateAsync({
apiKeyDataId: apiKeyData.apiKeyDataId,
name
});
handlePopUpToggle("apiKeyV2", false);
} else {
// create
const { apiKey } = await createMutateAsync({
name
});
setNewAPIKey(apiKey);
}
createNotification({
text: `Successfully ${popUp?.apiKeyV2?.data ? "updated" : "created"} API Key`,
type: "success"
});
reset();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${popUp?.apiKeyV2?.data ? "updated" : "created"} API Key`,
type: "error"
});
}
}
const hasAPIKey = Boolean(newAPIKey);
return (
<Modal
isOpen={popUp?.apiKeyV2?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("apiKeyV2", isOpen);
reset();
setNewAPIKey("");
}}
>
<ModalContent title={`${popUp?.apiKeyV2?.data ? "Update" : "Create"} API Key V2`}>
{!hasAPIKey ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="My API Key"
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.apiKeyV2?.data ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
) : (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{newAPIKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isAPIKeyCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Click to copy
</span>
</IconButton>
</div>
)}
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,81 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal
} from "@app/components/v2";
import { useDeleteAPIKeyV2 } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { APIKeyV2Modal } from "./APIKeyV2Modal";
import { APIKeyV2Table } from "./APIKeyV2Table";
export const APIKeyV2Section = () => {
const { createNotification } = useNotificationContext();
const { mutateAsync: deleteMutateAsync } = useDeleteAPIKeyV2();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"apiKeyV2",
"deleteAPIKeyV2"
] as const);
const onDeleteAPIKeyDataSubmit = async (apiKeyDataId: string) => {
try {
await deleteMutateAsync({
apiKeyDataId
});
createNotification({
text: "Successfully deleted API Key V2",
type: "success"
});
handlePopUpClose("deleteAPIKeyV2");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete API Key V2",
type: "error"
});
}
}
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">
API Keys V2 (Beta)
</p>
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("apiKeyV2")}
>
Add API Key
</Button>
</div>
<APIKeyV2Table
handlePopUpOpen={handlePopUpOpen}
/>
<APIKeyV2Modal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteAPIKeyV2.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteAPIKeyV2?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteAPIKeyV2", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteAPIKeyDataSubmit(
(popUp?.deleteAPIKeyV2?.data as { apiKeyDataId: string })?.apiKeyDataId
)
}
/>
</div>
);
}
@@ -0,0 +1,107 @@
import { faKey, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import {
useGetMyAPIKeysV2
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteAPIKeyV2", "apiKeyV2"]>,
data?: {
apiKeyDataId?: string;
name?: string;
}
) => void;
};
export const APIKeyV2Table = ({
handlePopUpOpen
}: Props) => {
const { data, isLoading } = useGetMyAPIKeysV2();
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="">Name</Th>
<Th className="">Last Used</Th>
<Th className="">Created At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="api-keys-v2" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({
_id,
name,
lastUsed,
createdAt
}) => {
return (
<Tr className="h-10" key={`api-key-v2-${_id}`}>
<Td>{name}</Td>
<Td>{lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end">
<IconButton
onClick={async () => {
handlePopUpOpen("apiKeyV2", {
apiKeyDataId: _id,
name
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => {
handlePopUpOpen("deleteAPIKeyV2", {
apiKeyDataId: _id
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No API key v2 on file" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1 @@
export { APIKeyV2Section } from "./APIKeyV2Section";
@@ -1,7 +1,11 @@
// import { APIKeyV2Section } from "../APIKeyV2Section";
import { APIKeySection } from "../APIKeySection";
export const PersonalAPIKeyTab = () => {
return (
<APIKeySection />
<>
{/* <APIKeyV2Section /> */}
<APIKeySection />
</>
);
}
@@ -3,7 +3,7 @@ import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Checkbox } from "@app/components/v2";
import { Alert, AlertDescription, Checkbox } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
@@ -76,30 +76,39 @@ export const E2EESection = () => {
};
return bot ? (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
<p className="text-gray-400 mb-8">
<p className="mb-8 text-gray-400">
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
read/decrypt your secret values.
</p>
<p className="text-gray-400 mb-8">
<p className="mb-8 text-gray-400">
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={!bot.isActive}
isDisabled={!isAllowed}
onCheckedChange={async () => {
await toggleBotActivate();
}}
>
End-to-end encryption enabled
</Checkbox>
<div className="flex w-full flex-col gap-y-3">
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={!bot.isActive}
isDisabled={!isAllowed}
onCheckedChange={async () => {
await toggleBotActivate();
}}
>
End-to-end encryption enabled
</Checkbox>
</div>
<div>
<Alert variant="warning">
<AlertDescription>
Enabling End-to-end encryption disables all the integrations
</AlertDescription>
</Alert>
</div>
</div>
)}
</ProjectPermissionCan>