mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 09:26:47 +00:00
feat: add support for no bootstrap cert EST
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasSkipBootstrapCertValidationCol = await knex.schema.hasColumn(
|
||||||
|
TableName.CertificateTemplateEstConfig,
|
||||||
|
"skipBootstrapCertValidation"
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasCaChainCol = await knex.schema.hasColumn(TableName.CertificateTemplateEstConfig, "encryptedCaChain");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
|
||||||
|
if (!hasSkipBootstrapCertValidationCol) {
|
||||||
|
t.boolean("skipBootstrapCertValidation").defaultTo(false).notNullable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCaChainCol) {
|
||||||
|
t.binary("encryptedCaChain").nullable().alter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasSkipBootstrapCertValidationCol = await knex.schema.hasColumn(
|
||||||
|
TableName.CertificateTemplateEstConfig,
|
||||||
|
"skipBootstrapCertValidation"
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasCaChainCol = await knex.schema.hasColumn(TableName.CertificateTemplateEstConfig, "encryptedCaChain");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
|
||||||
|
if (hasSkipBootstrapCertValidationCol) {
|
||||||
|
t.dropColumn("skipBootstrapCertValidation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCaChainCol) {
|
||||||
|
t.binary("encryptedCaChain").notNullable().alter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -12,11 +12,12 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const CertificateTemplateEstConfigsSchema = z.object({
|
export const CertificateTemplateEstConfigsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
certificateTemplateId: z.string().uuid(),
|
certificateTemplateId: z.string().uuid(),
|
||||||
encryptedCaChain: zodBuffer,
|
encryptedCaChain: zodBuffer.nullable().optional(),
|
||||||
hashedPassphrase: z.string(),
|
hashedPassphrase: z.string(),
|
||||||
isEnabled: z.boolean(),
|
isEnabled: z.boolean(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
skipBootstrapCertValidation: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
|
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
|
||||||
|
|||||||
@@ -171,27 +171,29 @@ export const certificateEstServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const caCerts = estConfig.caChain
|
if (!estConfig.skipBootstrapCertValidation) {
|
||||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
const caCerts = estConfig.caChain
|
||||||
?.map((cert) => {
|
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||||
return new x509.X509Certificate(cert);
|
?.map((cert) => {
|
||||||
});
|
return new x509.X509Certificate(cert);
|
||||||
|
});
|
||||||
|
|
||||||
if (!caCerts) {
|
if (!caCerts) {
|
||||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||||
)?.[0];
|
)?.[0];
|
||||||
|
|
||||||
if (!leafCertificate) {
|
if (!leafCertificate) {
|
||||||
throw new BadRequestError({ message: "Missing client certificate" });
|
throw new BadRequestError({ message: "Missing client certificate" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const certObj = new x509.X509Certificate(leafCertificate);
|
const certObj = new x509.X509Certificate(leafCertificate);
|
||||||
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
||||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { validateTemplateRegexField } from "@app/services/certificate-template/c
|
|||||||
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
|
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
certificateTemplateId: true,
|
certificateTemplateId: true,
|
||||||
isEnabled: true
|
isEnabled: true,
|
||||||
|
skipBootstrapCertValidation: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -241,11 +242,18 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
certificateTemplateId: z.string().trim()
|
certificateTemplateId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
caChain: z.string().trim().min(1),
|
.object({
|
||||||
passphrase: z.string().min(1),
|
caChain: z.string().trim().optional(),
|
||||||
isEnabled: z.boolean().default(true)
|
passphrase: z.string().min(1),
|
||||||
}),
|
isEnabled: z.boolean().default(true),
|
||||||
|
skipBootstrapCertValidation: z.boolean().default(false)
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
({ caChain, skipBootstrapCertValidation }) =>
|
||||||
|
skipBootstrapCertValidation || (!skipBootstrapCertValidation && caChain),
|
||||||
|
"CA chain is required"
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: sanitizedEstConfig
|
200: sanitizedEstConfig
|
||||||
}
|
}
|
||||||
@@ -289,8 +297,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
certificateTemplateId: z.string().trim()
|
certificateTemplateId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
caChain: z.string().trim().min(1).optional(),
|
caChain: z.string().trim().optional(),
|
||||||
passphrase: z.string().min(1).optional(),
|
passphrase: z.string().min(1).optional(),
|
||||||
|
skipBootstrapCertValidation: z.boolean().optional(),
|
||||||
isEnabled: z.boolean().optional()
|
isEnabled: z.boolean().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -235,7 +235,8 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
skipBootstrapCertValidation
|
||||||
}: TCreateEstConfigurationDTO) => {
|
}: TCreateEstConfigurationDTO) => {
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan.pkiEst) {
|
if (!plan.pkiEst) {
|
||||||
@@ -266,39 +267,45 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
let encryptedCaChain: Buffer | undefined;
|
||||||
projectId: certTemplate.projectId,
|
if (caChain) {
|
||||||
projectDAL,
|
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||||
kmsService
|
projectId: certTemplate.projectId,
|
||||||
});
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
// validate CA chain
|
// validate CA chain
|
||||||
const certificates = caChain
|
const certificates = caChain
|
||||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||||
?.map((cert) => new x509.X509Certificate(cert));
|
?.map((cert) => new x509.X509Certificate(cert));
|
||||||
|
|
||||||
if (!certificates) {
|
if (!certificates) {
|
||||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await isCertChainValid(certificates))) {
|
||||||
|
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(caChain)
|
||||||
|
});
|
||||||
|
|
||||||
|
encryptedCaChain = cipherTextBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await isCertChainValid(certificates))) {
|
|
||||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
|
||||||
kmsId: certificateManagerKmsId
|
|
||||||
});
|
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
|
|
||||||
plainText: Buffer.from(caChain)
|
|
||||||
});
|
|
||||||
|
|
||||||
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
|
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
|
||||||
const estConfig = await certificateTemplateEstConfigDAL.create({
|
const estConfig = await certificateTemplateEstConfigDAL.create({
|
||||||
certificateTemplateId,
|
certificateTemplateId,
|
||||||
hashedPassphrase,
|
hashedPassphrase,
|
||||||
encryptedCaChain,
|
encryptedCaChain,
|
||||||
isEnabled
|
isEnabled,
|
||||||
|
skipBootstrapCertValidation
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...estConfig, projectId: certTemplate.projectId };
|
return { ...estConfig, projectId: certTemplate.projectId };
|
||||||
@@ -312,7 +319,8 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
skipBootstrapCertValidation
|
||||||
}: TUpdateEstConfigurationDTO) => {
|
}: TUpdateEstConfigurationDTO) => {
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan.pkiEst) {
|
if (!plan.pkiEst) {
|
||||||
@@ -360,7 +368,8 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updatedData: TCertificateTemplateEstConfigsUpdate = {
|
const updatedData: TCertificateTemplateEstConfigsUpdate = {
|
||||||
isEnabled
|
isEnabled,
|
||||||
|
skipBootstrapCertValidation
|
||||||
};
|
};
|
||||||
|
|
||||||
if (caChain) {
|
if (caChain) {
|
||||||
@@ -442,18 +451,24 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
kmsId: certificateManagerKmsId
|
kmsId: certificateManagerKmsId
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedCaChain = await kmsDecryptor({
|
let decryptedCaChain = "";
|
||||||
cipherTextBlob: estConfig.encryptedCaChain
|
if (estConfig.encryptedCaChain) {
|
||||||
});
|
decryptedCaChain = (
|
||||||
|
await kmsDecryptor({
|
||||||
|
cipherTextBlob: estConfig.encryptedCaChain
|
||||||
|
})
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificateTemplateId,
|
certificateTemplateId,
|
||||||
id: estConfig.id,
|
id: estConfig.id,
|
||||||
isEnabled: estConfig.isEnabled,
|
isEnabled: estConfig.isEnabled,
|
||||||
caChain: decryptedCaChain.toString(),
|
caChain: decryptedCaChain,
|
||||||
hashedPassphrase: estConfig.hashedPassphrase,
|
hashedPassphrase: estConfig.hashedPassphrase,
|
||||||
projectId: certTemplate.projectId,
|
projectId: certTemplate.projectId,
|
||||||
orgId: certTemplate.orgId
|
orgId: certTemplate.orgId,
|
||||||
|
skipBootstrapCertValidation: estConfig.skipBootstrapCertValidation
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ export type TDeleteCertTemplateDTO = {
|
|||||||
|
|
||||||
export type TCreateEstConfigurationDTO = {
|
export type TCreateEstConfigurationDTO = {
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
caChain: string;
|
caChain?: string;
|
||||||
passphrase: string;
|
passphrase: string;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
|
skipBootstrapCertValidation: boolean;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateEstConfigurationDTO = {
|
export type TUpdateEstConfigurationDTO = {
|
||||||
@@ -44,6 +45,7 @@ export type TUpdateEstConfigurationDTO = {
|
|||||||
caChain?: string;
|
caChain?: string;
|
||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
isEnabled?: boolean;
|
isEnabled?: boolean;
|
||||||
|
skipBootstrapCertValidation?: boolean;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TGetEstConfigurationDTO =
|
export type TGetEstConfigurationDTO =
|
||||||
|
|||||||
@@ -46,9 +46,10 @@ export type TDeleteCertificateTemplateDTO = {
|
|||||||
|
|
||||||
export type TCreateEstConfigDTO = {
|
export type TCreateEstConfigDTO = {
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
caChain: string;
|
caChain?: string;
|
||||||
passphrase: string;
|
passphrase: string;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
|
skipBootstrapCertValidation: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateEstConfigDTO = {
|
export type TUpdateEstConfigDTO = {
|
||||||
@@ -56,11 +57,13 @@ export type TUpdateEstConfigDTO = {
|
|||||||
caChain?: string;
|
caChain?: string;
|
||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
isEnabled?: boolean;
|
isEnabled?: boolean;
|
||||||
|
skipBootstrapCertValidation?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TEstConfig = {
|
export type TEstConfig = {
|
||||||
id: string;
|
id: string;
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
caChain: string;
|
caChain: string;
|
||||||
isEnabled: false;
|
isEnabled: boolean;
|
||||||
|
skipBootstrapCertValidation: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
+57
-21
@@ -33,9 +33,10 @@ type Props = {
|
|||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
method: z.nativeEnum(EnrollmentMethod),
|
method: z.nativeEnum(EnrollmentMethod),
|
||||||
caChain: z.string(),
|
caChain: z.string().optional(),
|
||||||
passphrase: z.string().optional(),
|
passphrase: z.string().optional(),
|
||||||
isEnabled: z.boolean()
|
isEnabled: z.boolean(),
|
||||||
|
skipBootstrapCertValidation: z.boolean().optional().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
@@ -53,6 +54,8 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
setError,
|
setError,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema)
|
resolver: zodResolver(schema)
|
||||||
@@ -62,16 +65,26 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
|||||||
const { mutateAsync: updateEstConfig } = useUpdateEstConfig();
|
const { mutateAsync: updateEstConfig } = useUpdateEstConfig();
|
||||||
const [isPassphraseFocused, setIsPassphraseFocused] = useToggle(false);
|
const [isPassphraseFocused, setIsPassphraseFocused] = useToggle(false);
|
||||||
|
|
||||||
|
const skipBootstrapCertValidation = watch("skipBootstrapCertValidation");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (skipBootstrapCertValidation) {
|
||||||
|
setValue("caChain", "");
|
||||||
|
}
|
||||||
|
}, [skipBootstrapCertValidation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
reset({
|
reset({
|
||||||
caChain: data.caChain,
|
caChain: data.caChain,
|
||||||
isEnabled: data.isEnabled
|
isEnabled: data.isEnabled,
|
||||||
|
skipBootstrapCertValidation: data.skipBootstrapCertValidation
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
caChain: "",
|
caChain: "",
|
||||||
isEnabled: false
|
isEnabled: false,
|
||||||
|
skipBootstrapCertValidation: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@@ -83,7 +96,8 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
|||||||
certificateTemplateId,
|
certificateTemplateId,
|
||||||
caChain,
|
caChain,
|
||||||
passphrase,
|
passphrase,
|
||||||
isEnabled
|
isEnabled,
|
||||||
|
skipBootstrapCertValidation
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!passphrase) {
|
if (!passphrase) {
|
||||||
@@ -95,7 +109,8 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
|||||||
certificateTemplateId,
|
certificateTemplateId,
|
||||||
caChain,
|
caChain,
|
||||||
passphrase,
|
passphrase,
|
||||||
isEnabled
|
isEnabled,
|
||||||
|
skipBootstrapCertValidation
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,22 +167,43 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
|||||||
)}
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="caChain"
|
name="skipBootstrapCertValidation"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<FormControl
|
return (
|
||||||
label="Certificate Authority Chain"
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
isError={Boolean(error)}
|
<Switch
|
||||||
errorText={error?.message}
|
id="skip-bootstrap-cert-validation"
|
||||||
isRequired
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
>
|
isChecked={field.value}
|
||||||
<TextArea
|
>
|
||||||
{...field}
|
<p className="ml-1 w-full">Skip Bootstrap Certificate Validation</p>
|
||||||
className="min-h-[15rem] border-none bg-mineshaft-900 text-gray-400"
|
</Switch>
|
||||||
reSize="none"
|
</FormControl>
|
||||||
/>
|
);
|
||||||
</FormControl>
|
}}
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
{!skipBootstrapCertValidation && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="caChain"
|
||||||
|
disabled={skipBootstrapCertValidation}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Certificate Authority Chain"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired={!skipBootstrapCertValidation}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
isDisabled={skipBootstrapCertValidation}
|
||||||
|
className="min-h-[15rem] border-none bg-mineshaft-900 text-gray-400"
|
||||||
|
reSize="none"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="passphrase"
|
name="passphrase"
|
||||||
|
|||||||
Reference in New Issue
Block a user