misc: addressed comments

This commit is contained in:
Sheen Capadngan
2025-07-29 04:56:50 +08:00
parent 585cb1b30c
commit e430abfc9e
15 changed files with 72 additions and 62 deletions
@@ -3,17 +3,17 @@ import { Knex } from "knex";
import { TableName } from "../schemas"; import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreKeys"))) { if (!(await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues"))) {
await knex.schema.alterTable(TableName.Project, (t) => { await knex.schema.alterTable(TableName.Project, (t) => {
t.specificType("secretDetectionIgnoreKeys", "text[]"); t.specificType("secretDetectionIgnoreValues", "text[]");
}); });
} }
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreKeys")) { if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues")) {
await knex.schema.alterTable(TableName.Project, (t) => { await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("secretDetectionIgnoreKeys"); t.dropColumn("secretDetectionIgnoreValues");
}); });
} }
} }
+1 -1
View File
@@ -31,7 +31,7 @@ export const ProjectsSchema = z.object({
secretSharing: z.boolean().default(true), secretSharing: z.boolean().default(true),
showSnapshotsLegacy: z.boolean().default(false), showSnapshotsLegacy: z.boolean().default(false),
defaultProduct: z.string().nullable().optional(), defaultProduct: z.string().nullable().optional(),
secretDetectionIgnoreKeys: z.string().array().nullable().optional() secretDetectionIgnoreValues: z.string().array().nullable().optional()
}); });
export type TProjects = z.infer<typeof ProjectsSchema>; export type TProjects = z.infer<typeof ProjectsSchema>;
@@ -1410,6 +1410,7 @@ export const secretApprovalRequestServiceFactory = ({
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
await scanSecretPolicyViolations( await scanSecretPolicyViolations(
projectId,
secretPath, secretPath,
[ [
...(data[SecretOperations.Create] || []), ...(data[SecretOperations.Create] || []),
@@ -1418,7 +1419,7 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.secretKey, secretKey: el.secretKey,
secretValue: el.secretValue as string secretValue: el.secretValue as string
})), })),
project.secretDetectionIgnoreKeys || [] project.secretDetectionIgnoreValues || []
); );
// for created secret approval change // for created secret approval change
@@ -165,28 +165,29 @@ export const parseScanErrorMessage = (err: unknown): string => {
}; };
export const scanSecretPolicyViolations = async ( export const scanSecretPolicyViolations = async (
projectId: string,
secretPath: string, secretPath: string,
secrets: { secretKey: string; secretValue: string }[], secrets: { secretKey: string; secretValue: string }[],
ignoreKeys: string[] ignoreValues: string[]
) => { ) => {
const appCfg = getConfig(); const appCfg = getConfig();
if (!appCfg.PARAMS_FOLDER_SECRET_DETECTION_ENABLED) { if (!appCfg.PARAMS_FOLDER_SECRET_DETECTION_ENABLED) {
return; return;
} }
const paramFolderSecretDetectionPaths = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.map((el) => el.secretPath) ?? [];
const isPathMatched = paramFolderSecretDetectionPaths.some((pattern) => const match = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.find(
picomatch.isMatch(secretPath, pattern, { strictSlashes: false }) (el) => el.projectId === projectId && picomatch.isMatch(secretPath, el.secretPath, { strictSlashes: false })
); );
if (!isPathMatched) { if (!match) {
return; return;
} }
const tempFolder = await createTempFolder(); const tempFolder = await createTempFolder();
try { try {
const scanPromises = secrets const scanPromises = secrets
.filter((secret) => !ignoreKeys.includes(secret.secretKey)) .filter((secret) => !ignoreValues.includes(secret.secretValue))
.map(async (secret) => { .map(async (secret) => {
const secretFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`); const secretFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`);
await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`); await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`);
+1 -1
View File
@@ -705,7 +705,7 @@ export const PROJECTS = {
secretSharing: "Enable or disable secret sharing for the project.", secretSharing: "Enable or disable secret sharing for the project.",
showSnapshotsLegacy: "Enable or disable legacy snapshots for the project.", showSnapshotsLegacy: "Enable or disable legacy snapshots for the project.",
defaultProduct: "The default product in which the project will open", defaultProduct: "The default product in which the project will open",
secretDetectionIgnoreKeys: "The list of secret keys to ignore for secret detection." secretDetectionIgnoreValues: "The list of secret values to ignore for secret detection."
}, },
GET_KEY: { GET_KEY: {
workspaceId: "The ID of the project to get the key from." workspaceId: "The ID of the project to get the key from."
+3 -3
View File
@@ -211,10 +211,9 @@ const envSchema = z
.optional() .optional()
.transform((val) => { .transform((val) => {
if (!val) return undefined; if (!val) return undefined;
return JSON.parse(val) as { secretPath: string }[]; return JSON.parse(val) as { secretPath: string; projectId: string }[];
}) })
), ),
PARAMS_FOLDER_SECRET_DETECTION_ENABLED: zodStrBool.default("false"),
// HSM // HSM
HSM_LIB_PATH: zpStr(z.string().optional()), HSM_LIB_PATH: zpStr(z.string().optional()),
@@ -353,7 +352,8 @@ const envSchema = z
isHsmConfigured: isHsmConfigured:
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(","),
PARAMS_FOLDER_SECRET_DETECTION_ENABLED: (data.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.length ?? 0) > 0
})); }));
export type TEnvConfig = Readonly<z.infer<typeof envSchema>>; export type TEnvConfig = Readonly<z.infer<typeof envSchema>>;
@@ -265,7 +265,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
hasDeleteProtection: true, hasDeleteProtection: true,
secretSharing: true, secretSharing: true,
showSnapshotsLegacy: true, showSnapshotsLegacy: true,
secretDetectionIgnoreKeys: true secretDetectionIgnoreValues: true
}); });
export const SanitizedTagSchema = SecretTagsSchema.pick({ export const SanitizedTagSchema = SecretTagsSchema.pick({
@@ -370,7 +370,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing), secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing),
showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy), showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy),
defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct), defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct),
secretDetectionIgnoreKeys: z.array(z.string()).optional().describe(PROJECTS.UPDATE.secretDetectionIgnoreKeys) secretDetectionIgnoreValues: z
.array(z.string())
.optional()
.describe(PROJECTS.UPDATE.secretDetectionIgnoreValues)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -394,7 +397,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
slug: req.body.slug, slug: req.body.slug,
secretSharing: req.body.secretSharing, secretSharing: req.body.secretSharing,
showSnapshotsLegacy: req.body.showSnapshotsLegacy, showSnapshotsLegacy: req.body.showSnapshotsLegacy,
secretDetectionIgnoreKeys: req.body.secretDetectionIgnoreKeys secretDetectionIgnoreValues: req.body.secretDetectionIgnoreValues
}, },
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -667,9 +667,9 @@ export const projectServiceFactory = ({
} }
} }
if (update.secretDetectionIgnoreKeys && !hasRole(ProjectMembershipRole.Admin)) { if (update.secretDetectionIgnoreValues && !hasRole(ProjectMembershipRole.Admin)) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Only admins can update secret detection ignore keys" message: "Only admins can update secret detection ignore values"
}); });
} }
@@ -683,7 +683,7 @@ export const projectServiceFactory = ({
secretSharing: update.secretSharing, secretSharing: update.secretSharing,
defaultProduct: update.defaultProduct, defaultProduct: update.defaultProduct,
showSnapshotsLegacy: update.showSnapshotsLegacy, showSnapshotsLegacy: update.showSnapshotsLegacy,
secretDetectionIgnoreKeys: update.secretDetectionIgnoreKeys secretDetectionIgnoreValues: update.secretDetectionIgnoreValues
}); });
return updatedProject; return updatedProject;
@@ -96,7 +96,7 @@ export type TUpdateProjectDTO = {
slug?: string; slug?: string;
secretSharing?: boolean; secretSharing?: boolean;
showSnapshotsLegacy?: boolean; showSnapshotsLegacy?: boolean;
secretDetectionIgnoreKeys?: string[]; secretDetectionIgnoreValues?: string[];
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -301,6 +301,7 @@ export const secretV2BridgeServiceFactory = ({
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
await scanSecretPolicyViolations( await scanSecretPolicyViolations(
projectId,
secretPath, secretPath,
[ [
{ {
@@ -308,7 +309,7 @@ export const secretV2BridgeServiceFactory = ({
secretValue: inputSecret.secretValue secretValue: inputSecret.secretValue
} }
], ],
project.secretDetectionIgnoreKeys || [] project.secretDetectionIgnoreValues || []
); );
const { nestedReferences, localReferences } = getAllSecretReferences(inputSecret.secretValue); const { nestedReferences, localReferences } = getAllSecretReferences(inputSecret.secretValue);
@@ -525,6 +526,7 @@ export const secretV2BridgeServiceFactory = ({
if (secretValue) { if (secretValue) {
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
await scanSecretPolicyViolations( await scanSecretPolicyViolations(
projectId,
secretPath, secretPath,
[ [
{ {
@@ -532,7 +534,7 @@ export const secretV2BridgeServiceFactory = ({
secretValue secretValue
} }
], ],
project.secretDetectionIgnoreKeys || [] project.secretDetectionIgnoreValues || []
); );
} }
@@ -1616,7 +1618,7 @@ export const secretV2BridgeServiceFactory = ({
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` }); throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
await scanSecretPolicyViolations(secretPath, inputSecrets, project.secretDetectionIgnoreKeys || []); await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []);
// get all tags // get all tags
const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds);
@@ -1960,6 +1962,7 @@ export const secretV2BridgeServiceFactory = ({
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
await scanSecretPolicyViolations( await scanSecretPolicyViolations(
projectId,
secretPath, secretPath,
secretsToUpdate secretsToUpdate
.filter((el) => el.secretValue) .filter((el) => el.secretValue)
@@ -1967,7 +1970,7 @@ export const secretV2BridgeServiceFactory = ({
secretKey: el.newSecretName || el.secretKey, secretKey: el.newSecretName || el.secretKey,
secretValue: el.secretValue as string secretValue: el.secretValue as string
})), })),
project.secretDetectionIgnoreKeys || [] project.secretDetectionIgnoreValues || []
); );
const bulkUpdatedSecrets = await fnSecretBulkUpdate({ const bulkUpdatedSecrets = await fnSecretBulkUpdate({
+2 -2
View File
@@ -282,7 +282,7 @@ export const useUpdateProject = () => {
newSlug, newSlug,
secretSharing, secretSharing,
showSnapshotsLegacy, showSnapshotsLegacy,
secretDetectionIgnoreKeys secretDetectionIgnoreValues
}) => { }) => {
const { data } = await apiRequest.patch<{ workspace: Workspace }>( const { data } = await apiRequest.patch<{ workspace: Workspace }>(
`/api/v1/workspace/${projectID}`, `/api/v1/workspace/${projectID}`,
@@ -292,7 +292,7 @@ export const useUpdateProject = () => {
slug: newSlug, slug: newSlug,
secretSharing, secretSharing,
showSnapshotsLegacy, showSnapshotsLegacy,
secretDetectionIgnoreKeys secretDetectionIgnoreValues
} }
); );
return data.workspace; return data.workspace;
+2 -2
View File
@@ -40,7 +40,7 @@ export type Workspace = {
hasDeleteProtection: boolean; hasDeleteProtection: boolean;
secretSharing: boolean; secretSharing: boolean;
showSnapshotsLegacy: boolean; showSnapshotsLegacy: boolean;
secretDetectionIgnoreKeys: string[]; secretDetectionIgnoreValues: string[];
}; };
export type WorkspaceEnv = { export type WorkspaceEnv = {
@@ -82,7 +82,7 @@ export type UpdateProjectDTO = {
newSlug?: string; newSlug?: string;
secretSharing?: boolean; secretSharing?: boolean;
showSnapshotsLegacy?: boolean; showSnapshotsLegacy?: boolean;
secretDetectionIgnoreKeys?: string[]; secretDetectionIgnoreValues?: string[];
}; };
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number }; export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
@@ -4,7 +4,7 @@ import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection"; import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { EnvironmentSection } from "../EnvironmentSection"; import { EnvironmentSection } from "../EnvironmentSection";
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection"; import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
import { SecretDetectionIgnoreKeysSection } from "../SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection"; import { SecretDetectionIgnoreValuesSection } from "../SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection";
import { SecretSharingSection } from "../SecretSharingSection"; import { SecretSharingSection } from "../SecretSharingSection";
import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection"; import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection";
import { SecretTagsSection } from "../SecretTagsSection"; import { SecretTagsSection } from "../SecretTagsSection";
@@ -20,7 +20,7 @@ export const SecretSettingsTab = () => {
<SecretSharingSection /> <SecretSharingSection />
<SecretSnapshotsLegacySection /> <SecretSnapshotsLegacySection />
<PointInTimeVersionLimitSection /> <PointInTimeVersionLimitSection />
{config.paramsFolderSecretDetectionEnabled && <SecretDetectionIgnoreKeysSection />} {config.paramsFolderSecretDetectionEnabled && <SecretDetectionIgnoreValuesSection />}
<BackfillSecretReferenceSecretion /> <BackfillSecretReferenceSecretion />
</div> </div>
); );
@@ -12,9 +12,9 @@ import { useUpdateProject } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
const formSchema = z.object({ const formSchema = z.object({
ignoreKeys: z ignoreValues: z
.object({ .object({
key: z.string().trim().min(1, "Secret key name is required") value: z.string().trim().min(1, "Secret value is required")
}) })
.array() .array()
.default([]) .default([])
@@ -22,7 +22,7 @@ const formSchema = z.object({
type TForm = z.infer<typeof formSchema>; type TForm = z.infer<typeof formSchema>;
export const SecretDetectionIgnoreKeysSection = () => { export const SecretDetectionIgnoreValuesSection = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission(); const { membership } = useProjectPermission();
const { mutateAsync: updateProject } = useUpdateProject(); const { mutateAsync: updateProject } = useUpdateProject();
@@ -35,37 +35,39 @@ export const SecretDetectionIgnoreKeysSection = () => {
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
ignoreKeys: [] ignoreValues: []
} }
}); });
const ignoreKeysFormFields = useFieldArray({ const ignoreValuesFormFields = useFieldArray({
control, control,
name: "ignoreKeys" name: "ignoreValues"
}); });
useEffect(() => { useEffect(() => {
const existingIgnoreKeys = currentWorkspace?.secretDetectionIgnoreKeys || []; const existingIgnoreValues = currentWorkspace?.secretDetectionIgnoreValues || [];
reset({ reset({
ignoreKeys: ignoreValues:
existingIgnoreKeys.length > 0 ? existingIgnoreKeys.map((key) => ({ key })) : [{ key: "" }] // Show one empty field by default existingIgnoreValues.length > 0
? existingIgnoreValues.map((value) => ({ value }))
: [{ value: "" }] // Show one empty field by default
}); });
}, [currentWorkspace?.secretDetectionIgnoreKeys, reset]); }, [currentWorkspace?.secretDetectionIgnoreValues, reset]);
const handleIgnoreKeysSubmit = async ({ ignoreKeys }: TForm) => { const handleIgnoreValuesSubmit = async ({ ignoreValues }: TForm) => {
try { try {
await updateProject({ await updateProject({
projectID: currentWorkspace.id, projectID: currentWorkspace.id,
secretDetectionIgnoreKeys: ignoreKeys.map((item) => item.key) secretDetectionIgnoreValues: ignoreValues.map((item) => item.value)
}); });
createNotification({ createNotification({
text: "Successfully updated secret detection ignore keys", text: "Successfully updated secret detection ignore values",
type: "success" type: "success"
}); });
} catch { } catch {
createNotification({ createNotification({
text: "Failed updating secret detection ignore keys", text: "Failed updating secret detection ignore values",
type: "error" type: "error"
}); });
} }
@@ -78,41 +80,41 @@ export const SecretDetectionIgnoreKeysSection = () => {
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">Secret Detection Ignore Keys</p> <p className="text-xl font-semibold">Secret Detection Ignore Values</p>
</div> </div>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400"> <p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
Define secret keys that should be ignored when scanning parameter folders for misplaced Define secret values that should be ignored when scanning parameter folders for misplaced
secrets. These keys will not trigger policy violation alerts even if they contain sensitive secrets. These values will not trigger policy violation alerts even if they contain
data. sensitive data.
</p> </p>
<form onSubmit={handleSubmit(handleIgnoreKeysSubmit)} autoComplete="off"> <form onSubmit={handleSubmit(handleIgnoreValuesSubmit)} autoComplete="off">
<div className="mb-4"> <div className="mb-4">
<p className="mb-3 text-sm font-medium text-gray-300">Ignored Secret Keys</p> <p className="mb-3 text-sm font-medium text-gray-300">Ignored Secret Values</p>
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
{ignoreKeysFormFields.fields.map(({ id: ignoreKeyFieldId }, i) => ( {ignoreValuesFormFields.fields.map(({ id: ignoreValueFieldId }, i) => (
<div key={ignoreKeyFieldId} className="flex items-end space-x-2"> <div key={ignoreValueFieldId} className="flex items-end space-x-2">
<div className="flex-grow"> <div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Secret Key Name</span>} {i === 0 && <span className="text-xs text-mineshaft-400">Secret Value</span>}
<Controller <Controller
control={control} control={control}
name={`ignoreKeys.${i}.key`} name={`ignoreValues.${i}.value`}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
className="mb-0" className="mb-0"
> >
<Input {...field} placeholder="PUBLIC_API_KEY" isDisabled={!isAdmin} /> <Input {...field} placeholder="sk-1234567890abcdef" isDisabled={!isAdmin} />
</FormControl> </FormControl>
)} )}
/> />
</div> </div>
<IconButton <IconButton
ariaLabel="delete ignore key" ariaLabel="delete ignore value"
className="bottom-0.5 h-9" className="bottom-0.5 h-9"
variant="outline_bg" variant="outline_bg"
onClick={() => ignoreKeysFormFields.remove(i)} onClick={() => ignoreValuesFormFields.remove(i)}
isDisabled={!isAdmin} isDisabled={!isAdmin}
> >
<FontAwesomeIcon icon={faTrash} /> <FontAwesomeIcon icon={faTrash} />
@@ -124,10 +126,10 @@ export const SecretDetectionIgnoreKeysSection = () => {
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs" size="xs"
variant="outline_bg" variant="outline_bg"
onClick={() => ignoreKeysFormFields.append({ key: "" })} onClick={() => ignoreValuesFormFields.append({ value: "" })}
isDisabled={!isAdmin} isDisabled={!isAdmin}
> >
Add Ignore Key Add Ignore Value
</Button> </Button>
</div> </div>
</div> </div>