merge main

This commit is contained in:
Scott Wilson
2025-07-30 16:38:35 -07:00
369 changed files with 2581 additions and 27780 deletions
@@ -11,7 +11,7 @@ import { createNotification } from "@app/components/notifications";
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input, Spinner } from "@app/components/v2";
import { Button, ContentLoader, Input } from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { SessionStorageKeys } from "@app/const";
import { useToggle } from "@app/hooks";
@@ -329,9 +329,8 @@ export const PasswordStep = ({
if (hasExchangedPrivateKey) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Spinner />
<p className="text-white opacity-80">Loading, please wait</p>
<div className="fixed left-0 top-0 h-screen w-screen bg-bunker-800">
<ContentLoader />
</div>
);
}
@@ -1,26 +1,21 @@
import { useState } from "react";
import { Spinner } from "@app/components/v2";
import { ContentLoader } from "@app/components/v2";
import { useGetMyDuplicateAccount } from "@app/hooks/api";
import { EmailDuplicationConfirmation } from "./EmailDuplicationConfirmation";
import { SelectOrganizationSection } from "./SelectOrgSection";
const LoadingScreen = () => {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Spinner />
<p className="text-white opacity-80">Loading, please wait</p>
</div>
);
};
export const SelectOrganizationPage = () => {
const duplicateAccounts = useGetMyDuplicateAccount();
const [removeDuplicateLater, setRemoveDuplicateLater] = useState(false);
if (duplicateAccounts.isPending) {
return <LoadingScreen />;
return (
<div className="h-screen w-screen bg-bunker-800">
<ContentLoader />
</div>
);
}
if (duplicateAccounts.data?.duplicateAccounts?.length && !removeDuplicateLater) {
@@ -12,7 +12,7 @@ import { Mfa } from "@app/components/auth/Mfa";
import { createNotification } from "@app/components/notifications";
import { IsCliLoginSuccessful } from "@app/components/utilities/attemptCliLogin";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Spinner } from "@app/components/v2";
import { Button, ContentLoader, Spinner } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { OrgMembershipRole } from "@app/helpers/roles";
import { useToggle } from "@app/hooks";
@@ -29,15 +29,6 @@ import { AuthMethod } from "@app/hooks/api/users/types";
import { navigateUserToOrg } from "../LoginPage/Login.utils";
const LoadingScreen = () => {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Spinner />
<p className="text-white opacity-80">Loading, please wait</p>
</div>
);
};
export const SelectOrganizationSection = () => {
const navigate = useNavigate();
const { t } = useTranslation();
@@ -228,7 +219,11 @@ export const SelectOrganizationSection = () => {
!user ||
((isInitialOrgCheckLoading || defaultSelectedOrg) && !shouldShowMfa)
) {
return <LoadingScreen />;
return (
<div className="h-screen w-screen bg-bunker-800">
<ContentLoader />
</div>
);
}
return (
@@ -92,9 +92,9 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.GCP:
return <GcpConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
return <AzureKeyVaultConnectionForm />;
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureAppConfiguration:
return <AzureAppConfigurationConnectionForm />;
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} />;
case AppConnection.Databricks:
return <DatabricksConnectionForm onSubmit={onSubmit} />;
case AppConnection.Humanitec:
@@ -200,9 +200,11 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.GCP:
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
return <AzureKeyVaultConnectionForm appConnection={appConnection} />;
return <AzureKeyVaultConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureAppConfiguration:
return <AzureAppConfigurationConnectionForm appConnection={appConnection} />;
return (
<AzureAppConfigurationConnectionForm appConnection={appConnection} onSubmit={onSubmit} />
);
case AppConnection.Databricks:
return <DatabricksConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Humanitec:
@@ -20,19 +20,83 @@ import {
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureAppConfigurationConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
const baseSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.AzureAppConfiguration),
method: z.nativeEnum(AzureAppConfigurationConnectionMethod),
tenantId: z.string().trim().optional()
method: z.nativeEnum(AzureAppConfigurationConnectionMethod)
});
const oauthSchema = baseSchema.extend({
tenantId: z.string().trim().min(1, "Tenant ID is required"),
method: z.literal(AzureAppConfigurationConnectionMethod.OAuth)
});
const clientSecretSchema = baseSchema.extend({
method: z.literal(AzureAppConfigurationConnectionMethod.ClientSecret),
credentials: z.object({
clientSecret: z.string().trim().min(1, "Client Secret is required"),
clientId: z.string().trim().min(1, "Client ID is required"),
tenantId: z.string().trim().min(1, "Tenant ID is required")
})
});
const formSchema = z.discriminatedUnion("method", [oauthSchema, clientSecretSchema]);
type FormData = z.infer<typeof formSchema>;
export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) => {
const getDefaultValues = (appConnection?: TAzureAppConfigurationConnection): Partial<FormData> => {
if (!appConnection) {
return {
app: AppConnection.AzureAppConfiguration,
method: AzureAppConfigurationConnectionMethod.OAuth
};
}
const base = {
name: appConnection.name,
description: appConnection.description,
app: appConnection.app,
method: appConnection.method
};
const { credentials } = appConnection;
switch (appConnection.method) {
case AzureAppConfigurationConnectionMethod.OAuth:
if ("tenantId" in credentials) {
return {
...base,
method: AzureAppConfigurationConnectionMethod.OAuth,
tenantId: credentials.tenantId
};
}
break;
case AzureAppConfigurationConnectionMethod.ClientSecret:
if ("clientSecret" in credentials && "clientId" in credentials) {
return {
...base,
method: AzureAppConfigurationConnectionMethod.ClientSecret,
credentials: {
clientSecret: credentials.clientSecret,
clientId: credentials.clientId,
tenantId: credentials.tenantId
}
};
}
break;
default:
return base;
}
return base;
};
export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -43,41 +107,36 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) =>
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection
? {
...appConnection,
tenantId: appConnection.credentials.tenantId
}
: {
app: AppConnection.AzureAppConfiguration,
method: AzureAppConfigurationConnectionMethod.OAuth
}
defaultValues: getDefaultValues(appConnection)
});
const {
handleSubmit,
control,
watch,
setValue,
formState: { isSubmitting, isDirty }
} = form;
const selectedMethod = watch("method");
const onSubmit = (formData: FormData) => {
setIsRedirecting(true);
const onSubmitHandler = async (formData: FormData) => {
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureAppConfigurationConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
switch (formData.method) {
case AzureAppConfigurationConnectionMethod.OAuth:
setIsRedirecting(true);
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureAppConfigurationConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-app-configuration`
);
break;
case AzureAppConfigurationConnectionMethod.ClientSecret:
await onSubmit(formData);
break;
default:
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
}
@@ -89,6 +148,9 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) =>
case AzureAppConfigurationConnectionMethod.OAuth:
isMissingConfig = !oauthClientId;
break;
case AzureAppConfigurationConnectionMethod.ClientSecret:
isMissingConfig = false;
break;
default:
throw new Error(`Unhandled Azure Connection method: ${selectedMethod}`);
}
@@ -97,25 +159,8 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) =>
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(onSubmitHandler)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="tenantId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The Azure Active Directory (Entra ID) Tenant ID."
isError={Boolean(error?.message)}
label="Tenant ID"
isOptional
errorText={error?.message}
>
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
</FormControl>
)}
/>
<Controller
name="method"
control={control}
@@ -155,6 +200,65 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) =>
</FormControl>
)}
/>
<Controller
name="tenantId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The Azure Active Directory (Entra ID) Tenant ID."
isError={Boolean(error?.message)}
label="Tenant ID"
isOptional={selectedMethod === AzureAppConfigurationConnectionMethod.OAuth}
errorText={error?.message}
>
<Input
{...field}
placeholder="00000000-0000-0000-0000-000000000000"
onChange={(e) => {
field.onChange(e.target.value);
setValue("credentials.tenantId", e.target.value);
}}
/>
</FormControl>
)}
/>
{/* Client Secret-specific fields */}
{selectedMethod === AzureAppConfigurationConnectionMethod.ClientSecret && (
<>
<Controller
name="credentials.clientId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Client ID"
errorText={error?.message}
>
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
</FormControl>
)}
/>
<Controller
name="credentials.clientSecret"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Client Secret"
errorText={error?.message}
>
<Input
{...field}
type="password"
placeholder="~JzD8e6S.tH~w8XRaNnKcb7W1fM4rCns7FY"
/>
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex items-center">
<Button
className="mr-4"
@@ -121,7 +121,7 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
const selectedMethod = watch("method");
const onSubmitHandler = (formData: FormData) => {
const onSubmitHandler = async (formData: FormData) => {
const state = crypto.randomBytes(16).toString("hex");
switch (formData.method) {
case AzureClientSecretsConnectionMethod.OAuth:
@@ -137,7 +137,7 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
break;
case AzureClientSecretsConnectionMethod.ClientSecret:
onSubmit(formData);
await onSubmit(formData);
break;
default:
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
@@ -240,7 +240,11 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
label="Client Secret"
errorText={error?.message}
>
<Input {...field} type="password" placeholder="Enter your Client Secret" />
<Input
{...field}
type="password"
placeholder="~JzD8e6S.tH~w8XRaNnKcb7W1fM4rCns7FY"
/>
</FormControl>
)}
/>
@@ -21,13 +21,6 @@ import {
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type AccessTokenForm = z.infer<typeof accessTokenSchema>;
type Props = {
appConnection?: TAzureDevOpsConnection;
onSubmit: (formData: AccessTokenForm) => Promise<void>;
};
// Base schema with common fields
const baseSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.AzureDevOps),
@@ -49,10 +42,30 @@ const accessTokenSchema = baseSchema.extend({
})
});
const clientSecretSchema = baseSchema.extend({
method: z.literal(AzureDevOpsConnectionMethod.ClientSecret),
credentials: z.object({
clientSecret: z.string().trim().min(1, "Client Secret is required"),
tenantId: z.string().trim().min(1, "Tenant ID is required"),
clientId: z.string().trim().min(1, "Client ID is required"),
orgName: z.string().trim().min(1, "Organization name is required")
})
});
// Union schema
const formSchema = z.discriminatedUnion("method", [oauthSchema, accessTokenSchema]);
const formSchema = z.discriminatedUnion("method", [
oauthSchema,
accessTokenSchema,
clientSecretSchema
]);
type FormData = z.infer<typeof formSchema>;
type OnSubmitForm = z.infer<typeof accessTokenSchema> | z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureDevOpsConnection;
onSubmit: (formData: OnSubmitForm) => Promise<void>;
};
const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormData> => {
if (!appConnection) {
@@ -93,6 +106,25 @@ const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormD
};
}
break;
case AzureDevOpsConnectionMethod.ClientSecret:
if (
"clientSecret" in credentials &&
"tenantId" in credentials &&
"clientId" in credentials &&
"orgName" in credentials
) {
return {
...base,
method: AzureDevOpsConnectionMethod.ClientSecret,
credentials: {
clientSecret: credentials.clientSecret,
tenantId: credentials.tenantId,
clientId: credentials.clientId,
orgName: credentials.orgName
}
};
}
break;
default:
return base;
}
@@ -118,7 +150,8 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
handleSubmit,
control,
watch,
formState: { isSubmitting, isDirty }
formState: { isSubmitting, isDirty },
setValue
} = form;
const selectedMethod = watch("method");
@@ -138,11 +171,12 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-devops`
);
break;
case AzureDevOpsConnectionMethod.AccessToken:
onSubmit(formData);
await onSubmit(formData);
break;
case AzureDevOpsConnectionMethod.ClientSecret:
await onSubmit(formData);
break;
default:
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
}
@@ -196,8 +230,8 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
)}
/>
{/* OAuth-specific fields */}
{selectedMethod === AzureDevOpsConnectionMethod.OAuth && (
{(selectedMethod === AzureDevOpsConnectionMethod.OAuth ||
selectedMethod === AzureDevOpsConnectionMethod.ClientSecret) && (
<>
<Controller
name="tenantId"
@@ -209,7 +243,14 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
label="Tenant ID"
errorText={error?.message}
>
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
<Input
{...field}
placeholder="00000000-0000-0000-0000-000000000000"
onChange={(e) => {
field.onChange(e.target.value);
setValue("credentials.tenantId", e.target.value);
}}
/>
</FormControl>
)}
/>
@@ -223,7 +264,50 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
label="Organization Name"
errorText={error?.message}
>
<Input {...field} placeholder="myorganization" />
<Input
{...field}
placeholder="myorganization"
onChange={(e) => {
field.onChange(e.target.value);
setValue("credentials.orgName", e.target.value);
}}
/>
</FormControl>
)}
/>
</>
)}
{/* Client Secret-specific fields */}
{selectedMethod === AzureDevOpsConnectionMethod.ClientSecret && (
<>
<Controller
name="credentials.clientId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Client ID"
errorText={error?.message}
>
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
</FormControl>
)}
/>
<Controller
name="credentials.clientSecret"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Client Secret"
errorText={error?.message}
>
<Input
{...field}
type="password"
placeholder="~JzD8e6S.tH~w8XRaNnKcb7W1fM4rCns7FY"
/>
</FormControl>
)}
/>
@@ -20,19 +20,83 @@ import {
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureKeyVaultConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
const baseSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.AzureKeyVault),
method: z.nativeEnum(AzureKeyVaultConnectionMethod),
tenantId: z.string().trim().optional()
method: z.nativeEnum(AzureKeyVaultConnectionMethod)
});
const oauthSchema = baseSchema.extend({
tenantId: z.string().trim().min(1, "Tenant ID is required"),
method: z.literal(AzureKeyVaultConnectionMethod.OAuth)
});
const clientSecretSchema = baseSchema.extend({
method: z.literal(AzureKeyVaultConnectionMethod.ClientSecret),
credentials: z.object({
clientSecret: z.string().trim().min(1, "Client Secret is required"),
clientId: z.string().trim().min(1, "Client ID is required"),
tenantId: z.string().trim().min(1, "Tenant ID is required")
})
});
const formSchema = z.discriminatedUnion("method", [oauthSchema, clientSecretSchema]);
type FormData = z.infer<typeof formSchema>;
export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
const getDefaultValues = (appConnection?: TAzureKeyVaultConnection): Partial<FormData> => {
if (!appConnection) {
return {
app: AppConnection.AzureKeyVault,
method: AzureKeyVaultConnectionMethod.OAuth
};
}
const base = {
name: appConnection.name,
description: appConnection.description,
app: appConnection.app,
method: appConnection.method
};
const { credentials } = appConnection;
switch (appConnection.method) {
case AzureKeyVaultConnectionMethod.OAuth:
if ("tenantId" in credentials) {
return {
...base,
method: AzureKeyVaultConnectionMethod.OAuth,
tenantId: credentials.tenantId
};
}
break;
case AzureKeyVaultConnectionMethod.ClientSecret:
if ("clientSecret" in credentials && "clientId" in credentials) {
return {
...base,
method: AzureKeyVaultConnectionMethod.ClientSecret,
credentials: {
clientSecret: credentials.clientSecret,
clientId: credentials.clientId,
tenantId: credentials.tenantId
}
};
}
break;
default:
return base;
}
return base;
};
export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -43,41 +107,37 @@ export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection
? {
...appConnection,
tenantId: appConnection.credentials.tenantId
}
: {
app: AppConnection.AzureKeyVault,
method: AzureKeyVaultConnectionMethod.OAuth
}
defaultValues: getDefaultValues(appConnection)
});
const {
handleSubmit,
control,
watch,
setValue,
formState: { isSubmitting, isDirty }
} = form;
const selectedMethod = watch("method");
const onSubmit = (formData: FormData) => {
setIsRedirecting(true);
const onSubmitHandler = async (formData: FormData) => {
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureKeyVaultConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
switch (formData.method) {
case AzureKeyVaultConnectionMethod.OAuth:
setIsRedirecting(true);
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureKeyVaultConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault`
);
break;
case AzureKeyVaultConnectionMethod.ClientSecret:
await onSubmit(formData);
break;
default:
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
}
@@ -89,6 +149,9 @@ export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
case AzureKeyVaultConnectionMethod.OAuth:
isMissingConfig = !oauthClientId;
break;
case AzureKeyVaultConnectionMethod.ClientSecret:
isMissingConfig = false;
break;
default:
throw new Error(`Unhandled Azure Connection method: ${selectedMethod}`);
}
@@ -97,25 +160,9 @@ export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(onSubmitHandler)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="tenantId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The Azure Active Directory (Entra ID) Tenant ID."
isError={Boolean(error?.message)}
label="Tenant ID"
isOptional
errorText={error?.message}
>
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
</FormControl>
)}
/>
<Controller
name="method"
control={control}
@@ -155,6 +202,66 @@ export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
</FormControl>
)}
/>
<Controller
name="tenantId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The Azure Active Directory (Entra ID) Tenant ID."
isError={Boolean(error?.message)}
label="Tenant ID"
isOptional={selectedMethod === AzureKeyVaultConnectionMethod.OAuth}
errorText={error?.message}
>
<Input
{...field}
placeholder="00000000-0000-0000-0000-000000000000"
onChange={(e) => {
field.onChange(e.target.value);
setValue("credentials.tenantId", e.target.value);
}}
/>
</FormControl>
)}
/>
{/* Client Secret-specific fields */}
{selectedMethod === AzureKeyVaultConnectionMethod.ClientSecret && (
<>
<Controller
name="credentials.clientId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Client ID"
errorText={error?.message}
>
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
</FormControl>
)}
/>
<Controller
name="credentials.clientSecret"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Client Secret"
errorText={error?.message}
>
<Input
{...field}
type="password"
placeholder="~JzD8e6S.tH~w8XRaNnKcb7W1fM4rCns7FY"
/>
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex items-center">
<Button
className="mr-4"
@@ -3,11 +3,31 @@ import crypto from "crypto";
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
FormControl,
Input,
ModalClose,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { useSubscription } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { gatewaysQueryKeys } from "@app/hooks/api";
import {
GitHubConnectionMethod,
TGitHubConnection,
@@ -26,7 +46,12 @@ type Props = {
const formSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.GitHub),
method: z.nativeEnum(GitHubConnectionMethod)
method: z.nativeEnum(GitHubConnectionMethod),
credentials: z
.object({
host: z.string().optional()
})
.optional()
});
type FormData = z.infer<typeof formSchema>;
@@ -44,7 +69,8 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.GitHub,
method: GitHubConnectionMethod.App
method: GitHubConnectionMethod.App,
gatewayId: null
}
});
@@ -55,6 +81,9 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
formState: { isSubmitting, isDirty }
} = form;
const { subscription } = useSubscription();
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const selectedMethod = watch("method");
const onSubmit = (formData: FormData) => {
@@ -66,15 +95,20 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
const githubHost =
formData.credentials?.host && formData.credentials.host.length > 0
? `https://${formData.credentials.host}`
: "https://github.com";
switch (formData.method) {
case GitHubConnectionMethod.App:
window.location.assign(
`https://github.com/apps/${appClientSlug}/installations/new?state=${state}`
`${githubHost}/apps/${appClientSlug}/installations/new?state=${state}`
);
break;
case GitHubConnectionMethod.OAuth:
window.location.assign(
`https://github.com/login/oauth/authorize?client_id=${oauthClientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/organization/app-connections/github/oauth/callback&state=${state}`
`${githubHost}/login/oauth/authorize?client_id=${oauthClientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/organization/app-connections/github/oauth/callback&state=${state}`
);
break;
default:
@@ -141,6 +175,81 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
</FormControl>
)}
/>
{subscription.gateway && (
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="enterprise-options" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">GitHub Enterprise Options</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0">
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
<Controller
name="credentials.host"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Hostname"
isOptional
>
<Input {...field} placeholder="github.com" />
</FormControl>
)}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
<div className="mt-8 flex items-center">
<Button
className="mr-4"
@@ -6,6 +6,7 @@ import { z } from "zod";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
import { useSubscription } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
@@ -78,6 +79,7 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
formState: { isSubmitting, isDirty }
} = form;
const { subscription } = useSubscription();
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
@@ -99,55 +101,57 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
}}
>
{!isUpdate && <GenericAppConnectionsFields />}
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
{subscription.gateway && (
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
)}
<Controller
name="method"
control={control}
@@ -6,6 +6,7 @@ import { z } from "zod";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
import { useSubscription } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
@@ -75,6 +76,7 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
formState: { isSubmitting, isDirty }
} = form;
const { subscription } = useSubscription();
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
@@ -96,55 +98,57 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
}}
>
{!isUpdate && <GenericAppConnectionsFields />}
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
{subscription.gateway && (
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
)}
<Controller
name="method"
control={control}
@@ -6,6 +6,7 @@ import { z } from "zod";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
import { useSubscription } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
@@ -75,6 +76,7 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => {
formState: { isSubmitting, isDirty }
} = form;
const { subscription } = useSubscription();
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
@@ -96,55 +98,57 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => {
}}
>
{!isUpdate && <GenericAppConnectionsFields />}
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
{subscription.gateway && (
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
)}
<Controller
name="method"
control={control}
@@ -6,6 +6,7 @@ import { z } from "zod";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
import { useSubscription } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
@@ -75,6 +76,7 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => {
formState: { isSubmitting, isDirty }
} = form;
const { subscription } = useSubscription();
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
@@ -96,55 +98,57 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => {
}}
>
{!isUpdate && <GenericAppConnectionsFields />}
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
{subscription.gateway && (
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
)}
<Controller
name="method"
control={control}
@@ -30,7 +30,8 @@ type BaseFormData = {
isUpdate?: boolean;
};
type GithubFormData = BaseFormData & Pick<TGitHubConnection, "name" | "method" | "description">;
type GithubFormData = BaseFormData &
Pick<TGitHubConnection, "name" | "method" | "description" | "gatewayId" | "credentials">;
type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
@@ -395,7 +396,7 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitHub);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, gatewayId, credentials } = formData;
try {
if (connectionId) {
@@ -406,14 +407,18 @@ export const OAuthCallbackPage = () => {
connectionId,
credentials: {
code: code as string,
installationId: installationId as string
}
installationId: installationId as string,
...(credentials?.host && { host: credentials.host })
},
gatewayId
}
: {
connectionId,
credentials: {
code: code as string
}
code: code as string,
...(credentials?.host && { host: credentials.host })
},
gatewayId
})
});
} else {
@@ -426,14 +431,18 @@ export const OAuthCallbackPage = () => {
method: GitHubConnectionMethod.App,
credentials: {
code: code as string,
installationId: installationId as string
}
installationId: installationId as string,
...(credentials?.host && { host: credentials.host })
},
gatewayId
}
: {
method: GitHubConnectionMethod.OAuth,
credentials: {
code: code as string
}
code: code as string,
...(credentials?.host && { host: credentials.host })
},
gatewayId
})
});
}
@@ -32,6 +32,13 @@ export const PmtMethodsTable = () => {
const handleDeletePmtMethodBtnClick = async () => {
if (!currentOrg?.id || !pmtMethodToRemove) return;
if (data?.length === 1) {
createNotification({
type: "error",
text: "You must have at least one payment method"
});
return;
}
try {
await deleteOrgPmtMethod.mutateAsync({
organizationId: currentOrg.id,
@@ -2,14 +2,11 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { NewProjectModal } from "@app/components/projects";
import { PageHeader } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { usePopUp } from "@app/hooks/usePopUp";
import { AllProjectView } from "./components/AllProjectView";
@@ -38,8 +35,6 @@ export const ProjectsPage = () => {
"upgradePlan"
] as const);
const { data: serverDetails, isLoading } = useFetchServerStatus();
const { subscription } = useSubscription();
const isAddingProjectsAllowed = subscription?.workspaceLimit
@@ -52,30 +47,6 @@ export const ProjectsPage = () => {
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
{!isLoading && !serverDetails?.redisConfigured && (
<div className="mb-4 flex flex-col items-start justify-start text-3xl">
<p className="mb-4 mr-4 font-semibold text-white">Announcements</p>
<div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100">
<FontAwesomeIcon
icon={faExclamationCircle}
className="mr-4 p-4 text-2xl text-mineshaft-50"
/>
Attention: Updated versions of Infisical now require Redis for full functionality. Learn
how to configure it
<a
href="https://infisical.com/docs/self-hosting/configuration/redis"
rel="noopener noreferrer"
target="_blank"
>
<span className="cursor-pointer pl-1 text-white underline underline-offset-2 duration-100 hover:text-blue-200 hover:decoration-blue-400">
here
</span>
</a>
.
</div>
</div>
)}
<div className="mb-4 flex flex-col items-start justify-start">
<PageHeader
title={
@@ -59,7 +59,6 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { useGetReminder } from "@app/hooks/api/reminders";
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { camelCaseToSpaces } from "@app/lib/fn/string";
@@ -269,9 +268,9 @@ export const SecretDetailSidebar = ({
) => {
switch (actorType) {
case ActorType.USER:
return `/${ProjectType.SecretManager}/${currentWorkspace.id}/members/${membershipId}`;
return `/projects/secret-management/${currentWorkspace.id}/members/${membershipId}`;
case ActorType.IDENTITY:
return `/${ProjectType.SecretManager}/${currentWorkspace.id}/identities/${actorId}`;
return `/projects/secret-management/${currentWorkspace.id}/identities/${actorId}`;
default:
return null;
}
@@ -1,12 +1,17 @@
import { useServerConfig } from "@app/context";
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { EnvironmentSection } from "../EnvironmentSection";
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
import { SecretDetectionIgnoreValuesSection } from "../SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection";
import { SecretSharingSection } from "../SecretSharingSection";
import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection";
import { SecretTagsSection } from "../SecretTagsSection";
export const SecretSettingsTab = () => {
const { config } = useServerConfig();
return (
<div>
<EnvironmentSection />
@@ -15,6 +20,7 @@ export const SecretSettingsTab = () => {
<SecretSharingSection />
<SecretSnapshotsLegacySection />
<PointInTimeVersionLimitSection />
{config.paramsFolderSecretDetectionEnabled && <SecretDetectionIgnoreValuesSection />}
<BackfillSecretReferenceSecretion />
</div>
);
@@ -0,0 +1,144 @@
import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { useUpdateProject } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
const formSchema = z.object({
ignoreValues: z
.object({
value: z.string().trim().min(1, "Secret value is required")
})
.array()
.default([])
});
type TForm = z.infer<typeof formSchema>;
export const SecretDetectionIgnoreValuesSection = () => {
const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission();
const { mutateAsync: updateProject } = useUpdateProject();
const {
control,
formState: { isSubmitting, isDirty },
handleSubmit,
reset
} = useForm<TForm>({
resolver: zodResolver(formSchema),
defaultValues: {
ignoreValues: []
}
});
const ignoreValuesFormFields = useFieldArray({
control,
name: "ignoreValues"
});
useEffect(() => {
const existingIgnoreValues = currentWorkspace?.secretDetectionIgnoreValues || [];
reset({
ignoreValues:
existingIgnoreValues.length > 0
? existingIgnoreValues.map((value) => ({ value }))
: [{ value: "" }] // Show one empty field by default
});
}, [currentWorkspace?.secretDetectionIgnoreValues, reset]);
const handleIgnoreValuesSubmit = async ({ ignoreValues }: TForm) => {
try {
await updateProject({
projectID: currentWorkspace.id,
secretDetectionIgnoreValues: ignoreValues.map((item) => item.value)
});
createNotification({
text: "Successfully updated secret detection ignore values",
type: "success"
});
} catch {
createNotification({
text: "Failed updating secret detection ignore values",
type: "error"
});
}
};
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
if (!currentWorkspace) return null;
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">Secret Detection</p>
</div>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">Define secret values to ignore when scanning designated parameter folders. Add values here to prevent false positives or allow approved sensitive data. These ignored values will not trigger policy violation alerts.</p>
<form onSubmit={handleSubmit(handleIgnoreValuesSubmit)} autoComplete="off">
<div className="mb-4">
<div className="flex flex-col space-y-2">
{ignoreValuesFormFields.fields.map(({ id: ignoreValueFieldId }, i) => (
<div key={ignoreValueFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Secret Value</span>}
<Controller
control={control}
name={`ignoreValues.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} placeholder="sk-1234567890abcdef" isDisabled={!isAdmin} />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete ignore value"
className="bottom-0.5 h-9"
variant="outline_bg"
onClick={() => ignoreValuesFormFields.remove(i)}
isDisabled={!isAdmin}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div className="mt-2 flex justify-end">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => ignoreValuesFormFields.append({ value: "" })}
isDisabled={!isAdmin}
>
Add value to ignore
</Button>
</div>
</div>
</div>
<Button
colorSchema="secondary"
type="submit"
isLoading={isSubmitting}
disabled={!isAdmin || !isDirty}
>
Save
</Button>
</form>
</div>
);
};