Merge branch 'heads/main' into daniel/srp-removal-round-2

This commit is contained in:
Daniel Hougaard
2025-07-30 04:44:57 +04:00
474 changed files with 5372 additions and 28647 deletions
@@ -22,7 +22,6 @@ export const KmipPage = () => {
description="Integrate with Infisical KMS via Key Management Interoperability Protocol."
/>
<ProjectPermissionCan
passThrough={false}
renderGuardBanner
I={ProjectPermissionKmipActions.ReadClients}
a={ProjectPermissionSub.Kmip}
@@ -22,7 +22,6 @@ export const OverviewPage = () => {
description="Manage keys and perform cryptographic operations."
/>
<ProjectPermissionCan
passThrough={false}
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Cmek}
@@ -114,7 +114,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureClientSecrets:
return <AzureClientSecretsConnectionForm />;
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureDevOps:
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
case AppConnection.Windmill:
@@ -222,7 +222,7 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.AzureClientSecrets:
return <AzureClientSecretsConnectionForm appConnection={appConnection} />;
return <AzureClientSecretsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureDevOps:
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.Windmill:
@@ -1,3 +1,4 @@
/* eslint-disable no-case-declarations */
import crypto from "crypto";
import { useState } from "react";
@@ -20,19 +21,83 @@ import {
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureClientSecretsConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
const baseSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.AzureClientSecrets),
method: z.nativeEnum(AzureClientSecretsConnectionMethod),
tenantId: z.string().trim().min(1, "Tenant ID is required")
method: z.nativeEnum(AzureClientSecretsConnectionMethod)
});
const oauthSchema = baseSchema.extend({
tenantId: z.string().trim().min(1, "Tenant ID is required"),
method: z.literal(AzureClientSecretsConnectionMethod.OAuth)
});
const clientSecretSchema = baseSchema.extend({
method: z.literal(AzureClientSecretsConnectionMethod.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 AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partial<FormData> => {
if (!appConnection) {
return {
app: AppConnection.AzureClientSecrets,
method: AzureClientSecretsConnectionMethod.OAuth
};
}
const base = {
name: appConnection.name,
description: appConnection.description,
app: appConnection.app,
method: appConnection.method
};
const { credentials } = appConnection;
switch (appConnection.method) {
case AzureClientSecretsConnectionMethod.OAuth:
if ("tenantId" in credentials) {
return {
...base,
method: AzureClientSecretsConnectionMethod.OAuth,
tenantId: credentials.tenantId
};
}
break;
case AzureClientSecretsConnectionMethod.ClientSecret:
if ("clientSecret" in credentials && "clientId" in credentials) {
return {
...base,
method: AzureClientSecretsConnectionMethod.ClientSecret,
credentials: {
clientSecret: credentials.clientSecret,
clientId: credentials.clientId,
tenantId: credentials.tenantId
}
};
}
break;
default:
return base;
}
return base;
};
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -43,70 +108,51 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection
? {
...appConnection,
tenantId: appConnection.credentials.tenantId
}
: {
app: AppConnection.AzureClientSecrets,
method: AzureClientSecretsConnectionMethod.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 = (formData: FormData) => {
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureClientSecretsConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
switch (formData.method) {
case AzureClientSecretsConnectionMethod.OAuth:
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-client-secrets`
setIsRedirecting(true);
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureClientSecretsConnectionFormData",
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://graph.microsoft.com/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
);
break;
case AzureClientSecretsConnectionMethod.ClientSecret:
onSubmit(formData);
break;
default:
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
}
};
const isMissingConfig = !oauthClientId;
const isMissingConfig =
selectedMethod === AzureClientSecretsConnectionMethod.OAuth && !oauthClientId;
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
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 Directory (tenant) ID."
isError={Boolean(error?.message)}
label="Tenant ID"
errorText={error?.message}
>
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
</FormControl>
)}
/>
<Controller
name="method"
control={control}
@@ -146,6 +192,61 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
</FormControl>
)}
/>
<Controller
name="tenantId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The Directory (tenant) ID."
isError={Boolean(error?.message)}
label="Tenant ID"
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>
)}
/>
{/* Access Token-specific fields */}
{selectedMethod === AzureClientSecretsConnectionMethod.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="Enter your Client Secret" />
</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,
host: credentials.host
},
gatewayId
}
: {
connectionId,
credentials: {
code: code as string
}
code: code as string,
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,
host: credentials.host
},
gatewayId
}
: {
method: GitHubConnectionMethod.OAuth,
credentials: {
code: code as string
}
code: code as string,
host: credentials.host
},
gatewayId
})
});
}
@@ -29,6 +29,7 @@ import {
} from "@app/hooks/api/auditLogs/constants";
import { EventType } from "@app/hooks/api/auditLogs/enums";
import { UserAgentType } from "@app/hooks/api/auth/types";
import { Workspace } from "@app/hooks/api/workspace/types";
import { LogFilterItem } from "./LogFilterItem";
import { auditLogFilterFormSchema, Presets, TAuditLogFilterFormData } from "./types";
@@ -43,6 +44,7 @@ type Props = {
presets?: Presets;
setFilter: (data: TAuditLogFilterFormData) => void;
filter: TAuditLogFilterFormData;
project?: Workspace;
};
const getActiveFilterCount = (filter: TAuditLogFilterFormData) => {
@@ -69,7 +71,7 @@ const getActiveFilterCount = (filter: TAuditLogFilterFormData) => {
return filterCount;
};
export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
const { data: workspaces = [] } = useGetUserWorkspaces();
const { currentOrg } = useOrganization();
@@ -90,7 +92,7 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
values: filter
});
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
const selectedProject = watch("project");
const selectedProject = project ?? watch("project");
const showSecretsSection =
selectedEventTypes?.some(
@@ -270,48 +272,50 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
)}
/>
</LogFilterItem>
<LogFilterItem
label="Project"
onClear={() => {
resetField("project");
resetField("environment");
setValue("secretPath", "");
setValue("secretKey", "");
}}
>
<Controller
control={control}
name="project"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="mb-0 w-full"
>
<FilterableSelect
value={value}
isClearable
onChange={(e) => {
if (e === null) {
setValue("secretPath", "");
setValue("secretKey", "");
}
resetField("environment");
onChange(e);
}}
placeholder="All projects"
options={workspacesInOrg.map(({ name, id, type }) => ({
name,
id,
type
}))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</LogFilterItem>
{!project && (
<LogFilterItem
label="Project"
onClear={() => {
resetField("project");
resetField("environment");
setValue("secretPath", "");
setValue("secretKey", "");
}}
>
<Controller
control={control}
name="project"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="mb-0 w-full"
>
<FilterableSelect
value={value}
isClearable
onChange={(e) => {
if (e === null) {
setValue("secretPath", "");
setValue("secretKey", "");
}
resetField("environment");
onChange(e);
}}
placeholder="All projects"
options={workspacesInOrg.map(({ name, id, type }) => ({
name,
id,
type
}))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</LogFilterItem>
)}
<AnimatePresence initial={false}>
{showSecretsSection && (
<motion.div
@@ -7,6 +7,7 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { Timezone } from "@app/helpers/datetime";
import { withPermission } from "@app/hoc";
import { Workspace } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { LogsDateFilter } from "./LogsDateFilter";
@@ -24,10 +25,11 @@ type Props = {
refetchInterval?: number;
showFilters?: boolean;
pageView?: boolean;
project?: Workspace;
};
export const LogsSection = withPermission(
({ presets, refetchInterval, showFilters = true, pageView = false }: Props) => {
({ presets, refetchInterval, showFilters = true, pageView = false, project }: Props) => {
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -83,7 +85,12 @@ export const LogsSection = withPermission(
/>
)}
{showFilters && (
<LogsFilter presets={presets} setFilter={setLogFilter} filter={logFilter} />
<LogsFilter
project={project}
presets={presets}
setFilter={setLogFilter}
filter={logFilter}
/>
)}
</div>
</div>
@@ -94,7 +101,7 @@ export const LogsSection = withPermission(
secretPath: logFilter.secretPath || undefined,
secretKey: logFilter.secretKey || undefined,
eventMetadata: logFilter?.eventMetadata,
projectId: logFilter?.project?.id,
projectId: project?.id || logFilter?.project?.id,
actorType: presets?.actorType,
limit: 15,
eventType: logFilter?.eventType,
@@ -119,7 +126,7 @@ export const LogsSection = withPermission(
return (
<div className="space-y-2">
<div className="flex w-full justify-end">
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
{showFilters && (
<LogsDateFilter
filter={dateFilter}
@@ -52,7 +52,7 @@ 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>
@@ -9,7 +9,7 @@ import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { SelectImportFromPlatformModal } from "./components/SelectImportFromPlatformModal";
export const ImportTab = () => {
export const ExternalMigrationsTab = () => {
const { membership } = useOrgPermission();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const);
@@ -30,7 +30,7 @@ export const ImportTab = () => {
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/guides/migrating-from-envkey"
href="https://infisical.com/docs/documentation/platform/external-migrations/overview"
>
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
@@ -45,25 +45,21 @@ export const EnvKeyPlatformModal = ({ onClose }: Props) => {
return;
}
try {
await importEnvKey({
file: data.file,
decryptionKey: data.encryptionKey
});
createNotification({
title: "Import started",
text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.",
type: "info"
});
await importEnvKey({
file: data.file,
decryptionKey: data.encryptionKey
});
createNotification({
title: "Import started",
text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.",
type: "info"
});
onClose();
reset();
onClose();
reset();
if (fileUploadRef.current) {
fileUploadRef.current.value = "";
}
} catch {
reset();
if (fileUploadRef.current) {
fileUploadRef.current.value = "";
}
};
@@ -1,11 +1,12 @@
import { useState } from "react";
import { faKey } from "@fortawesome/free-solid-svg-icons";
import { faKey, faVault } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { Modal, ModalContent } from "@app/components/v2";
import { EnvKeyPlatformModal } from "./EnvKeyPlatformModal";
import { VaultPlatformModal } from "./VaultPlatformModal";
type Props = {
isOpen?: boolean;
@@ -22,6 +23,11 @@ const PLATFORM_LIST = [
icon: faKey,
platform: "env-key",
title: "Env Key"
},
{
icon: faVault,
platform: "vault",
title: "Vault"
}
] as const;
@@ -82,18 +88,32 @@ export const SelectImportFromPlatformModal = ({ isOpen, onToggle }: Props) => {
</div>
</motion.div>
)}
{wizardStep === WizardSteps.PlatformInputs &&
selectedPlatform?.platform === "env-key" && (
<motion.div
key="env-key-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<EnvKeyPlatformModal onClose={() => handleFormReset(false)} />
</motion.div>
)}
{wizardStep === WizardSteps.PlatformInputs && (
<>
{selectedPlatform?.platform === "env-key" && (
<motion.div
key="env-key-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<EnvKeyPlatformModal onClose={() => handleFormReset(false)} />
</motion.div>
)}
{selectedPlatform?.platform === "vault" && (
<motion.div
key="vault-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<VaultPlatformModal onClose={() => handleFormReset(false)} />
</motion.div>
)}
</>
)}
</AnimatePresence>
</ModalContent>
</Modal>
@@ -0,0 +1,222 @@
import { Controller, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Tooltip } from "@app/components/v2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { useImportVault } from "@app/hooks/api/migration/mutations";
type Props = {
id?: string;
onClose: () => void;
};
enum VaultMappingType {
KeyVault = "key-vault",
Namespace = "namespace"
}
const MAPPING_TYPE_MENU_ITEMS = [
{
value: VaultMappingType.KeyVault,
label: "Key Vaults",
tooltip: (
<div>
When using key vaults for mapping, each key vault within Vault will be created in Infisical
as a project. Each secret path inside the key vault, will be created as an environment
inside the corresponding project. When using Key Vaults as the project mapping type, a
default environment called &quot;Production&quot; will be created for each project, which
will contain the secrets from the key vault.
<div className="mt-4 flex flex-col gap-1 text-sm">
<div>Key Vault → Project</div>
<div>Default Environment (Production)</div>
<div>Secret Path → Secret Folder</div>
<div>Secret data → Secrets</div>
</div>
</div>
)
},
{
value: VaultMappingType.Namespace,
label: "Namespaces",
tooltip: (
<div>
When using namespaces for mapping, each namespace within Vault will be created in Infisical
as a project. Each key vault (KV) inside the namespace, will be created as an environment
inside the corresponding project.
<div className="mt-4 flex flex-col gap-1 text-sm">
<div>Namespace → Project</div>
<div>Key Vault → Project Environment</div>
<div>Secret Path → Secret Folder</div>
<div>Secret data → Secrets</div>
</div>
</div>
)
}
];
export const VaultPlatformModal = ({ onClose }: Props) => {
const formSchema = z.object({
vaultUrl: z.string().min(1),
vaultNamespace: z.string().trim().optional(),
vaultAccessToken: z.string().min(1),
mappingType: z.nativeEnum(VaultMappingType).default(VaultMappingType.KeyVault)
});
type TFormData = z.infer<typeof formSchema>;
const { mutateAsync: importVault } = useImportVault();
const {
control,
handleSubmit,
reset,
formState: { isLoading, isDirty, isSubmitting, isValid, errors }
} = useForm<TFormData>({
resolver: zodResolver(formSchema)
});
console.log({
isSubmitting,
isLoading,
isValid,
errors
});
const onSubmit = async (data: TFormData) => {
await importVault({
vaultAccessToken: data.vaultAccessToken,
vaultNamespace: data.vaultNamespace,
vaultUrl: data.vaultUrl,
mappingType: data.mappingType
});
createNotification({
title: "Import started",
text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.",
type: "info"
});
onClose();
reset();
};
return (
<div>
<NoticeBannerV2 title="Vault KV Secret Engine Import" className="mb-4">
<p className="text-sm">
The Vault migration currently supports importing static secrets from Vault
Dedicated/Self-Hosted.
<div className="mt-2 text-xs opacity-80">
Currently only KV Secret Engine V2 is supported for Vault migrations.
</div>
</p>
</NoticeBannerV2>
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
<Controller
control={control}
name="vaultUrl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Vault URL"
isRequired
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="vaultNamespace"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Vault Namespace"
errorText={error?.message}
isError={Boolean(error)}
>
<Input type="text" placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="vaultAccessToken"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Vault Access Token"
isRequired
errorText={error?.message}
isError={Boolean(error)}
>
<Input type="password" placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="mappingType"
defaultValue={VaultMappingType.KeyVault}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Mapping"
isError={Boolean(error)}
isRequired
errorText={error?.message}
className="flex-1"
>
<div className="mt-2 grid h-full w-full grid-cols-2 gap-4">
{MAPPING_TYPE_MENU_ITEMS.map((el) => (
<div
key={el.value}
className={twMerge(
"flex w-full cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 p-4 opacity-75 transition-all",
field.value === el.value
? "border-primary-700 border-opacity-70 bg-mineshaft-600 opacity-100"
: "hover:border-primary-700 hover:bg-mineshaft-600"
)}
onClick={() => field.onChange(el.value)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter") {
field.onChange(el.value);
}
}}
>
<div className="flex items-center gap-1">
<div className="text-center text-sm">{el.label}</div>
{el.tooltip && (
<div className="text-center text-sm">
<Tooltip content={el.tooltip} className="max-w-96">
<FontAwesomeIcon className="opacity-60" icon={faQuestionCircle} />
</Tooltip>
</div>
)}
</div>
</div>
))}
</div>
</FormControl>
)}
/>
<div className="mt-6 flex items-center space-x-4">
<Button
type="submit"
isLoading={isLoading}
isDisabled={!isDirty || isSubmitting || isLoading || !isValid}
>
Import data
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form>
</div>
);
};
@@ -0,0 +1 @@
export { ExternalMigrationsTab } from "./ExternalMigrationsTab";
@@ -1 +0,0 @@
export { ImportTab } from "./ImportTab";
@@ -5,7 +5,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
import { ImportTab } from "../ImportTab";
import { ExternalMigrationsTab } from "../ExternalMigrationsTab";
import { KmipTab } from "../KmipTab/OrgKmipTab";
import { OrgEncryptionTab } from "../OrgEncryptionTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
@@ -39,7 +39,11 @@ export const OrgTabGroup = () => {
component: OrgWorkflowIntegrationTab
},
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
{ name: "Import", key: "tab-import", component: ImportTab },
{
name: "External Migrations",
key: "tab-external-migrations",
component: ExternalMigrationsTab
},
{
name: "Project Templates",
key: "project-templates",
@@ -155,8 +155,8 @@ export const SpecificPrivilegeSecretForm = ({
const selectablePaths = useMemo(() => {
if (!policies) return [];
const environmentPolicies = policies.filter(
(policy) => policy.environment.slug === selectedEnvironment
const environmentPolicies = policies.filter((policy) =>
policy.environments.find((env) => env.slug === selectedEnvironment)
);
privilegeForm.setValue("secretPath", "", {
@@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { LogsSection } from "@app/pages/organization/AuditLogsPage/components";
export const AuditLogsPage = () => {
const { currentWorkspace } = useWorkspace();
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>Project Audit Logs</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
title="Audit logs"
description="Audit logs for security and compliance teams to monitor information access."
/>
<LogsSection pageView project={currentWorkspace} />
</div>
</div>
</div>
);
};
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs"
)({
component: AuditLogsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Audit Logs"
}
]
};
}
});
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs"
)({
component: AuditLogsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Audit Logs"
}
]
};
}
});
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs"
)({
component: AuditLogsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Audit Logs"
}
]
};
}
});
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs"
)({
component: AuditLogsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Audit Logs"
}
]
};
}
});
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs"
)({
component: AuditLogsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Audit Logs"
}
]
};
}
});
@@ -1,5 +1,5 @@
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync";
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
@@ -10,13 +10,12 @@ import {
faArrowRight,
faArrowRightToBracket,
faArrowUp,
faFileImport,
faFilter,
faFingerprint,
faFolder,
faFolderBlank,
faFolderPlus,
faKey,
faList,
faPlus,
faRotate
} from "@fortawesome/free-solid-svg-icons";
@@ -96,7 +95,12 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
import {
SecretType,
SecretV3RawSanitized,
TSecretFolder,
WorkspaceEnv
} from "@app/hooks/api/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import {
useDynamicSecretOverview,
@@ -135,7 +139,6 @@ enum RowType {
Folder = "folder",
DynamicSecret = "dynamic",
Secret = "secret",
Import = "import",
SecretRotation = "rotation"
}
@@ -144,11 +147,10 @@ type Filter = {
};
const DEFAULT_FILTER_STATE = {
[RowType.Folder]: true,
[RowType.DynamicSecret]: true,
[RowType.Secret]: true,
[RowType.Import]: true,
[RowType.SecretRotation]: true
[RowType.Folder]: false,
[RowType.DynamicSecret]: false,
[RowType.Secret]: false,
[RowType.SecretRotation]: false
};
const DEFAULT_COLLAPSED_HEADER_HEIGHT = 120;
@@ -265,11 +267,8 @@ export const OverviewPage = () => {
)
);
const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs);
useEffect(() => {
setVisibleEnvs(userAvailableEnvs);
}, [userAvailableEnvs]);
const [filteredEnvs, setFilteredEnvs] = useState<WorkspaceEnv[]>([]);
const visibleEnvs = filteredEnvs.length ? filteredEnvs : userAvailableEnvs;
const {
secretImports,
@@ -282,6 +281,7 @@ export const OverviewPage = () => {
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
});
const isFilteredByResources = Object.values(filter).some(Boolean);
const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
{
projectId: workspaceId,
@@ -289,11 +289,11 @@ export const OverviewPage = () => {
secretPath,
orderDirection,
orderBy,
includeFolders: filter.folder,
includeDynamicSecrets: filter.dynamic,
includeSecrets: filter.secret,
includeImports: filter.import,
includeSecretRotations: filter.rotation,
includeFolders: isFilteredByResources ? filter.folder : true,
includeDynamicSecrets: isFilteredByResources ? filter.dynamic : true,
includeSecrets: isFilteredByResources ? filter.secret : true,
includeImports: true,
includeSecretRotations: isFilteredByResources ? filter.rotation : true,
search: debouncedSearchFilter,
limit,
offset
@@ -529,10 +529,10 @@ export const OverviewPage = () => {
};
const handleEnvSelect = (envId: string) => {
if (visibleEnvs.map((env) => env.id).includes(envId)) {
setVisibleEnvs(visibleEnvs.filter((env) => env.id !== envId));
if (filteredEnvs.map((env) => env.id).includes(envId)) {
setFilteredEnvs(filteredEnvs.filter((env) => env.id !== envId));
} else {
setVisibleEnvs(visibleEnvs.concat(userAvailableEnvs.filter((env) => env.id === envId)));
setFilteredEnvs(filteredEnvs.concat(userAvailableEnvs.filter((env) => env.id === envId)));
}
};
@@ -792,11 +792,11 @@ export const OverviewPage = () => {
envNames: string[],
envs: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]
): ProjectSecretsImportedBy[] => {
const filteredEnvs = envs.filter((env) => envNames.includes(env.environment));
const environments = envs.filter((env) => envNames.includes(env.environment));
if (filteredEnvs.length === 0) return [];
if (environments.length === 0) return [];
const allImportedBy = filteredEnvs.flatMap((env) => env.importedBy);
const allImportedBy = environments.flatMap((env) => env.importedBy);
const groupedBySlug: Record<string, ProjectSecretsImportedBy[]> = {};
allImportedBy.forEach((item) => {
@@ -902,9 +902,7 @@ export const OverviewPage = () => {
const isTableEmpty = totalCount === 0;
const isTableFiltered =
Boolean(Object.values(filter).filter((enabled) => !enabled).length) ||
userAvailableEnvs.length !== visibleEnvs.length;
const isTableFiltered = isFilteredByResources || filteredEnvs.length > 0;
if (!isProjectV3)
return (
@@ -969,26 +967,42 @@ export const OverviewPage = () => {
<div className="mt-4 flex items-center justify-between">
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
<div className="flex flex-row items-center justify-center space-x-2">
{isTableFiltered && (
<Button
variant="plain"
colorSchema="secondary"
onClick={() => {
setFilteredEnvs([]);
setFilter(DEFAULT_FILTER_STATE);
}}
>
Clear Filters
</Button>
)}
{userAvailableEnvs.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Environments"
variant="plain"
<Button
size="sm"
variant="outline_bg"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
"flex h-[2.5rem]",
isTableFiltered && "border-primary/40 bg-primary/10"
)}
leftIcon={
<FontAwesomeIcon
icon={faFilter}
className={isTableFiltered ? "text-primary/80" : undefined}
/>
}
>
<Tooltip content="Choose visible environments" className="mb-2">
<FontAwesomeIcon icon={faList} />
</Tooltip>
</IconButton>
Filters
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
align="end"
sideOffset={2}
>
{/* <DropdownMenuItem className="px-1.5" asChild>
<Button
@@ -1002,20 +1016,8 @@ export const OverviewPage = () => {
Create an environment
</Button>
</DropdownMenuItem> */}
<DropdownMenuLabel>Filter project resources</DropdownMenuLabel>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
handleToggleRowType(RowType.Import);
}}
icon={filter[RowType.Import] && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faFileImport} className="text-green-700" />
<span>Imports</span>
</div>
</DropdownMenuItem>
<DropdownMenuLabel>Filter by Resource</DropdownMenuLabel>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
@@ -1070,11 +1072,11 @@ export const OverviewPage = () => {
<span>Secrets</span>
</div>
</DropdownMenuItem>
<DropdownMenuLabel>Choose visible environments</DropdownMenuLabel>
<DropdownMenuLabel>Filter by Environment</DropdownMenuLabel>
{userAvailableEnvs.map((availableEnv) => {
const { id: envId, name } = availableEnv;
const isEnvSelected = visibleEnvs.map((env) => env.id).includes(envId);
const isEnvSelected = filteredEnvs.map((env) => env.id).includes(envId);
return (
<DropdownMenuItem
onClick={(e) => {
@@ -1082,7 +1084,6 @@ export const OverviewPage = () => {
handleEnvSelect(envId);
}}
key={envId}
disabled={visibleEnvs?.length === 1}
icon={isEnvSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
@@ -1405,13 +1406,6 @@ export const OverviewPage = () => {
className="bg-mineshaft-700"
/>
)}
{userAvailableEnvs.length > 0 && visibleEnvs.length === 0 && (
<Tr>
<Td colSpan={visibleEnvs.length + 1}>
<EmptyState title="You have no visible environments" iconSize="3x" />
</Td>
</Tr>
)}
{userAvailableEnvs.length === 0 && (
<Tr>
<Td colSpan={visibleEnvs.length + 1}>
@@ -1444,8 +1438,8 @@ export const OverviewPage = () => {
<Td colSpan={visibleEnvs.length + 1}>
<EmptyState
title={
debouncedSearchFilter
? "No secret found for your search, add one now"
isTableFiltered || debouncedSearchFilter
? "No secrets found for your search, add one now"
: "Let's add some secrets"
}
icon={faFolderBlank}
@@ -166,17 +166,20 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
const filteredPolicies = useMemo(
() =>
policies
.filter(({ policyType, environment, name, secretPath }) => {
.filter(({ policyType, environments, name, secretPath }) => {
if (filters.type && policyType !== filters.type) return false;
if (filters.environmentIds.length && !filters.environmentIds.includes(environment.id))
if (
filters.environmentIds.length &&
!environments.some((env) => filters.environmentIds.includes(env.id))
)
return false;
const searchValue = search.trim().toLowerCase();
return (
name.toLowerCase().includes(searchValue) ||
environment.name.toLowerCase().includes(searchValue) ||
environments.some((env) => env.name.toLowerCase().includes(searchValue)) ||
(secretPath ?? "*").toLowerCase().includes(searchValue)
);
})
@@ -189,9 +192,18 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
.toLowerCase()
.localeCompare(policyTwo.policyType.toLowerCase());
case PolicyOrderBy.Environment:
return policyOne.environment.name
.toLowerCase()
.localeCompare(policyTwo.environment.name.toLowerCase());
// eslint-disable-next-line no-case-declarations
const getFirstEnvName = (policy: { environments: { name: string }[] }) => {
if (!policy.environments?.length) return "";
return (
policy.environments
.map((env) => env.name?.toLowerCase() || "")
.filter((name) => name)
.sort()[0] || ""
);
};
return getFirstEnvName(policyOne).localeCompare(getFirstEnvName(policyTwo));
case PolicyOrderBy.SecretPath:
return (policyOne.secretPath ?? "*")
.toLowerCase()
@@ -54,7 +54,7 @@ type Props = {
const formSchema = z
.object({
environment: z.object({ slug: z.string(), name: z.string() }),
environments: z.array(z.object({ slug: z.string(), name: z.string() })).min(1),
name: z.string().optional(),
secretPath: z.string().trim().min(1),
approvals: z.number().min(1).default(1),
@@ -134,7 +134,7 @@ const Form = ({
values: editValues
? ({
...editValues,
environment: editValues.environment,
environments: editValues.environments,
userApprovers:
editValues?.approvers
?.filter((approver) => approver.type === ApproverType.User)
@@ -191,7 +191,7 @@ const Form = ({
const { currentWorkspace } = useWorkspace();
const { data: groups } = useListWorkspaceGroups(projectId);
const environments = currentWorkspace?.environments || [];
const availableEnvironments = currentWorkspace?.environments || [];
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
@@ -204,11 +204,11 @@ const Form = ({
const formUserBypassers = watch("userBypassers");
const formGroupBypassers = watch("groupBypassers");
const formEnvironment = watch("environment")?.slug;
const formEnvironments = watch("environments");
const bypasserCount = (formUserBypassers || []).length + (formGroupBypassers || []).length;
const handleCreatePolicy = async ({
environment,
environments,
groupApprovers,
userApprovers,
groupBypassers,
@@ -226,7 +226,7 @@ const Form = ({
...data,
approvers: [...userApprovers, ...groupApprovers],
bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug,
environments: environments.map((env) => env.slug),
workspaceId: currentWorkspace?.id || ""
});
} else {
@@ -242,7 +242,7 @@ const Form = ({
numberOfApprovals: el.approvals
})),
bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug,
environments: environments.map((env) => env.slug),
projectSlug
});
}
@@ -261,7 +261,7 @@ const Form = ({
};
const handleUpdatePolicy = async ({
environment,
environments,
userApprovers,
groupApprovers,
userBypassers,
@@ -281,7 +281,8 @@ const Form = ({
...data,
approvers: [...userApprovers, ...groupApprovers],
bypassers: bypassers.length > 0 ? bypassers : undefined,
workspaceId: currentWorkspace?.id || ""
workspaceId: currentWorkspace?.id || "",
environments: environments.map((env) => env.slug)
});
} else {
await updateAccessApprovalPolicy({
@@ -297,7 +298,7 @@ const Form = ({
numberOfApprovals: el.approvals
})),
bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug,
environments: environments.map((env) => env.slug),
projectSlug
});
}
@@ -479,28 +480,28 @@ const Form = ({
<SecretPathInput
{...field}
value={field.value || ""}
environment={formEnvironment}
environment={formEnvironments?.[0]?.slug || ""}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="environment"
name="environments"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
label="Environments"
isRequired
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
>
<FilterableSelect
isDisabled={isEditMode}
value={value}
isMulti
onChange={onChange}
placeholder="Select environment..."
options={environments}
placeholder="Select environments..."
options={availableEnvironments}
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
@@ -37,7 +37,7 @@ import { TWorkspaceUser } from "@app/hooks/api/users/types";
interface IPolicy {
id: string;
name: string;
environment: WorkspaceEnv;
environments: WorkspaceEnv[];
projectId?: string;
secretPath?: string;
approvals: number;
@@ -112,7 +112,7 @@ export const ApprovalPolicyRow = ({
onClick={() => setIsExpanded.toggle()}
>
<Td>{policy.name || <span className="text-mineshaft-400">Unnamed Policy</span>}</Td>
<Td>{policy.environment.name}</Td>
<Td>{policy.environments.map((env) => env.name).join(", ")}</Td>
<Td>{policy.secretPath || "*"}</Td>
<Td>
<Badge
@@ -1,5 +1,5 @@
/* eslint-disable no-case-declarations */
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability";
@@ -53,6 +53,7 @@ import { PendingAction } from "@app/hooks/api/secretFolders/types";
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
import { SecretV3RawSanitized } from "@app/hooks/api/types";
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
import { useResizableColWidth } from "@app/hooks/useResizableColWidth";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { RequestAccessModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/RequestAccessModal";
import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView";
@@ -104,6 +105,8 @@ const Page = () => {
const { permission } = useProjectPermission();
const { mutateAsync: createCommit } = useCreateCommit();
const tableRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
const { isBatchMode, pendingChanges } = useBatchMode();
const { loadPendingChanges, setExistingKeys } = useBatchModeActions();
@@ -212,11 +215,11 @@ const Page = () => {
searchFilter: (routerQueryParams.search as string) || "",
// these should always be on by default for the UI, they will be disabled for the query below based off permissions
include: {
[RowType.Folder]: true,
[RowType.Import]: true,
[RowType.DynamicSecret]: true,
[RowType.Secret]: true,
[RowType.SecretRotation]: true
[RowType.Folder]: false,
[RowType.Import]: false,
[RowType.DynamicSecret]: false,
[RowType.Secret]: false,
[RowType.SecretRotation]: false
}
};
@@ -242,6 +245,7 @@ const Page = () => {
}
}, [currentWorkspace, environment]);
const isResourceTypeFiltered = Object.values(filter.include).some(Boolean);
const {
data,
isPending: isDetailsLoading,
@@ -255,12 +259,14 @@ const Page = () => {
orderBy,
search: debouncedSearchFilter,
orderDirection,
includeImports: canReadSecretImports && filter.include.import,
includeFolders: filter.include.folder,
includeImports: canReadSecretImports && (isResourceTypeFiltered ? filter.include.import : true),
includeFolders: isResourceTypeFiltered ? filter.include.folder : true,
viewSecretValue: canReadSecretValue,
includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic,
includeSecrets: canReadSecret && filter.include.secret,
includeSecretRotations: canReadSecretRotations && filter.include.rotation,
includeDynamicSecrets:
canReadDynamicSecret && (isResourceTypeFiltered ? filter.include.dynamic : true),
includeSecrets: canReadSecret && (isResourceTypeFiltered ? filter.include.secret : true),
includeSecretRotations:
canReadSecretRotations && (isResourceTypeFiltered ? filter.include.rotation : true),
tags: filter.tags
});
@@ -480,6 +486,14 @@ const Page = () => {
handlePopUpClose("snapshots");
}, []);
const { handleMouseDown, isResizing, colWidth } = useResizableColWidth({
initialWidth: 320,
minWidth: 100,
maxWidth: tableRef.current
? tableRef.current.clientWidth - 148 // ensure value column can't collapse completely
: 800
});
useEffect(() => {
// restore filters for path if set
const restore = filterHistory.get(secretPath);
@@ -769,8 +783,22 @@ const Page = () => {
isPITEnabled={isPITEnabled}
hasPathPolicies={hasPathPolicies}
onRequestAccess={(params) => handlePopUpOpen("requestAccess", params)}
onClearFilters={() =>
setFilter((prev) => ({
...prev,
tags: {},
include: {
secret: false,
import: false,
dynamic: false,
rotation: false,
folder: false
}
}))
}
/>
<div
ref={tableRef}
className={twMerge(
"thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300",
isNotEmpty && "rounded-b-none"
@@ -804,20 +832,34 @@ const Page = () => {
/>
</div>
</Tooltip>
<div
className="flex w-80 flex-shrink-0 items-center border-r border-mineshaft-600 py-2 pl-4"
role="button"
tabIndex={0}
onClick={handleSortToggle}
onKeyDown={(evt) => {
if (evt.key === "Enter") handleSortToggle();
}}
>
Key
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
className="ml-2"
<div className="relative">
<div
tabIndex={-1}
role="button"
className={`absolute -right-[0.05rem] z-40 h-full w-0.5 cursor-ew-resize hover:bg-blue-400/20 ${
isResizing ? "bg-blue-400/75" : "bg-transparent"
}`}
onMouseDown={handleMouseDown}
/>
<div className="pointer-events-none absolute -right-[0.04rem] top-2 z-30">
<div className="h-5 w-0.5 rounded-[1.5px] bg-gray-400 opacity-50" />
</div>
<div
className="flex flex-shrink-0 items-center border-r border-mineshaft-600 py-2 pl-4"
style={{ width: colWidth }}
role="button"
tabIndex={0}
onClick={handleSortToggle}
onKeyDown={(evt) => {
if (evt.key === "Enter") handleSortToggle();
}}
>
Key
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
className="ml-2"
/>
</div>
</div>
<div className="flex-grow px-4 py-2">Value</div>
</div>
@@ -911,6 +953,7 @@ const Page = () => {
)}
{canReadSecret && Boolean(mergedSecrets?.length) && (
<SecretListView
colWidth={colWidth}
secrets={mergedSecrets}
tags={tags}
isVisible={isVisible}
@@ -932,12 +975,12 @@ const Page = () => {
/>
)}
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
{!canReadSecret &&
!canReadDynamicSecret &&
!canReadSecretImports &&
folders?.length === 0 && <PermissionDeniedBanner />}
</div>
</div>
{!canReadSecret &&
!canReadDynamicSecret &&
!canReadSecretImports &&
folders?.length === 0 && <PermissionDeniedBanner />}
{!isDetailsLoading &&
(totalCount > 0 ||
pendingChanges.secrets.length > 0 ||
@@ -136,6 +136,7 @@ type Props = {
isPITEnabled: boolean;
onRequestAccess: (actions: ProjectPermissionActions[]) => void;
hasPathPolicies: boolean;
onClearFilters: () => void;
};
export const ActionBar = ({
@@ -159,7 +160,8 @@ export const ActionBar = ({
isPITEnabled = false,
usedBySecretSyncs,
onRequestAccess,
hasPathPolicies
hasPathPolicies,
onClearFilters
}: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"addFolder",
@@ -661,6 +663,9 @@ export const ActionBar = ({
}
};
const isTableFiltered =
Object.values(filter.tags).some(Boolean) || Object.values(filter.include).some(Boolean);
return (
<>
<div className="mt-4 flex items-center space-x-2">
@@ -676,18 +681,22 @@ export const ActionBar = ({
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
<Button
size="sm"
variant="outline_bg"
ariaLabel="Download"
className={twMerge(
"transition-all",
(Object.keys(filter.tags).length ||
Object.values(filter.include).filter((include) => !include).length) &&
"border-primary/50 text-primary"
"flex h-[2.5rem]",
isTableFiltered && "border-primary/40 bg-primary/10"
)}
leftIcon={
<FontAwesomeIcon
icon={faFilter}
className={isTableFiltered ? "text-primary/80" : undefined}
/>
}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
Filters
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="p-0">
<DropdownMenuGroup>Filter By</DropdownMenuGroup>
@@ -762,51 +771,62 @@ export const ActionBar = ({
<span>Secrets</span>
</div>
</DropdownMenuItem>
<DropdownSubMenu>
<DropdownSubMenuTrigger
iconPos="right"
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
>
Tags
</DropdownSubMenuTrigger>
<DropdownSubMenuContent className="thin-scrollbar max-h-[20rem] overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Apply Tags to Filter Secrets
</DropdownMenuLabel>
{tags.map(({ id, slug, color }) => (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onToggleTagFilter(slug);
}}
key={id}
icon={filter?.tags[slug] && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{slug}
</div>
</DropdownMenuItem>
))}
</DropdownSubMenuContent>
</DropdownSubMenu>
{Boolean(tags.length) && (
<DropdownSubMenu>
<DropdownSubMenuTrigger
iconPos="right"
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
>
Tags
</DropdownSubMenuTrigger>
<DropdownSubMenuContent className="thin-scrollbar max-h-[20rem] overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Apply Tags to Filter Secrets
</DropdownMenuLabel>
{tags.map(({ id, slug, color }) => (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onToggleTagFilter(slug);
}}
key={id}
icon={filter?.tags[slug] && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{slug}
</div>
</DropdownMenuItem>
))}
</DropdownSubMenuContent>
</DropdownSubMenu>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
{isTableFiltered && (
<Button variant="plain" colorSchema="secondary" onClick={onClearFilters}>
Clear Filters
</Button>
)}
<div className="flex-grow" />
<div>
{isProtectedBranch && (
<Tooltip content={`Protected by policy ${protectedBranchPolicyName}`}>
<IconButton variant="outline_bg" ariaLabel="protected">
<IconButton
variant="outline_bg"
className="border-primary/40 bg-primary/10"
ariaLabel="protected"
>
<FontAwesomeIcon icon={faLock} className="text-primary" />
</IconButton>
</Tooltip>
)}
</div>
<div className="flex-grow" />
<div>
<IconButton variant="outline_bg" ariaLabel="Download" onClick={handleSecretDownload}>
<FontAwesomeIcon icon={faDownload} />
@@ -235,7 +235,7 @@ export const FolderListView = ({
)}
</div>
{isPending ? (
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
<div className="flex w-16 items-center justify-between border-l border-mineshaft-600 px-3 py-3">
<IconButton
ariaLabel="edit-folder"
variant="plain"
@@ -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;
}
@@ -90,6 +90,7 @@ type Props = {
}[];
isPending?: boolean;
pendingAction?: PendingAction;
colWidth: number;
};
export const SecretItem = memo(
@@ -108,7 +109,8 @@ export const SecretItem = memo(
handleSecretShare,
importedBy,
isPending,
pendingAction
pendingAction,
colWidth
}: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"editSecret"
@@ -383,7 +385,10 @@ export const SecretItem = memo(
</>
)}
</div>
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
<div
className="flex h-11 flex-shrink-0 items-center px-4 py-2"
style={{ width: colWidth }}
>
<Controller
name="key"
control={control}
@@ -51,6 +51,7 @@ type Props = {
isImported: boolean;
}[];
}[];
colWidth: number;
};
export const SecretListView = ({
@@ -62,7 +63,8 @@ export const SecretListView = ({
isVisible,
isProtectedBranch = false,
usedBySecretSyncs,
importedBy
importedBy,
colWidth
}: Props) => {
const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -554,6 +556,7 @@ export const SecretListView = ({
))}
{secrets.map((secret) => (
<SecretItem
colWidth={colWidth}
environment={environment}
secretPath={secretPath}
tags={wsTags}
@@ -1,6 +1,6 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync";
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
type Props = {
secretSync: TRenderSync;
@@ -0,0 +1,21 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { Badge } from "@app/components/v2";
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
type Props = {
secretSync: TRenderSync;
};
export const RenderSyncOptionsSection = ({ secretSync }: Props) => {
const {
syncOptions: { autoRedeployServices }
} = secretSync;
return (
<GenericFieldLabel label="Auto Redeploy Services">
<Badge variant={autoRedeployServices ? "success" : "danger"}>
{autoRedeployServices ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
);
};
@@ -13,6 +13,7 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection";
import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection";
import { RenderSyncOptionsSection } from "./RenderSyncOptionsSection";
type Props = {
secretSync: TSecretSync;
@@ -40,6 +41,9 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
<AwsSecretsManagerSyncOptionsSection secretSync={secretSync} />
);
break;
case SecretSync.Render:
AdditionalSyncOptionsComponent = <RenderSyncOptionsSection secretSync={secretSync} />;
break;
case SecretSync.GitHub:
case SecretSync.GCPSecretManager:
case SecretSync.AzureKeyVault:
@@ -56,7 +60,6 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.OCIVault:
case SecretSync.OnePass:
case SecretSync.Heroku:
case SecretSync.Render:
case SecretSync.Flyio:
case SecretSync.GitLab:
case SecretSync.CloudflarePages:
@@ -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>
);
};
@@ -1,6 +1,6 @@
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { ContentLoader, EmptyState } from "@app/components/v2";
import { AccessRestrictedBanner, ContentLoader, EmptyState } from "@app/components/v2";
import { useSubscription, useWorkspace } from "@app/context";
import { useGetSecretScanningConfig } from "@app/hooks/api/secretScanningV2";
@@ -16,16 +16,14 @@ export const ProjectScanningConfigTab = () => {
if (!subscription.secretScanning) {
return (
<div className="flex h-full w-full items-center justify-center px-20">
<EmptyState
className="rounded-md text-center"
icon={faBan}
title={
<span>
<div className="mt-60 flex h-full w-full items-center justify-center px-20">
<AccessRestrictedBanner
body={
<>
Your current plan doesn&apos;t support Secret Scanning.
<br /> Please contact Infisical Support or reach out through our Slack channel for
assistance.
</span>
</>
}
/>
</div>