Compare commits

..

4 Commits

13 changed files with 557 additions and 678 deletions

View File

@ -5,6 +5,9 @@ export const mockSmtpServer = (): TSmtpService => {
return { return {
sendMail: async (data) => { sendMail: async (data) => {
storage.push(data); storage.push(data);
},
verify: async () => {
return true;
} }
}; };
}; };

View File

@ -17,7 +17,7 @@ import {
infisicalSymmetricDecrypt, infisicalSymmetricDecrypt,
infisicalSymmetricEncypt infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption"; } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TokenType } from "@app/services/auth-token/auth-token-types";
@ -56,7 +56,7 @@ type TOidcConfigServiceFactoryDep = {
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">; licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">; tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail" | "verify">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">; oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
}; };
@ -223,6 +223,7 @@ export const oidcConfigServiceFactory = ({
let newUser: TUsers | undefined; let newUser: TUsers | undefined;
if (serverCfg.trustOidcEmails) { if (serverCfg.trustOidcEmails) {
// we prioritize getting the most complete user to create the new alias under
newUser = await userDAL.findOne( newUser = await userDAL.findOne(
{ {
email, email,
@ -230,6 +231,23 @@ export const oidcConfigServiceFactory = ({
}, },
tx tx
); );
if (!newUser) {
// this fetches user entries created via invites
newUser = await userDAL.findOne(
{
username: email
},
tx
);
if (newUser && !newUser.isEmailVerified) {
// we automatically mark it as email-verified because we've configured trust for OIDC emails
newUser = await userDAL.updateById(newUser.id, {
isEmailVerified: true
});
}
}
} }
if (!newUser) { if (!newUser) {
@ -332,14 +350,20 @@ export const oidcConfigServiceFactory = ({
userId: user.id userId: user.id
}); });
await smtpService.sendMail({ await smtpService
template: SmtpTemplates.EmailVerification, .sendMail({
subjectLine: "Infisical confirmation code", template: SmtpTemplates.EmailVerification,
recipients: [user.email], subjectLine: "Infisical confirmation code",
substitutions: { recipients: [user.email],
code: token substitutions: {
} code: token
}); }
})
.catch((err: Error) => {
throw new OidcAuthError({
message: `Error sending email confirmation code for user registration - contact the Infisical instance admin. ${err.message}`
});
});
} }
return { isUserCompleted, providerAuthToken }; return { isUserCompleted, providerAuthToken };
@ -395,6 +419,18 @@ export const oidcConfigServiceFactory = ({
message: `Organization bot for organization with ID '${org.id}' not found`, message: `Organization bot for organization with ID '${org.id}' not found`,
name: "OrgBotNotFound" name: "OrgBotNotFound"
}); });
const serverCfg = await getServerCfg();
if (isActive && !serverCfg.trustOidcEmails) {
const isSmtpConnected = await smtpService.verify();
if (!isSmtpConnected) {
throw new BadRequestError({
message:
"Cannot enable OIDC when there are issues with the instance's SMTP configuration. Bypass this by turning on trust for OIDC emails in the server admin console."
});
}
}
const key = infisicalSymmetricDecrypt({ const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey, ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV, iv: orgBot.symmetricKeyIV,

View File

@ -133,3 +133,15 @@ export class ScimRequestError extends Error {
this.status = status; this.status = status;
} }
} }
export class OidcAuthError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
super(message || "Something went wrong");
this.name = name || "OidcAuthError";
this.error = error;
}
}

View File

@ -46,10 +46,10 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
await createTransport(smtpCfg) await createTransport(smtpCfg)
.verify() .verify()
.then(async () => { .then(async () => {
console.info("SMTP successfully connected"); console.info(`SMTP - Verified connection to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT}`);
}) })
.catch((err) => { .catch((err: Error) => {
console.error(`SMTP - Failed to connect to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT}`); console.error(`SMTP - Failed to connect to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT} - ${err.message}`);
logger.error(err); logger.error(err);
}); });

View File

@ -10,6 +10,7 @@ import {
GatewayTimeoutError, GatewayTimeoutError,
InternalServerError, InternalServerError,
NotFoundError, NotFoundError,
OidcAuthError,
RateLimitError, RateLimitError,
ScimRequestError, ScimRequestError,
UnauthorizedError UnauthorizedError
@ -83,7 +84,10 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
status: error.status, status: error.status,
detail: error.detail detail: error.detail
}); });
// Handle JWT errors and make them more human-readable for the end-user. } else if (error instanceof OidcAuthError) {
void res
.status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: error.message, error: error.name });
} else if (error instanceof jwt.JsonWebTokenError) { } else if (error instanceof jwt.JsonWebTokenError) {
const message = (() => { const message = (() => {
if (error.message === JWTErrors.JwtExpired) { if (error.message === JWTErrors.JwtExpired) {

View File

@ -77,5 +77,21 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => {
} }
}; };
return { sendMail }; const verify = async () => {
const isConnected = smtp
.verify()
.then(async () => {
logger.info("SMTP connected");
return true;
})
.catch((err: Error) => {
logger.error("SMTP error");
logger.error(err);
return false;
});
return isConnected;
};
return { sendMail, verify };
}; };

View File

@ -31,8 +31,7 @@ export const useCreateSecretV3 = ({
secretKey, secretKey,
secretValue, secretValue,
secretComment, secretComment,
skipMultilineEncoding, skipMultilineEncoding
tagIds
}) => { }) => {
const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, { const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, {
secretPath, secretPath,
@ -41,8 +40,7 @@ export const useCreateSecretV3 = ({
workspaceId, workspaceId,
secretValue, secretValue,
secretComment, secretComment,
skipMultilineEncoding, skipMultilineEncoding
tagIds
}); });
return data; return data;
}, },

View File

@ -132,7 +132,6 @@ export type TCreateSecretsV3DTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
type: SecretType; type: SecretType;
tagIds?: string[];
}; };
export type TUpdateSecretsV3DTO = { export type TUpdateSecretsV3DTO = {

View File

@ -1,5 +1,4 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Head from "next/head"; import Head from "next/head";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@ -11,12 +10,9 @@ import {
faCircleInfo faCircleInfo
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import queryString from "query-string"; import queryString from "query-string";
import z from "zod";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration } from "@app/hooks/api"; import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries"; import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types"; import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
@ -87,61 +83,10 @@ const mappingBehaviors = [
} }
]; ];
const schema = z
.object({
awsRegion: z.string().trim().min(1, { message: "AWS region is required" }),
secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }),
secretPrefix: z.string().default(""),
secretName: z.string().trim().min(1).optional(),
mappingBehavior: z.nativeEnum(IntegrationMappingBehavior),
kmsKeyId: z.string().optional(),
shouldTag: z.boolean().optional(),
tags: z
.object({
key: z.string(),
value: z.string()
})
.array()
})
.refine(
(val) =>
val.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE ||
(val.mappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE &&
val.secretName &&
val.secretName !== ""),
{
message: "Secret name must be defined for many-to-one integrations",
path: ["secretName"]
}
);
type TFormSchema = z.infer<typeof schema>;
export default function AWSSecretManagerCreateIntegrationPage() { export default function AWSSecretManagerCreateIntegrationPage() {
const router = useRouter(); const router = useRouter();
const { mutateAsync } = useCreateIntegration(); const { mutateAsync } = useCreateIntegration();
const {
control,
setValue,
handleSubmit,
watch,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(schema),
defaultValues: {
shouldTag: false,
secretPath: "/",
secretPrefix: "",
mappingBehavior: IntegrationMappingBehavior.MANY_TO_ONE,
tags: []
}
});
const shouldTagState = watch("shouldTag");
const selectedSourceEnvironment = watch("sourceEnvironment");
const selectedAWSRegion = watch("awsRegion");
const selectedMappingBehavior = watch("mappingBehavior");
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
@ -149,6 +94,25 @@ export default function AWSSecretManagerCreateIntegrationPage() {
(integrationAuthId as string) ?? "" (integrationAuthId as string) ?? ""
); );
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [selectedAWSRegion, setSelectedAWSRegion] = useState("");
const [selectedMappingBehavior, setSelectedMappingBehavior] = useState(
IntegrationMappingBehavior.MANY_TO_ONE
);
const [targetSecretName, setTargetSecretName] = useState("");
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState("");
const [tagKey, setTagKey] = useState("");
const [tagValue, setTagValue] = useState("");
const [kmsKeyId, setKmsKeyId] = useState("");
const [secretPrefix, setSecretPrefix] = useState("");
// const [path, setPath] = useState('');
// const [pathErrorText, setPathErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [shouldTag, setShouldTag] = useState(false);
const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } = const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } =
useGetIntegrationAuthAwsKmsKeys({ useGetIntegrationAuthAwsKmsKeys({
integrationAuthId: String(integrationAuthId), integrationAuthId: String(integrationAuthId),
@ -157,46 +121,63 @@ export default function AWSSecretManagerCreateIntegrationPage() {
useEffect(() => { useEffect(() => {
if (workspace) { if (workspace) {
setValue("sourceEnvironment", workspace.environments[0].slug); setSelectedSourceEnvironment(workspace.environments[0].slug);
setValue("awsRegion", awsRegions[0].slug); setSelectedAWSRegion(awsRegions[0].slug);
} }
}, [workspace]); }, [workspace]);
const handleButtonClick = async ({ // const isValidAWSPath = (path: string) => {
secretName, // const pattern = /^\/[\w./]+\/$/;
sourceEnvironment, // return pattern.test(path) && path.length <= 2048;
awsRegion, // }
secretPath,
shouldTag, const handleButtonClick = async () => {
tags,
secretPrefix,
kmsKeyId,
mappingBehavior
}: TFormSchema) => {
try { try {
if (!selectedMappingBehavior) {
return;
}
if (
selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE &&
targetSecretName.trim() === ""
) {
setTargetSecretName("Secret name cannot be blank");
return;
}
if (!integrationAuth?.id) return; if (!integrationAuth?.id) return;
setIsLoading(true);
await mutateAsync({ await mutateAsync({
integrationAuthId: integrationAuth?.id, integrationAuthId: integrationAuth?.id,
isActive: true, isActive: true,
app: secretName, app: targetSecretName.trim(),
sourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
region: awsRegion, region: selectedAWSRegion,
secretPath, secretPath,
metadata: { metadata: {
...(shouldTag ...(shouldTag
? { ? {
secretAWSTag: tags secretAWSTag: [
{
key: tagKey,
value: tagValue
}
]
} }
: {}), : {}),
...(secretPrefix && { secretPrefix }), ...(secretPrefix && { secretPrefix }),
...(kmsKeyId && { kmsKeyId }), ...(kmsKeyId && { kmsKeyId }),
mappingBehavior mappingBehavior: selectedMappingBehavior
} }
}); });
setIsLoading(false);
setTargetSecretNameErrorText("");
router.push(`/integrations/${localStorage.getItem("projectData.id")}`); router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) { } catch (err) {
setIsLoading(false);
console.error(err); console.error(err);
} }
}; };
@ -210,305 +191,226 @@ export default function AWSSecretManagerCreateIntegrationPage() {
<title>Set Up AWS Secrets Manager Integration</title> <title>Set Up AWS Secrets Manager Integration</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Head> </Head>
<form onSubmit={handleSubmit(handleButtonClick)}> <Card className="max-w-lg rounded-md border border-mineshaft-600">
<Card className="max-w-lg rounded-md border border-mineshaft-600"> <CardTitle
<CardTitle className="px-6 text-left text-xl"
className="px-6 text-left text-xl" subTitle="Choose which environment in Infisical you want to sync to secerts in AWS Secrets Manager."
subTitle="Choose which environment in Infisical you want to sync to secerts in AWS Secrets Manager." >
> <div className="flex flex-row items-center">
<div className="flex flex-row items-center"> <div className="inline flex items-center">
<div className="flex items-center"> <Image
<Image src="/images/integrations/Amazon Web Services.png"
src="/images/integrations/Amazon Web Services.png" height={35}
height={35} width={35}
width={35} alt="AWS logo"
alt="AWS logo" />
/>
</div>
<span className="ml-1.5">AWS Secrets Manager Integration </span>
<Link
href="https://infisical.com/docs/integrations/cloud/aws-secret-manager"
passHref
>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div> </div>
</CardTitle> <span className="ml-1.5">AWS Secrets Manager Integration </span>
<Tabs defaultValue={TabSections.Connection} className="px-6"> <Link href="https://infisical.com/docs/integrations/cloud/aws-secret-manager" passHref>
<TabList> <a target="_blank" rel="noopener noreferrer">
<div className="flex w-full flex-row border-b border-mineshaft-600"> <div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<Tab value={TabSections.Connection}>Connection</Tab> <FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<Tab value={TabSections.Options}>Options</Tab> Docs
</div> <FontAwesomeIcon
</TabList> icon={faArrowUpRightFromSquare}
<TabPanel value={TabSections.Connection}> className="ml-1.5 mb-[0.07rem] text-xxs"
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<Controller
control={control}
name="sourceEnvironment"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Environment"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
value={field.value}
onValueChange={(val) => {
field.onChange(val);
}}
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secrets Path"
errorText={error?.message}
isError={Boolean(error)}
>
<SecretPathInput {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="awsRegion"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="AWS region"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
>
{awsRegions.map((awsRegion) => (
<SelectItem
value={awsRegion.slug}
className="flex w-full justify-between"
key={`aws-environment-${awsRegion.slug}`}
>
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="mappingBehavior"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Mapping Behavior"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
>
{mappingBehaviors.map((option) => (
<SelectItem
value={option.value}
className="text-left"
key={`mapping-behavior-${option.value}`}
>
{option.label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && (
<Controller
control={control}
name="secretName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="AWS SM Secret Name"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
placeholder={`${workspace.name
.toLowerCase()
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
{...field}
/>
</FormControl>
)}
/>
)}
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: -30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mt-2 ml-1">
<Controller
control={control}
name="shouldTag"
render={({ field: { onChange, value } }) => (
<Switch
id="tag-aws"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
Tag in AWS Secrets Manager
</Switch>
)}
/> />
</div> </div>
{shouldTagState && ( </a>
<div className="mt-4 flex justify-between"> </Link>
<Controller
control={control}
name="tags.0.key"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tag Key"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="managed-by" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="tags.0.value"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tag Value"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="infisical" {...field} />
</FormControl>
)}
/>
</div>
)}
<Controller
control={control}
name="secretPrefix"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Prefix"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Input placeholder="INFISICAL_" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="kmsKeyId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Encryption Key"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
>
{integrationAuthAwsKmsKeys?.length ? (
integrationAuthAwsKmsKeys.map((key) => {
return (
<SelectItem
value={key.id as string}
key={`repo-id-${key.id}`}
className="w-[28.4rem] text-sm"
>
{key.alias}
</SelectItem>
);
})
) : (
<SelectItem isDisabled value="no-keys" key="no-keys">
No KMS keys available
</SelectItem>
)}
</Select>
</FormControl>
)}
/>
</motion.div>
</TabPanel>
</Tabs>
<Button
color="mineshaft"
variant="outline_bg"
type="submit"
className="mb-6 mt-2 ml-auto mr-6"
isLoading={isSubmitting}
>
Create Integration
</Button>
</Card>
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div> </div>
<span className="mt-4 text-sm text-mineshaft-300"> </CardTitle>
After creating an integration, your secrets will start syncing immediately. This might <Tabs defaultValue={TabSections.Connection} className="px-6">
cause an unexpected override of current secrets in AWS Secrets Manager with secrets from <TabList>
Infisical. <div className="flex w-full flex-row border-b border-mineshaft-600">
</span> <Tab value={TabSections.Connection}>Connection</Tab>
<Tab value={TabSections.Options}>Options</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Connection}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<FormControl label="Project Environment">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`flyio-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="AWS Region">
<Select
value={selectedAWSRegion}
onValueChange={(val) => {
setSelectedAWSRegion(val);
setKmsKeyId("");
}}
className="w-full border border-mineshaft-500"
>
{awsRegions.map((awsRegion) => (
<SelectItem
value={awsRegion.slug}
className="flex w-full justify-between"
key={`aws-environment-${awsRegion.slug}`}
>
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Mapping Behavior">
<Select
value={selectedMappingBehavior}
onValueChange={(val) => {
setSelectedMappingBehavior(val as IntegrationMappingBehavior);
}}
className="w-full border border-mineshaft-500 text-left"
>
{mappingBehaviors.map((option) => (
<SelectItem
value={option.value}
className="text-left"
key={`aws-environment-${option.value}`}
>
{option.label}
</SelectItem>
))}
</Select>
</FormControl>
{selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && (
<FormControl
label="AWS SM Secret Name"
errorText={targetSecretNameErrorText}
isError={targetSecretNameErrorText !== "" ?? false}
>
<Input
placeholder={`${workspace.name
.toLowerCase()
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
value={targetSecretName}
onChange={(e) => setTargetSecretName(e.target.value)}
/>
</FormControl>
)}
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: -30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mt-2 ml-1">
<Switch
id="tag-aws"
onCheckedChange={() => setShouldTag(!shouldTag)}
isChecked={shouldTag}
>
Tag in AWS Secrets Manager
</Switch>
</div>
{shouldTag && (
<div className="mt-4 flex justify-between">
<FormControl label="Tag Key">
<Input
placeholder="managed-by"
value={tagKey}
onChange={(e) => setTagKey(e.target.value)}
/>
</FormControl>
<FormControl label="Tag Value">
<Input
placeholder="infisical"
value={tagValue}
onChange={(e) => setTagValue(e.target.value)}
/>
</FormControl>
</div>
)}
<FormControl label="Secret Prefix" className="mt-4">
<Input
value={secretPrefix}
onChange={(e) => setSecretPrefix(e.target.value)}
placeholder="INFISICAL_"
/>
</FormControl>
<FormControl label="Encryption Key" className="mt-4">
<Select
value={kmsKeyId}
onValueChange={(e) => {
if (e === "no-keys") return;
setKmsKeyId(e);
}}
className="w-full border border-mineshaft-500"
>
{integrationAuthAwsKmsKeys?.length ? (
integrationAuthAwsKmsKeys.map((key) => {
return (
<SelectItem
value={key.id as string}
key={`repo-id-${key.id}`}
className="w-[28.4rem] text-sm"
>
{key.alias}
</SelectItem>
);
})
) : (
<SelectItem isDisabled value="no-keys" key="no-keys">
No KMS keys available
</SelectItem>
)}
</Select>
</FormControl>
</motion.div>
</TabPanel>
</Tabs>
<Button
onClick={handleButtonClick}
color="mineshaft"
variant="outline_bg"
className="mb-6 mt-2 ml-auto mr-6"
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div> </div>
</form> <span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might
cause an unexpected override of current secrets in AWS Secrets Manager with secrets from
Infisical.
</span>
</div>
</div> </div>
) : ( ) : (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">

View File

@ -9,14 +9,7 @@ import { twMerge } from "tailwind-merge";
import NavHeader from "@app/components/navigation/NavHeader"; import NavHeader from "@app/components/navigation/NavHeader";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { PermissionDeniedBanner } from "@app/components/permissions"; import { PermissionDeniedBanner } from "@app/components/permissions";
import { import { Checkbox, ContentLoader, Pagination, Tooltip } from "@app/components/v2";
Checkbox,
ContentLoader,
Modal,
ModalContent,
Pagination,
Tooltip
} from "@app/components/v2";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@ -48,10 +41,7 @@ import { SecretDropzone } from "./components/SecretDropzone";
import { SecretListView, SecretNoAccessListView } from "./components/SecretListView"; import { SecretListView, SecretNoAccessListView } from "./components/SecretListView";
import { SnapshotView } from "./components/SnapshotView"; import { SnapshotView } from "./components/SnapshotView";
import { import {
PopUpNames,
StoreProvider, StoreProvider,
usePopUpAction,
usePopUpState,
useSelectedSecretActions, useSelectedSecretActions,
useSelectedSecrets useSelectedSecrets
} from "./SecretMainPage.store"; } from "./SecretMainPage.store";
@ -133,9 +123,6 @@ const SecretMainPageContent = () => {
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter); const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map()); const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
const createSecretPopUp = usePopUpState(PopUpNames.CreateSecretForm);
const { togglePopUp } = usePopUpAction();
useEffect(() => { useEffect(() => {
if ( if (
!isWorkspaceLoading && !isWorkspaceLoading &&
@ -533,24 +520,13 @@ const SecretMainPageContent = () => {
onChangePerPage={(newPerPage) => setPerPage(newPerPage)} onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/> />
)} )}
<Modal <CreateSecretForm
isOpen={createSecretPopUp.isOpen} environment={environment}
onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)} workspaceId={workspaceId}
> secretPath={secretPath}
<ModalContent autoCapitalize={currentWorkspace?.autoCapitalization}
title="Create Secret" isProtectedBranch={isProtectedBranch}
subTitle="Add a secret to this particular environment and folder" />
bodyClassName="overflow-visible"
>
<CreateSecretForm
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
autoCapitalize={currentWorkspace?.autoCapitalization}
isProtectedBranch={isProtectedBranch}
/>
</ModalContent>
</Modal>
<SecretDropzone <SecretDropzone
secrets={secrets} secrets={secrets}
environment={environment} environment={environment}

View File

@ -1,24 +1,20 @@
import { ClipboardEvent } from "react"; import { ClipboardEvent } from "react";
import { Controller, useForm } from "react-hook-form"; 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 { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, MultiSelect } from "@app/components/v2"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { getKeyValue } from "@app/helpers/parseEnvVar"; import { getKeyValue } from "@app/helpers/parseEnvVar";
import { useCreateSecretV3, useGetWsTags } from "@app/hooks/api"; import { useCreateSecretV3 } from "@app/hooks/api";
import { SecretType } from "@app/hooks/api/types"; import { SecretType } from "@app/hooks/api/types";
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store"; import { PopUpNames, usePopUpAction, usePopUpState } from "../../SecretMainPage.store";
const typeSchema = z.object({ const typeSchema = z.object({
key: z.string().trim().min(1, { message: "Secret key is required" }), key: z.string().trim().min(1, { message: "Secret key is required" }),
value: z.string().optional(), value: z.string().optional()
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
}); });
type TFormSchema = z.infer<typeof typeSchema>; type TFormSchema = z.infer<typeof typeSchema>;
@ -47,16 +43,12 @@ export const CreateSecretForm = ({
setValue, setValue,
formState: { errors, isSubmitting } formState: { errors, isSubmitting }
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) }); } = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
const { closePopUp } = usePopUpAction(); const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm);
const { closePopUp, togglePopUp } = usePopUpAction();
const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { permission } = useProjectPermission();
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
canReadTags ? workspaceId : ""
);
const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => { const handleFormSubmit = async ({ key, value }: TFormSchema) => {
try { try {
await createSecretV3({ await createSecretV3({
environment, environment,
@ -65,8 +57,7 @@ export const CreateSecretForm = ({
secretKey: key, secretKey: key,
secretValue: value || "", secretValue: value || "",
secretComment: "", secretComment: "",
type: SecretType.Shared, type: SecretType.Shared
tagIds: tags?.map((el) => el.value)
}); });
closePopUp(PopUpNames.CreateSecretForm); closePopUp(PopUpNames.CreateSecretForm);
reset(); reset();
@ -97,90 +88,67 @@ export const CreateSecretForm = ({
}; };
return ( return (
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate> <Modal
<FormControl isOpen={isOpen}
label="Key" onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)}
isRequired >
isError={Boolean(errors?.key)} <ModalContent
errorText={errors?.key?.message} title="Create secret"
subTitle="Add a secret to the particular environment and folder"
> >
<Input <form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
{...register("key")}
placeholder="Type your secret name"
onPaste={handlePaste}
autoCapitalization={autoCapitalize}
/>
</FormControl>
<Controller
control={control}
name="value"
render={({ field }) => (
<FormControl <FormControl
label="Value" label="Key"
isError={Boolean(errors?.value)} isRequired
errorText={errors?.value?.message} isError={Boolean(errors?.key)}
errorText={errors?.key?.message}
> >
<InfisicalSecretInput <Input
{...field} {...register("key")}
environment={environment} placeholder="Type your secret name"
secretPath={secretPath} onPaste={handlePaste}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5" autoCapitalization={autoCapitalize}
/> />
</FormControl> </FormControl>
)} <Controller
/> control={control}
<Controller name="value"
control={control} render={({ field }) => (
name="tags" <FormControl
render={({ field }) => ( label="Value"
<FormControl isError={Boolean(errors?.value)}
label="Tags" errorText={errors?.value?.message}
isError={Boolean(errors?.value)} >
errorText={errors?.value?.message} <InfisicalSecretInput
helperText={ {...field}
!canReadTags ? ( environment={environment}
<div className="flex items-center space-x-2"> secretPath={secretPath}
<FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" /> containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
<span>You do not have permission to read tags.</span> />
</div> </FormControl>
) : ( )}
"" />
) <div className="mt-7 flex items-center">
} <Button
> isDisabled={isSubmitting}
<MultiSelect isLoading={isSubmitting}
className="w-full" key="layout-create-project-submit"
placeholder="Select tags to assign to secret..." className="mr-4"
isMulti type="submit"
name="tagIds" >
isDisabled={!canReadTags} Create Secret
isLoading={isTagsLoading} </Button>
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))} <Button
value={field.value} key="layout-cancel-create-project"
onChange={field.onChange} onClick={() => closePopUp(PopUpNames.CreateSecretForm)}
/> variant="plain"
</FormControl> colorSchema="secondary"
)} >
/> Cancel
<div className="mt-7 flex items-center"> </Button>
<Button </div>
isDisabled={isSubmitting} </form>
isLoading={isSubmitting} </ModalContent>
key="layout-create-project-submit" </Modal>
className="mr-4"
type="submit"
>
Create Secret
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => closePopUp(PopUpNames.CreateSecretForm)}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
); );
}; };

View File

@ -1116,23 +1116,13 @@ export const SecretOverviewPage = () => {
)} )}
</div> </div>
</div> </div>
<Modal <CreateSecretForm
secretPath={secretPath}
isOpen={popUp.addSecretsInAllEnvs.isOpen} isOpen={popUp.addSecretsInAllEnvs.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)} getSecretByKey={getSecretByKey}
> onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
<ModalContent onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
className="max-h-[80vh]" />
bodyClassName="overflow-visible"
title="Create Secrets"
subTitle="Create a secret across multiple environments"
>
<CreateSecretForm
secretPath={secretPath}
getSecretByKey={getSecretByKey}
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
/>
</ModalContent>
</Modal>
<Modal <Modal
isOpen={popUp.addFolder.isOpen} isOpen={popUp.addFolder.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}

View File

@ -1,7 +1,7 @@
import { ClipboardEvent } from "react"; import { ClipboardEvent } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faTriangleExclamation, faWarning } from "@fortawesome/free-solid-svg-icons"; import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
@ -13,7 +13,8 @@ import {
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
MultiSelect, Modal,
ModalContent,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
@ -24,20 +25,14 @@ import {
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { getKeyValue } from "@app/helpers/parseEnvVar"; import { getKeyValue } from "@app/helpers/parseEnvVar";
import { import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
useCreateFolder,
useCreateSecretV3,
useGetWsTags,
useUpdateSecretV3
} from "@app/hooks/api";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
const typeSchema = z const typeSchema = z
.object({ .object({
key: z.string().trim().min(1, "Key is required"), key: z.string().trim().min(1, "Key is required"),
value: z.string().optional(), value: z.string().optional(),
environments: z.record(z.boolean().optional()), environments: z.record(z.boolean().optional())
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
}) })
.refine((data) => data.key !== undefined, { .refine((data) => data.key !== undefined, {
message: "Please enter secret name" message: "Please enter secret name"
@ -49,10 +44,18 @@ type Props = {
secretPath?: string; secretPath?: string;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined; getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
// modal props // modal props
isOpen?: boolean;
onClose: () => void; onClose: () => void;
onTogglePopUp: (isOpen: boolean) => void;
}; };
export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }: Props) => { export const CreateSecretForm = ({
secretPath = "/",
isOpen,
getSecretByKey,
onClose,
onTogglePopUp
}: Props) => {
const { const {
register, register,
handleSubmit, handleSubmit,
@ -66,18 +69,14 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const environments = currentWorkspace?.environments || []; const environments = currentWorkspace?.environments || [];
const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: createFolder } = useCreateFolder();
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
canReadTags ? workspaceId : ""
);
const handleFormSubmit = async ({ key, value, environments: selectedEnv, tags }: TFormSchema) => { const handleFormSubmit = async ({ key, value, environments: selectedEnv }: TFormSchema) => {
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]); const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]);
const isEnvironmentsSelected = environmentsSelected.length; const isEnvironmentsSelected = environmentsSelected.length;
@ -121,8 +120,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
secretPath, secretPath,
secretKey: key, secretKey: key,
secretValue: value || "", secretValue: value || "",
type: SecretType.Shared, type: SecretType.Shared
tagIds: tags?.map((el) => el.value)
})), })),
environment environment
}; };
@ -136,8 +134,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
secretKey: key, secretKey: key,
secretValue: value || "", secretValue: value || "",
secretComment: "", secretComment: "",
type: SecretType.Shared, type: SecretType.Shared
tagIds: tags?.map((el) => el.value)
})), })),
environment environment
}; };
@ -200,136 +197,114 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
}; };
return ( return (
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate> <Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
<FormControl <ModalContent
label="Key" className="max-h-[80vh] overflow-y-auto"
isRequired title="Bulk Create & Update"
isError={Boolean(errors?.key)} subTitle="Create & update a secret across many environments"
errorText={errors?.key?.message}
> >
<Input <form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
{...register("key")}
placeholder="Type your secret name"
onPaste={handlePaste}
autoCapitalization={currentWorkspace?.autoCapitalization}
/>
</FormControl>
<Controller
control={control}
name="value"
render={({ field }) => (
<FormControl <FormControl
label="Value" label="Key"
isError={Boolean(errors?.value)} isRequired
errorText={errors?.value?.message} isError={Boolean(errors?.key)}
errorText={errors?.key?.message}
> >
<InfisicalSecretInput <Input
{...field} {...register("key")}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5" placeholder="Type your secret name"
onPaste={handlePaste}
autoCapitalization={currentWorkspace?.autoCapitalization}
/> />
</FormControl> </FormControl>
)} <Controller
/> control={control}
<Controller name="value"
control={control} render={({ field }) => (
name="tags" <FormControl
render={({ field }) => ( label="Value"
<FormControl isError={Boolean(errors?.value)}
label="Tags" errorText={errors?.value?.message}
isError={Boolean(errors?.value)} >
errorText={errors?.value?.message} <InfisicalSecretInput
helperText={ {...field}
!canReadTags ? ( containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
<div className="flex items-center space-x-2"> />
<FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" /> </FormControl>
<span>You do not have permission to read tags.</span> )}
</div> />
) : ( <FormLabel label="Environments" className="mb-2" />
"" <div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
{environments
.filter((environmentSlug) =>
permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: environmentSlug.slug,
secretPath,
secretName: "*",
secretTags: ["*"]
})
)
) )
} .map((env) => {
> return (
<MultiSelect <Controller
className="w-full" name={`environments.${env.slug}`}
placeholder="Select tags to assign to secrets..." key={`secret-input-${env.slug}`}
isMulti control={control}
name="tagIds" render={({ field }) => (
isDisabled={!canReadTags} <Checkbox
isLoading={isTagsLoading} isChecked={field.value}
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))} onCheckedChange={field.onChange}
value={field.value} id={`secret-input-${env.slug}`}
onChange={field.onChange} className="!justify-start"
/> >
</FormControl> <span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
)} <span title={env.name} className="truncate">
/> {env.name}
<FormLabel label="Environments" className="mb-2" /> </span>
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2"> <span>
{environments {getSecretByKey(env.slug, newSecretKey) && (
.filter((environmentSlug) => <Tooltip
permission.can( className="max-w-[150px]"
ProjectPermissionActions.Create, content="Secret already exists, and it will be overwritten"
subject(ProjectPermissionSub.Secrets, { >
environment: environmentSlug.slug, <FontAwesomeIcon
secretPath, icon={faWarning}
secretName: "*", className="ml-1 text-yellow-400"
secretTags: ["*"] />
}) </Tooltip>
) )}
) </span>
.map((env) => { </span>
return ( </Checkbox>
<Controller )}
name={`environments.${env.slug}`} />
key={`secret-input-${env.slug}`} );
control={control} })}
render={({ field }) => ( </div>
<Checkbox <div className="mt-7 flex items-center">
isChecked={field.value} <Button
onCheckedChange={field.onChange} isDisabled={isSubmitting}
id={`secret-input-${env.slug}`} isLoading={isSubmitting}
className="!justify-start" key="layout-create-project-submit"
> className="mr-4"
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap"> type="submit"
<span title={env.name} className="truncate"> >
{env.name} Create Secret
</span> </Button>
<span> <Button
{getSecretByKey(env.slug, newSecretKey) && ( key="layout-cancel-create-project"
<Tooltip onClick={onClose}
className="max-w-[150px]" variant="plain"
content="Secret already exists, and it will be overwritten" colorSchema="secondary"
> >
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" /> Cancel
</Tooltip> </Button>
)} </div>
</span> </form>
</span> </ModalContent>
</Checkbox> </Modal>
)}
/>
);
})}
</div>
<div className="mt-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="mr-4"
type="submit"
>
Create Secret
</Button>
<Button
key="layout-cancel-create-project"
onClick={onClose}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
); );
}; };