Merge remote-tracking branch 'origin/main' into misc/privilege-management-v2-transition

This commit is contained in:
Sheen Capadngan
2025-03-19 19:51:41 +08:00
264 changed files with 6747 additions and 890 deletions
@@ -28,13 +28,13 @@ import {
useGetServerRootKmsEncryptionDetails,
useUpdateServerConfig
} from "@app/hooks/api";
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
import { AuthPanel } from "./components/AuthPanel";
import { EncryptionPanel } from "./components/EncryptionPanel";
import { IntegrationPanel } from "./components/IntegrationPanel";
import { RateLimitPanel } from "./components/RateLimitPanel";
import { UserPanel } from "./components/UserPanel";
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
enum TabSections {
Settings = "settings",
@@ -46,12 +46,22 @@ const schema = z.object({
caCert: z.string().trim().default(""),
boundIssuer: z.string().min(1),
boundAudiences: z.string().optional().default(""),
boundClaims: z.array(
z.object({
key: z.string(),
value: z.string()
})
),
boundClaims: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.default([]),
claimMetadataMapping: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.default([]),
boundSubject: z.string().optional().default("")
});
@@ -96,10 +106,11 @@ export const IdentityOidcAuthForm = ({
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
boundClaims: [],
claimMetadataMapping: []
}
});
const {
fields: boundClaimsFields,
append: appendBoundClaimField,
@@ -109,6 +120,15 @@ export const IdentityOidcAuthForm = ({
name: "boundClaims"
});
const {
fields: claimMetadataMappingFields,
append: appendClaimMetadataMappingField,
remove: removeClaimMetadataMappingField
} = useFieldArray({
control,
name: "claimMetadataMapping"
});
const {
fields: accessTokenTrustedIpsFields,
append: appendAccessTokenTrustedIp,
@@ -126,6 +146,12 @@ export const IdentityOidcAuthForm = ({
key,
value
})),
claimMetadataMapping: data?.claimMetadataMapping
? Object.entries(data.claimMetadataMapping).map(([key, value]) => ({
key,
value
}))
: undefined,
boundSubject: data.boundSubject,
accessTokenTTL: String(data.accessTokenTTL),
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
@@ -149,7 +175,8 @@ export const IdentityOidcAuthForm = ({
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
claimMetadataMapping: []
});
}
}, [data]);
@@ -164,6 +191,7 @@ export const IdentityOidcAuthForm = ({
boundIssuer,
boundAudiences,
boundClaims,
claimMetadataMapping,
boundSubject
}: FormData) => {
try {
@@ -180,6 +208,9 @@ export const IdentityOidcAuthForm = ({
boundIssuer,
boundAudiences,
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
claimMetadataMapping: claimMetadataMapping
? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value]))
: undefined,
boundSubject,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
@@ -194,6 +225,9 @@ export const IdentityOidcAuthForm = ({
boundIssuer,
boundAudiences,
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
claimMetadataMapping: claimMetadataMapping
? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value]))
: undefined,
boundSubject,
organizationId: orgId,
accessTokenTTL: Number(accessTokenTTL),
@@ -223,7 +257,9 @@ export const IdentityOidcAuthForm = ({
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["accessTokenTrustedIps", "caCert", "boundClaims"].includes(Object.keys(fields)[0])
["accessTokenTrustedIps", "caCert", "claimMetadataMapping"].includes(
Object.keys(fields)[0]
)
? IdentityFormTab.Advanced
: IdentityFormTab.Configuration
);
@@ -313,63 +349,6 @@ export const IdentityOidcAuthForm = ({
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
{boundClaimsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@@ -449,6 +428,155 @@ export const IdentityOidcAuthForm = ({
Add Claims
</Button>
</div>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
{claimMetadataMappingFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`claimMetadataMapping.${index}.key`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Token Claim Mapping" : undefined}
icon={
index === 0 ? (
<Tooltip
className="text-center"
content={
<div className="w-[180px]">
<p>Map OIDC token claims to metadata fields</p>
<p className="mt-2 text-sm">Example:</p>
<p className="mt-1 text-sm">
&apos;role&apos; → &apos;token.groups&apos;
</p>
<p className="mt-1 text-xs text-gray-400">
Becomes: identity.metadata.oidc.claims.role
</p>
</div>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
) : undefined
}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="Field name"
/>
</FormControl>
);
}}
/>
<Controller
control={control}
name={`claimMetadataMapping.${index}.value`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="Token claim"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => removeClaimMetadataMappingField(index)}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() =>
appendClaimMetadataMappingField({
key: "",
value: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add Token Mapping
</Button>
</div>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@@ -519,24 +647,21 @@ export const IdentityOidcAuthForm = ({
</div>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Add"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
<div className="mt-8 flex justify-between">
<div className="flex items-center">
<Button
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
variant="outline_bg"
className="mr-4"
isDisabled={isSubmitting}
>
Cancel
</Button>
<Button type="submit" isLoading={isSubmitting}>
{isUpdate ? "Update" : "Add"} Auth Method
</Button>
</div>
</div>
</form>
);
@@ -15,6 +15,7 @@ import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
@@ -62,6 +63,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <AzureAppConfigurationConnectionForm />;
case AppConnection.Databricks:
return <DatabricksConnectionForm onSubmit={onSubmit} />;
case AppConnection.Humanitec:
return <HumanitecConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -107,6 +110,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <AzureAppConfigurationConnectionForm appConnection={appConnection} />;
case AppConnection.Databricks:
return <DatabricksConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Humanitec:
return <HumanitecConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
@@ -0,0 +1,132 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { HumanitecConnectionMethod, THumanitecConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: THumanitecConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Humanitec)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(HumanitecConnectionMethod.API_TOKEN),
credentials: z.object({
apiToken: z.string().trim().min(1, "Service API Token required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const HumanitecConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Humanitec,
method: HumanitecConnectionMethod.API_TOKEN
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Humanitec].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(HumanitecConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Service API Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Humanitec"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -111,6 +111,26 @@ export const ViewIdentityOidcAuthContent = ({
</Tooltip>
)}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Claim Metadata Mapping">
{data.claimMetadataMapping && Object.keys(data.claimMetadataMapping).length && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={
<pre className="whitespace-pre-wrap rounded bg-mineshaft-600 p-2">
{JSON.stringify(data.claimMetadataMapping, null, 2)}
</pre>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};
@@ -7,7 +7,8 @@ import {
OrgPermissionAppConnectionActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionKmipActions
OrgPermissionKmipActions,
OrgPermissionSecretShareAction
} from "@app/context/OrgPermissionContext/types";
import { TPermission } from "@app/hooks/api/roles/types";
@@ -78,6 +79,12 @@ const adminConsolePermissionSchmea = z
})
.optional();
const secretSharingPermissionSchema = z
.object({
[OrgPermissionSecretShareAction.ManageSettings]: z.boolean().optional()
})
.optional();
export const formSchema = z.object({
name: z.string().trim(),
description: z.string().trim().optional(),
@@ -111,7 +118,8 @@ export const formSchema = z.object({
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
"app-connections": appConnectionsPermissionSchema,
kmip: kmipPermissionSchema,
gateway: orgGatewayPermissionSchema
gateway: orgGatewayPermissionSchema,
"secret-share": secretSharingPermissionSchema
})
.optional()
});
@@ -0,0 +1,130 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../OrgRoleModifySection.utils";
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
Custom = "custom"
}
const PERMISSION_ACTIONS = [{ action: "manage-settings", label: "Manage settings" }] as const;
export const OrgPermissionSecretShareRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.secret-share"
});
const selectedPermissionCategory = useMemo(() => {
if (rule?.["manage-settings"]) {
return Permission.Custom;
}
return Permission.NoAccess;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
useEffect(() => {
const isRowCustom = selectedPermissionCategory === Permission.Custom;
if (isRowCustom) {
setIsRowExpanded.on();
}
}, []);
const handlePermissionChange = (val: Permission) => {
if (!val) return;
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
if (val === Permission.NoAccess) {
setValue("permissions.secret-share", { "manage-settings": false }, { shouldDirty: true });
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Secret Share</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.secret-share.${action}`}
key={`permissions.secret-share.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.secret-share.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};
@@ -74,7 +74,7 @@ type Props = {
title: string;
formName: keyof Omit<
Exclude<TFormSchema["permissions"], undefined>,
"workspace" | "organization-admin-console" | "kmip" | "gateway"
"workspace" | "organization-admin-console" | "kmip" | "gateway" | "secret-share"
>;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
@@ -18,6 +18,7 @@ import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow";
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow";
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
import { RolePermissionRow } from "./RolePermissionRow";
@@ -181,6 +182,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionSecretShareRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgRoleWorkspaceRow
control={control}
setValue={setValue}
@@ -1,4 +1,5 @@
import { Modal, ModalContent } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { ShareSecretForm } from "@app/pages/public/ShareSecretPage/components";
@@ -11,6 +12,7 @@ type Props = {
};
export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
return (
<Modal
isOpen={popUp?.createSharedSecret?.isOpen}
@@ -25,6 +27,9 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
<ShareSecretForm
isPublic={false}
value={(popUp.createSharedSecret.data as { value?: string })?.value}
allowSecretSharingOutsideOrganization={
currentOrg?.allowSecretSharingOutsideOrganization ?? true
}
/>
</ModalContent>
</Modal>
@@ -11,7 +11,7 @@ const SecretSharingQueryParams = z.object({
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
)({
component: SecretSharingPage,
@@ -0,0 +1,35 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import {
OrgPermissionSecretShareAction,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { SecretSharingSettingsTabGroup } from "./components";
export const SecretSharingSettingsPage = withPermission(
() => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title={t("settings.org.title")} />
<SecretSharingSettingsTabGroup />
</div>
</div>
</>
);
},
{
action: OrgPermissionSecretShareAction.ManageSettings,
subject: OrgPermissionSubjects.SecretShare
}
);
@@ -0,0 +1,58 @@
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Switch } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api";
export const SecretSharingAllowShareToAnyone = () => {
const { currentOrg } = useOrganization();
const { mutateAsync } = useUpdateOrg();
const handleSecretSharingToggle = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
await mutateAsync({
orgId: currentOrg.id,
allowSecretSharingOutsideOrganization: value
});
createNotification({
text: `Successfully ${value ? "enabled" : "disabled"} secret sharing to members outside of this organization`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: (err as { response: { data: { message: string } } }).response.data.message,
type: "error"
});
}
};
return (
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div className="py-4">
<div className="mb-2 flex justify-between">
<h3 className="text-md text-mineshaft-100">
Allow sharing secrets to members outside of this organization
</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Switch
id="enable-secret-sharing-outside-org"
onCheckedChange={(value) => handleSecretSharingToggle(value)}
isChecked={currentOrg?.allowSecretSharingOutsideOrganization ?? false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
If enabled, team members will be able to share secrets to members outside of this
organization
</p>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { SecretSharingAllowShareToAnyone } from "./SecretSharingAllowShareToAnyone";
@@ -0,0 +1,9 @@
import { SecretSharingAllowShareToAnyone } from "../SecretSharingAllowShareToAnyone";
export const SecretSharingSettingsGeneralTab = () => {
return (
<div className="w-full">
<SecretSharingAllowShareToAnyone />
</div>
);
};
@@ -0,0 +1 @@
export { SecretSharingSettingsGeneralTab } from "./SecretSharingSettingsGeneralTab";
@@ -0,0 +1,39 @@
import { useState } from "react";
import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { SecretSharingSettingsGeneralTab } from "../SecretSharingSettingsGeneralTab";
export const SecretSharingSettingsTabGroup = () => {
const search = useSearch({
from: ROUTE_PATHS.Organization.SecretSharingSettings.id
});
const tabs = [
{
name: "General",
key: "tab-secret-sharing-general",
component: SecretSharingSettingsGeneralTab
}
];
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
return (
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
{tabs.map(({ key, component: Component }) => (
<TabPanel value={key} key={`tab-panel-${key}`}>
<Component />
</TabPanel>
))}
</Tabs>
);
};
@@ -0,0 +1 @@
export { SecretSharingSettingsTabGroup } from "./SecretSharingSettingsTabGroup";
@@ -0,0 +1 @@
export { SecretSharingSettingsTabGroup } from "./SecretSharingSettingsTabGroup";
@@ -0,0 +1,37 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { SecretSharingSettingsPage } from "./SecretSharingSettingsPage";
const SettingsPageQueryParams = z.object({
selectedTab: z.string().catch("")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings"
)({
component: SecretSharingSettingsPage,
validateSearch: zodValidator(SettingsPageQueryParams),
search: {
middlewares: [stripSearchParams({ selectedTab: "" })]
},
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Secret Sharing",
link: linkOptions({ to: "/organization/secret-sharing" })
},
{
label: "Settings"
}
]
})
});
@@ -41,9 +41,14 @@ export type FormData = z.infer<typeof schema>;
type Props = {
isPublic: boolean; // whether or not this is a public (non-authenticated) secret sharing form
value?: string;
allowSecretSharingOutsideOrganization?: boolean;
};
export const ShareSecretForm = ({ isPublic, value }: Props) => {
export const ShareSecretForm = ({
isPublic,
value,
allowSecretSharingOutsideOrganization = true
}: Props) => {
const [secretLink, setSecretLink] = useState("");
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
initialState: "Copy to clipboard"
@@ -230,7 +235,9 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
onValueChange={(e) => onChange(e)}
className="w-full"
>
<SelectItem value={SecretSharingAccessType.Anyone}>Anyone</SelectItem>
{allowSecretSharingOutsideOrganization && (
<SelectItem value={SecretSharingAccessType.Anyone}>Anyone</SelectItem>
)}
<SelectItem value={SecretSharingAccessType.Organization}>
People within your organization
</SelectItem>
@@ -0,0 +1,14 @@
import { THumanitecSync } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: THumanitecSync;
};
export const HumanitecSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -7,6 +7,7 @@ import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncC
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
type Props = {
secretSync: TSecretSync;
@@ -28,6 +29,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <AzureAppConfigurationDestinationSyncCol secretSync={secretSync} />;
case SecretSync.Databricks:
return <DatabricksSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Humanitec:
return <HumanitecSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
@@ -3,6 +3,7 @@ import {
GitHubSyncScope,
GitHubSyncVisibility
} from "@app/hooks/api/secretSyncs/types/github-sync";
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
// This functional ensures parity across what is displayed in the destination column
// and the values used when search filtering
@@ -59,6 +60,19 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
case SecretSync.Databricks:
primaryText = destinationConfig.scope;
break;
case SecretSync.Humanitec:
switch (destinationConfig.scope) {
case HumanitecSyncScope.Application:
primaryText = destinationConfig.app;
break;
case HumanitecSyncScope.Environment:
primaryText = `${destinationConfig.app} / ${destinationConfig.env}`;
break;
default:
throw new Error(`Unhandled Humanitec Scope Destination Col Values ${destination}`);
}
secondaryText = `Organization - ${destinationConfig.org}`;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}
@@ -19,6 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
@@ -49,8 +50,10 @@ import {
import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useWorkspace
} from "@app/context";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
@@ -71,6 +74,7 @@ import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
@@ -131,6 +135,7 @@ export const OverviewPage = () => {
const [searchFilter, setSearchFilter] = useState("");
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter);
const secretPath = (routerSearch?.secretPath as string) || "/";
const { subscription } = useSubscription();
const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE);
const [filterHistory, setFilterHistory] = useState<
@@ -178,6 +183,15 @@ export const OverviewPage = () => {
}, []);
const userAvailableEnvs = currentWorkspace?.environments || [];
const userAvailableDynamicSecretEnvs = userAvailableEnvs.filter((env) =>
permission.can(
ProjectPermissionDynamicSecretActions.CreateRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: env.slug,
secretPath
})
)
);
const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs);
@@ -249,7 +263,9 @@ export const OverviewPage = () => {
"addSecretsInAllEnvs",
"addFolder",
"misc",
"updateFolder"
"updateFolder",
"addDynamicSecret",
"upgradePlan"
] as const);
const handleFolderCreate = async (folderName: string, description: string | null) => {
@@ -851,20 +867,43 @@ export const OverviewPage = () => {
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
leftIcon={<FontAwesomeIcon icon={faFolderPlus} className="pr-2" />}
onClick={() => {
handlePopUpOpen("addFolder");
handlePopUpClose("misc");
}}
isDisabled={!isAllowed}
variant="outline_bg"
className="h-10"
className="h-10 text-left"
isFullWidth
>
Add Folder
</Button>
)}
</ProjectPermissionCan>
<Tooltip
content={
userAvailableDynamicSecretEnvs.length === 0 ? "Access restricted" : ""
}
>
<Button
leftIcon={<FontAwesomeIcon icon={faFingerprint} className="pr-2" />}
onClick={() => {
if (subscription?.dynamicSecret) {
handlePopUpOpen("addDynamicSecret");
handlePopUpClose("misc");
return;
}
handlePopUpOpen("upgradePlan");
}}
isDisabled={userAvailableDynamicSecretEnvs.length === 0}
variant="outline_bg"
className="h-10 text-left"
isFullWidth
>
Add Dynamic Secret
</Button>
</Tooltip>
</div>
</DropdownMenuContent>
</DropdownMenu>
@@ -1170,6 +1209,24 @@ export const OverviewPage = () => {
/>
</ModalContent>
</Modal>
<CreateDynamicSecretForm
isOpen={popUp.addDynamicSecret.isOpen}
onToggle={(isOpen) => handlePopUpToggle("addDynamicSecret", isOpen)}
projectSlug={projectSlug}
environments={userAvailableDynamicSecretEnvs}
secretPath={secretPath}
/>
{subscription && (
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={
subscription.slug === null
? "You can perform this action under an Enterprise license"
: "You can perform this action if you switch to Infisical's Team plan"
}
/>
)}
</div>
);
};
@@ -655,8 +655,9 @@ export const ActionBar = ({
isOpen={popUp.addDynamicSecret.isOpen}
onToggle={(isOpen) => handlePopUpToggle("addDynamicSecret", isOpen)}
projectSlug={projectSlug}
environment={environment}
environments={[{ slug: environment, name: environment, id: "not-used" }]}
secretPath={secretPath}
isSingleEnvironmentMode
/>
<Modal
isOpen={popUp.addFolder.isOpen}
@@ -11,6 +11,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -18,6 +19,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -50,7 +52,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -59,15 +62,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const AwsElastiCacheInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -87,13 +92,20 @@ export const AwsElastiCacheInputForm = ({
revocationStatement: `{
"UserId": "{{username}}"
}`
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -104,7 +116,7 @@ export const AwsElastiCacheInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -299,6 +311,28 @@ export const AwsElastiCacheInputForm = ({
</AccordionContent>
</AccordionItem>
</Accordion>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -5,9 +5,10 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
import { Button, FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -40,7 +41,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -49,29 +51,41 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const AwsIamInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
formState: { isSubmitting },
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema)
resolver: zodResolver(formSchema),
defaultValues: {
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.AwsIam, inputs: provider },
@@ -80,7 +94,7 @@ export const AwsIamInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -286,6 +300,29 @@ export const AwsIamInputForm = ({
</FormControl>
)}
/>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -12,7 +12,7 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
import {
DropdownMenu,
DropdownMenuContent,
@@ -23,6 +23,7 @@ import { Tooltip } from "@app/components/v2/Tooltip";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
selectedUsers: z.array(
@@ -60,7 +61,8 @@ const formSchema = z.object({
name: z
.string()
.min(1)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -69,15 +71,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const AzureEntraIdInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -85,7 +89,10 @@ export const AzureEntraIdInputForm = ({
watch,
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema)
resolver: zodResolver(formSchema),
defaultValues: {
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const tenantId = watch("provider.tenantId");
const applicationId = watch("provider.applicationId");
@@ -107,7 +114,8 @@ export const AzureEntraIdInputForm = ({
selectedUsers,
provider,
maxTTL,
defaultTTL
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
@@ -129,7 +137,7 @@ export const AzureEntraIdInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
});
onCompleted();
@@ -373,6 +381,29 @@ export const AzureEntraIdInputForm = ({
</div>
</div>
</div>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isLoading || isError}>
@@ -11,6 +11,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -18,6 +19,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -52,7 +54,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -61,7 +64,8 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
const getSqlStatements = () => {
@@ -76,9 +80,10 @@ const getSqlStatements = () => {
export const CassandraInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -87,15 +92,23 @@ export const CassandraInputForm = ({
} = useForm<TForm>({
resolver: zodResolver(formSchema),
defaultValues: {
provider: getSqlStatements()
provider: getSqlStatements(),
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.Cassandra, inputs: provider },
@@ -104,7 +117,7 @@ export const CassandraInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -345,6 +358,29 @@ export const CassandraInputForm = ({
</AccordionContent>
</AccordionItem>
</Accordion>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -17,6 +17,7 @@ import { AnimatePresence, motion } from "framer-motion";
import { Modal, ModalContent } from "@app/components/v2";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
import { AwsIamInputForm } from "./AwsIamInputForm";
@@ -38,8 +39,9 @@ type Props = {
isOpen?: boolean;
onToggle: (isOpen: boolean) => void;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
secretPath: string;
isSingleEnvironmentMode?: boolean;
};
enum WizardSteps {
@@ -129,8 +131,9 @@ export const CreateDynamicSecretForm = ({
isOpen,
onToggle,
projectSlug,
environment,
secretPath
environments,
secretPath,
isSingleEnvironmentMode
}: Props) => {
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider);
const [selectedProvider, setSelectedProvider] = useState<DynamicSecretProviders | null>(null);
@@ -197,7 +200,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -215,7 +219,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -233,7 +238,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -251,7 +257,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -269,7 +276,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -287,7 +295,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -305,7 +314,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -323,7 +333,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -341,7 +352,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -359,7 +371,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -377,7 +390,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -395,7 +409,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -413,7 +428,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -432,7 +448,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -450,7 +467,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
environments={environments}
isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
</motion.div>
)}
@@ -9,6 +9,7 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import {
Button,
FilterableSelect,
FormControl,
FormLabel,
IconButton,
@@ -19,6 +20,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const authMethods = [
{
@@ -73,7 +75,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -82,15 +85,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const ElasticSearchInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -107,13 +112,20 @@ export const ElasticSearchInputForm = ({
},
roles: ["superuser"],
port: 443
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -124,7 +136,7 @@ export const ElasticSearchInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -406,6 +418,29 @@ export const ElasticSearchInputForm = ({
)}
/>
</div>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -7,9 +7,18 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
import {
Button,
FilterableSelect,
FormControl,
Input,
Select,
SelectItem,
TextArea
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
enum CredentialType {
Dynamic = "dynamic",
@@ -69,7 +78,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -79,7 +89,8 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const LdapInputForm = ({
@@ -87,7 +98,8 @@ export const LdapInputForm = ({
onCancel,
secretPath,
projectSlug,
environment
environments,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -107,7 +119,8 @@ export const LdapInputForm = ({
revocationLdif: "",
rollbackLdif: "",
credentialType: CredentialType.Dynamic
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
@@ -115,7 +128,13 @@ export const LdapInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -126,7 +145,7 @@ export const LdapInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -369,6 +388,29 @@ export const LdapInputForm = ({
)}
/>
)}
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -13,6 +13,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
FilterableSelect,
FormControl,
FormLabel,
IconButton,
@@ -22,6 +23,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -63,7 +65,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -72,7 +75,8 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
const ATLAS_SCOPE_TYPES = [
@@ -93,9 +97,10 @@ const ATLAS_SCOPE_TYPES = [
export const MongoAtlasInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -108,7 +113,8 @@ export const MongoAtlasInputForm = ({
defaultValues: {
provider: {
roles: [{ databaseName: "", roleName: "" }]
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
@@ -124,7 +130,13 @@ export const MongoAtlasInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -135,7 +147,7 @@ export const MongoAtlasInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -438,6 +450,29 @@ export const MongoAtlasInputForm = ({
</AccordionContent>
</AccordionItem>
</Accordion>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -7,9 +7,18 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2";
import {
Button,
FilterableSelect,
FormControl,
FormLabel,
IconButton,
Input,
SecretInput
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -46,7 +55,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -55,15 +65,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const MongoDBDatabaseInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -76,7 +88,8 @@ export const MongoDBDatabaseInputForm = ({
defaultValues: {
provider: {
roles: [{ roleName: "readWrite" }]
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
@@ -87,7 +100,13 @@ export const MongoDBDatabaseInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -105,7 +124,7 @@ export const MongoDBDatabaseInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -321,6 +340,29 @@ export const MongoDBDatabaseInputForm = ({
)}
/>
</div>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -7,9 +7,18 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2";
import {
Button,
FilterableSelect,
FormControl,
FormLabel,
IconButton,
Input,
SecretInput
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -50,7 +59,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -59,15 +69,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const RabbitMqInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -89,13 +101,20 @@ export const RabbitMqInputForm = ({
}
},
tags: []
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -106,7 +125,7 @@ export const RabbitMqInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -405,6 +424,29 @@ export const RabbitMqInputForm = ({
)}
/>
</div>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -11,6 +11,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -18,6 +19,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -50,7 +52,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -59,15 +62,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const RedisInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -81,13 +86,20 @@ export const RedisInputForm = ({
port: 6379,
creationStatement: "ACL SETUSER {{username}} on >{{password}} ~* &* +@all",
revocationStatement: "ACL DELUSER {{username}}"
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -98,7 +110,7 @@ export const RedisInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -313,6 +325,29 @@ export const RedisInputForm = ({
</AccordionContent>
</AccordionItem>
</Accordion>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -11,12 +11,14 @@ import {
AccordionItem,
AccordionTrigger,
Button,
FilterableSelect,
FormControl,
Input,
TextArea
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -48,7 +50,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -57,15 +60,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const SapAseInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -82,13 +87,20 @@ sp_adduser '{{username}}', '{{username}}', null;
sp_role 'grant', 'mon_role', '{{username}}';`,
revocationStatement: `sp_dropuser '{{username}}';
sp_droplogin '{{username}}';`
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -99,7 +111,7 @@ sp_droplogin '{{username}}';`
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -291,6 +303,29 @@ sp_droplogin '{{username}}';`
</AccordionContent>
</AccordionItem>
</Accordion>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -11,6 +11,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -18,6 +19,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -50,7 +52,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -59,15 +62,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const SapHanaInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -82,13 +87,20 @@ GRANT "MONITORING" TO {{username}};`,
revocationStatement: `REVOKE "MONITORING" FROM {{username}};
DROP USER {{username}};`,
renewStatement: "ALTER USER {{username}} VALID UNTIL '{{expiration}}';"
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -99,7 +111,7 @@ DROP USER {{username}};`,
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -311,6 +323,29 @@ DROP USER {{username}};`,
</AccordionContent>
</AccordionItem>
</Accordion>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -13,12 +13,14 @@ import {
AccordionItem,
AccordionTrigger,
Button,
FilterableSelect,
FormControl,
Input,
TextArea
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -54,7 +56,8 @@ const formSchema = z.object({
.string()
.trim()
.min(1)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -63,15 +66,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const SnowflakeInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -85,13 +90,20 @@ export const SnowflakeInputForm = ({
"CREATE USER {{username}} PASSWORD = '{{password}}' DEFAULT_ROLE = public DEFAULT_SECONDARY_ROLES = ('ALL') MUST_CHANGE_PASSWORD = FALSE DAYS_TO_EXPIRY = {{expiration}};",
revocationStatement: "DROP USER {{username}};",
renewStatement: "ALTER USER {{username}} SET DAYS_TO_EXPIRY = {{expiration}};"
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -102,7 +114,7 @@ export const SnowflakeInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch (err) {
@@ -314,6 +326,29 @@ export const SnowflakeInputForm = ({
</AccordionContent>
</AccordionItem>
</Accordion>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -12,6 +12,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -22,6 +23,7 @@ import {
import { useWorkspace } from "@app/context";
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
const passwordRequirementsSchema = z
.object({
@@ -79,7 +81,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -88,7 +91,8 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
const getSqlStatements = (provider: SqlProviders) => {
@@ -145,9 +149,10 @@ const getDefaultPort = (provider: SqlProviders) => {
export const SqlDatabaseInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const { currentWorkspace } = useWorkspace();
@@ -172,7 +177,8 @@ export const SqlDatabaseInputForm = ({
},
allowedSymbols: "-_.~!*"
}
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
@@ -181,9 +187,16 @@ export const SqlDatabaseInputForm = ({
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({
name,
maxTTL,
provider,
defaultTTL,
environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider },
@@ -192,7 +205,7 @@ export const SqlDatabaseInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -649,6 +662,29 @@ export const SqlDatabaseInputForm = ({
</AccordionContent>
</AccordionItem>
</Accordion>
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -5,9 +5,17 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
import {
Button,
FilterableSelect,
FormControl,
Input,
Select,
SelectItem
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
enum ConfigType {
URL = "url",
@@ -52,7 +60,8 @@ const formSchema = z.object({
.string()
.trim()
.min(1)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((val) => val.toLowerCase() === val, "Must be lowercase"),
environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer<typeof formSchema>;
@@ -62,15 +71,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
environments: WorkspaceEnv[];
isSingleEnvironmentMode?: boolean;
};
export const TotpInputForm = ({
onCompleted,
onCancel,
environment,
environments,
secretPath,
projectSlug
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -82,7 +93,8 @@ export const TotpInputForm = ({
defaultValues: {
provider: {
configType: ConfigType.URL
}
},
environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
@@ -90,7 +102,7 @@ export const TotpInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, provider }: TForm) => {
const handleCreateDynamicSecret = async ({ name, provider, environment }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -101,7 +113,7 @@ export const TotpInputForm = ({
path: secretPath,
defaultTTL: "1m",
projectSlug,
environmentSlug: environment
environmentSlug: environment.slug
});
onCompleted();
} catch (err) {
@@ -295,6 +307,29 @@ export const TotpInputForm = ({
</p>
</>
)}
{!isSingleEnvironmentMode && (
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
options={environments}
value={value}
onChange={onChange}
placeholder="Select the environment to create secret in..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
menuPlacement="top"
/>
</FormControl>
)}
/>
)}
</div>
</div>
</div>
@@ -306,7 +306,10 @@ export const SecretItem = memo(
)}
/>
)}
<div key="actions" className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2">
<div
key="actions"
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
>
<Tooltip content="Copy secret">
<IconButton
isDisabled={secret.secretValueHidden}
@@ -0,0 +1,49 @@
import { ReactNode } from "react";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import {
HumanitecSyncScope,
THumanitecSync
} from "@app/hooks/api/secretSyncs/types/humanitec-sync";
type Props = {
secretSync: THumanitecSync;
};
export const HumanitecSyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
let Components: ReactNode;
switch (destinationConfig.scope) {
case HumanitecSyncScope.Application:
Components = (
<>
<SecretSyncLabel label="Application">{destinationConfig.app}</SecretSyncLabel>
<SecretSyncLabel label="Organization">{destinationConfig.org}</SecretSyncLabel>
</>
);
break;
case HumanitecSyncScope.Environment:
Components = (
<>
<SecretSyncLabel label="Application">{destinationConfig.app}</SecretSyncLabel>
<SecretSyncLabel label="Organization">{destinationConfig.org}</SecretSyncLabel>
<SecretSyncLabel label="Environment">{destinationConfig.env}</SecretSyncLabel>
</>
);
break;
default:
throw new Error(
`Uhandled Humanitec Sync Destination Section Scope ${secretSync.destinationConfig.scope}`
);
}
return (
<>
<SecretSyncLabel className="capitalize" label="Scope">
{destinationConfig.scope.replace("-", " ")}
</SecretSyncLabel>
{Components}
</>
);
};
@@ -17,6 +17,7 @@ import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSy
import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection";
import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection";
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
type Props = {
secretSync: TSecretSync;
@@ -53,6 +54,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Databricks:
DestinationComponents = <DatabricksSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Humanitec:
DestinationComponents = <HumanitecSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { IconButton } from "@app/components/v2";
import { Badge, IconButton } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs";
@@ -24,7 +24,8 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
syncOptions: {
// appendSuffix,
// prependPrefix,
initialSyncBehavior
initialSyncBehavior,
disableSecretDeletion
}
} = secretSync;
@@ -46,6 +47,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.AzureKeyVault:
case SecretSync.AzureAppConfiguration:
case SecretSync.Databricks:
case SecretSync.Humanitec:
AdditionalSyncOptionsComponent = null;
break;
default:
@@ -57,24 +59,22 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
{AdditionalSyncOptionsComponent && (
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync options"
onClick={onEditOptions}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit sync options"
onClick={onEditOptions}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div>
<div className="space-y-3">
@@ -84,6 +84,11 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
{AdditionalSyncOptionsComponent}
{disableSecretDeletion && (
<SecretSyncLabel label="Secret Deletion">
<Badge variant="primary">Disabled</Badge>
</SecretSyncLabel>
)}
</div>
</div>
</div>