mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 15:40:47 +00:00
merge fixes
This commit is contained in:
@@ -72,7 +72,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
OrgPermissionSubjects.ProjectTemplates
|
||||
);
|
||||
|
||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
||||
const { data: projectTemplates = [] } = useListProjectTemplates(projectType, {
|
||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
|
||||
import { ProjectTemplatesTab } from "./components";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab }
|
||||
];
|
||||
|
||||
export const ProjectSettings = () => {
|
||||
const [selectedTab, setSelectedTab] = useState(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>
|
||||
);
|
||||
};
|
||||
+8
-5
@@ -7,6 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal";
|
||||
import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm";
|
||||
@@ -24,7 +25,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
|
||||
"editDetails"
|
||||
] as const);
|
||||
|
||||
const { id: templateId, name, description } = projectTemplate;
|
||||
const { id: templateId, name, description, type } = projectTemplate;
|
||||
|
||||
const deleteProjectTemplate = useDeleteProjectTemplate();
|
||||
|
||||
@@ -94,10 +95,12 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ProjectTemplateEnvironmentsForm
|
||||
isInfisicalTemplate={isInfisicalTemplate}
|
||||
projectTemplate={projectTemplate}
|
||||
/>
|
||||
{type === ProjectType.SecretManager && (
|
||||
<ProjectTemplateEnvironmentsForm
|
||||
isInfisicalTemplate={isInfisicalTemplate}
|
||||
projectTemplate={projectTemplate}
|
||||
/>
|
||||
)}
|
||||
<ProjectTemplateRolesSection
|
||||
isInfisicalTemplate={isInfisicalTemplate}
|
||||
projectTemplate={projectTemplate}
|
||||
+19
-24
@@ -6,15 +6,15 @@ import { twMerge } from "tailwind-merge";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent, ModalTrigger } from "@app/components/v2";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { isCustomProjectRole } from "@app/helpers/roles";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
|
||||
import { NewPermissionRule } from "@app/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule";
|
||||
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
@@ -44,7 +44,7 @@ export const ProjectTemplateEditRoleForm = ({
|
||||
role,
|
||||
isDisabled
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
|
||||
|
||||
const formMethods = useForm<TFormSchema>({
|
||||
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
||||
@@ -119,34 +119,29 @@ export const ProjectTemplateEditRoleForm = ({
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
|
||||
className={twMerge(
|
||||
"h-10 rounded-r-none border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={popUp.createPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
|
||||
<Button
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => handlePopUpToggle("addPolicy")}
|
||||
>
|
||||
<ModalTrigger asChild>
|
||||
<Button
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
New Policy
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
title="New Policy"
|
||||
subTitle="Policies grant additional permissions."
|
||||
>
|
||||
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
Add Policies
|
||||
</Button>
|
||||
<PolicySelectionModal
|
||||
isOpen={popUp.addPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
+11
-3
@@ -19,7 +19,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
@@ -35,6 +35,7 @@ const formSchema = z.object({
|
||||
slug: slugSchema({ min: 1, max: 32 })
|
||||
})
|
||||
.array()
|
||||
.nullish()
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof formSchema>;
|
||||
@@ -55,6 +56,8 @@ export const ProjectTemplateEnvironmentsForm = ({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const {
|
||||
fields: environments,
|
||||
move,
|
||||
@@ -67,7 +70,7 @@ export const ProjectTemplateEnvironmentsForm = ({
|
||||
const onFormSubmit = async (form: TFormSchema) => {
|
||||
try {
|
||||
const { environments: updatedEnvs } = await updateProjectTemplate.mutateAsync({
|
||||
environments: form.environments.map((env, index) => ({
|
||||
environments: form.environments?.map((env, index) => ({
|
||||
...env,
|
||||
position: index + 1
|
||||
})),
|
||||
@@ -89,6 +92,9 @@ export const ProjectTemplateEnvironmentsForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const isEnvironmentLimitExceeded =
|
||||
Boolean(subscription.environmentLimit) && environments.length >= subscription.environmentLimit;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
@@ -138,6 +144,8 @@ export const ProjectTemplateEnvironmentsForm = ({
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
renderTooltip={isEnvironmentLimitExceeded ? true : undefined}
|
||||
allowedLabel={`Plan environment limit of ${subscription.environmentLimit} exceeded. Contact Infisical to increase limit.`}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
@@ -147,7 +155,7 @@ export const ProjectTemplateEnvironmentsForm = ({
|
||||
variant="solid"
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
isDisabled={!isAllowed || isEnvironmentLimitExceeded}
|
||||
>
|
||||
Add Environment
|
||||
</Button>
|
||||
+11
-1
@@ -12,6 +12,7 @@ import {
|
||||
ModalContent,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useGetProjectTypeFromRoute } from "@app/hooks";
|
||||
import {
|
||||
TProjectTemplate,
|
||||
useCreateProjectTemplate,
|
||||
@@ -41,6 +42,7 @@ type FormProps = {
|
||||
const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
|
||||
const createProjectTemplate = useCreateProjectTemplate();
|
||||
const updateProjectTemplate = useUpdateProjectTemplate();
|
||||
const projectType = useGetProjectTypeFromRoute();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -55,9 +57,17 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
|
||||
});
|
||||
|
||||
const onFormSubmit = async (data: FormData) => {
|
||||
if (!projectType) {
|
||||
createNotification({
|
||||
text: "Failed to determine project type",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const mutation = projectTemplate
|
||||
? updateProjectTemplate.mutateAsync({ templateId: projectTemplate.id, ...data })
|
||||
: createProjectTemplate.mutateAsync(data);
|
||||
: createProjectTemplate.mutateAsync({ ...data, type: projectType });
|
||||
try {
|
||||
const template = await mutation;
|
||||
createNotification({
|
||||
+1
-1
@@ -50,7 +50,7 @@ export const ProjectTemplatesSection = () => {
|
||||
className="absolute min-h-[10rem] w-full"
|
||||
>
|
||||
<div>
|
||||
<p className="mb-6 text-bunker-300">
|
||||
<p className="mb-6 font-inter text-bunker-300">
|
||||
Create and configure templates with predefined roles and environments to streamline
|
||||
project setup
|
||||
</p>
|
||||
+42
-31
@@ -23,8 +23,9 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetProjectTypeFromRoute, usePopUp } from "@app/hooks";
|
||||
import { TProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { DeleteProjectTemplateModal } from "./DeleteProjectTemplateModal";
|
||||
|
||||
@@ -35,9 +36,12 @@ type Props = {
|
||||
export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { isPending, data: projectTemplates = [] } = useListProjectTemplates({
|
||||
enabled: subscription?.projectTemplates
|
||||
const projectType = useGetProjectTypeFromRoute();
|
||||
|
||||
const { isPending, data: projectTemplates = [] } = useListProjectTemplates(projectType, {
|
||||
enabled: subscription?.projectTemplates && Boolean(projectType)
|
||||
});
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteTemplate"] as const);
|
||||
@@ -50,6 +54,10 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
||||
[search, projectTemplates]
|
||||
);
|
||||
|
||||
const isSecretManagerTemplates = projectType === ProjectType.SecretManager;
|
||||
|
||||
const colSpan = isSecretManagerTemplates ? 4 : 3;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
@@ -64,20 +72,20 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Roles</Th>
|
||||
<Th>Environments</Th>
|
||||
{isSecretManagerTemplates && <Th>Environments</Th>}
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && (
|
||||
{subscription?.projectTemplates && isPending && (
|
||||
<TableSkeleton
|
||||
innerKey="project-templates-table"
|
||||
columns={4}
|
||||
columns={colSpan}
|
||||
key="project-templates"
|
||||
/>
|
||||
)}
|
||||
{filteredTemplates.map((template) => {
|
||||
const { id, name, roles, environments, description } = template;
|
||||
const { id, name, roles, environments = [], description } = template;
|
||||
return (
|
||||
<Tr
|
||||
onClick={() => onEdit(template)}
|
||||
@@ -116,28 +124,30 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
||||
</Tooltip>
|
||||
)}
|
||||
</Td>
|
||||
<Td className="pl-14">
|
||||
{environments.length}
|
||||
{environments.length > 0 && (
|
||||
<Tooltip
|
||||
content={
|
||||
<ul className="ml-2 list-disc">
|
||||
{environments
|
||||
.sort((a, b) => (a.position > b.position ? 1 : -1))
|
||||
.map((env) => (
|
||||
<li key={env.slug}>{env.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
size="sm"
|
||||
className="ml-2 text-mineshaft-400"
|
||||
icon={faCircleInfo}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Td>
|
||||
{isSecretManagerTemplates && environments && (
|
||||
<Td className="pl-14">
|
||||
{environments.length}
|
||||
{environments.length > 0 && (
|
||||
<Tooltip
|
||||
content={
|
||||
<ul className="ml-2 list-disc">
|
||||
{environments
|
||||
.sort((a, b) => (a.position > b.position ? 1 : -1))
|
||||
.map((env) => (
|
||||
<li key={env.slug}>{env.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
size="sm"
|
||||
className="ml-2 text-mineshaft-400"
|
||||
icon={faCircleInfo}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Td>
|
||||
)}
|
||||
<Td className="w-5">
|
||||
{name !== "default" && (
|
||||
<OrgPermissionCan
|
||||
@@ -164,9 +174,10 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{!isPending && filteredTemplates?.length === 0 && (
|
||||
{(!subscription?.projectTemplates ||
|
||||
(!isPending && filteredTemplates?.length === 0)) && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<Td colSpan={colSpan}>
|
||||
<EmptyState
|
||||
title={
|
||||
search.trim()
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./ProjectTemplatesTab";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./ProjectSettings";
|
||||
@@ -7,9 +7,10 @@ type Props = {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
};
|
||||
|
||||
export const NoticeBannerV2 = ({ title, children, className }: Props) => {
|
||||
export const NoticeBannerV2 = ({ title, children, className, titleClassName }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
@@ -17,7 +18,7 @@ export const NoticeBannerV2 = ({ title, children, className }: Props) => {
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="mb-1 flex items-center text-sm">
|
||||
<div className={twMerge("mb-1 flex items-center text-sm", titleClassName)}>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
|
||||
{title}
|
||||
</div>
|
||||
|
||||
@@ -6,21 +6,9 @@ export enum OrgMembershipRole {
|
||||
NoAccess = "no-access"
|
||||
}
|
||||
|
||||
enum ProjectMemberRole {
|
||||
Admin = "admin",
|
||||
Member = "member",
|
||||
Viewer = "viewer",
|
||||
NoAccess = "no-access"
|
||||
}
|
||||
|
||||
export const isCustomOrgRole = (slug: string) =>
|
||||
!Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole);
|
||||
|
||||
export const formatProjectRoleName = (name: string) => {
|
||||
if (name === ProjectMemberRole.Member) return "developer";
|
||||
return name;
|
||||
};
|
||||
|
||||
export const isCustomProjectRole = (slug: string) =>
|
||||
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
||||
|
||||
@@ -28,3 +16,24 @@ export const findOrgMembershipRole = (roles: TOrgRole[], roleIdOrSlug: string) =
|
||||
isCustomOrgRole(roleIdOrSlug)
|
||||
? roles.find((r) => r.id === roleIdOrSlug)
|
||||
: roles.find((r) => r.slug === roleIdOrSlug);
|
||||
|
||||
export const formatProjectRoleName = (role: string, customRoleName?: string) => {
|
||||
switch (role) {
|
||||
case ProjectMembershipRole.Admin:
|
||||
return "Admin";
|
||||
case ProjectMembershipRole.Member:
|
||||
return "Developer";
|
||||
case ProjectMembershipRole.Viewer:
|
||||
return "Viewer";
|
||||
case ProjectMembershipRole.NoAccess:
|
||||
return "No Access";
|
||||
case ProjectMembershipRole.Custom:
|
||||
return customRoleName ?? role;
|
||||
case ProjectMembershipRole.SshHostBootstrapper:
|
||||
return "SSH Host Bootstrapper";
|
||||
case ProjectMembershipRole.KmsCryptographicOperator:
|
||||
return "Cryptographic Operator";
|
||||
default:
|
||||
return role;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ export {
|
||||
useAdminGrantServerAdminAccess,
|
||||
useAdminRemoveIdentitySuperAdminAccess,
|
||||
useCreateAdminUser,
|
||||
useInvalidateCache,
|
||||
useRemoveUserServerAdminAccess,
|
||||
useUpdateServerConfig,
|
||||
useUpdateServerEncryptionStrategy
|
||||
|
||||
@@ -8,6 +8,7 @@ import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
||||
import {
|
||||
RootKeyEncryptionStrategy,
|
||||
TCreateAdminUserDTO,
|
||||
TInvalidateCacheDTO,
|
||||
TServerConfig,
|
||||
TUpdateServerConfigDTO
|
||||
} from "./types";
|
||||
@@ -126,3 +127,15 @@ export const useUpdateServerEncryptionStrategy = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useInvalidateCache = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, object, TInvalidateCacheDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
await apiRequest.post("/api/v1/admin/invalidate-cache", dto);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInvalidateCache() });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AdminGetIdentitiesFilters,
|
||||
AdminGetUsersFilters,
|
||||
AdminIntegrationsConfig,
|
||||
TGetInvalidatingCacheStatus,
|
||||
TGetServerRootKmsEncryptionDetails,
|
||||
TServerConfig
|
||||
} from "./types";
|
||||
@@ -22,8 +23,10 @@ export const adminQueryKeys = {
|
||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
||||
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
||||
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
||||
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
|
||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
|
||||
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
|
||||
getInvalidateCache: () => ["admin-invalidate-cache"] as const,
|
||||
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const
|
||||
};
|
||||
|
||||
export const fetchServerConfig = async () => {
|
||||
@@ -118,3 +121,18 @@ export const useGetServerRootKmsEncryptionDetails = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetInvalidatingCacheStatus = (enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: adminQueryKeys.getInvalidateCache(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGetInvalidatingCacheStatus>(
|
||||
"/api/v1/admin/invalidating-cache-status"
|
||||
);
|
||||
|
||||
return data.invalidating;
|
||||
},
|
||||
enabled,
|
||||
refetchInterval: (data) => (data ? 3000 : false)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ export type TServerConfig = {
|
||||
enabledLoginMethods: LoginMethod[];
|
||||
authConsentContent?: string;
|
||||
pageFrameContent?: string;
|
||||
invalidatingCache: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateServerConfigDTO = {
|
||||
@@ -84,3 +85,16 @@ export enum RootKeyEncryptionStrategy {
|
||||
Software = "SOFTWARE",
|
||||
HSM = "HSM"
|
||||
}
|
||||
|
||||
export enum CacheType {
|
||||
ALL = "all",
|
||||
SECRETS = "secrets"
|
||||
}
|
||||
|
||||
export type TInvalidateCacheDTO = {
|
||||
type: CacheType;
|
||||
};
|
||||
|
||||
export type TGetInvalidatingCacheStatus = {
|
||||
invalidating: boolean;
|
||||
};
|
||||
|
||||
@@ -183,10 +183,17 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
"Microsoft Teams Workflow Integration Check Installation Status",
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS]: "Get Microsoft Teams tenant teams",
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET]: "Get Microsoft Teams Workflow Integration",
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST]: "List Microsoft Teams Workflow Integration"
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST]:
|
||||
"List Microsoft Teams Workflow Integration",
|
||||
|
||||
[EventType.LOGIN_IDENTITY_LDAP_AUTH]: "Identity login via LDAP Auth",
|
||||
[EventType.ADD_IDENTITY_LDAP_AUTH]: "Attached LDAP Auth to identity",
|
||||
[EventType.UPDATE_IDENTITY_LDAP_AUTH]: "Updated LDAP Auth for identity",
|
||||
[EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity",
|
||||
[EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
|
||||
[UserAgentType.WEB]: "Web",
|
||||
[UserAgentType.CLI]: "CLI",
|
||||
[UserAgentType.K8_OPERATOR]: "K8s operator",
|
||||
|
||||
@@ -47,6 +47,13 @@ export enum EventType {
|
||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
||||
|
||||
LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth",
|
||||
ADD_IDENTITY_LDAP_AUTH = "add-identity-ldap-auth",
|
||||
UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth",
|
||||
GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth",
|
||||
REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth",
|
||||
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
|
||||
@@ -8,5 +8,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
|
||||
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth",
|
||||
[IdentityAuthMethod.AZURE_AUTH]: "Azure Auth",
|
||||
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth",
|
||||
[IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth",
|
||||
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth"
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export enum IdentityAuthMethod {
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth",
|
||||
OIDC_AUTH = "oidc-auth",
|
||||
LDAP_AUTH = "ldap-auth",
|
||||
JWT_AUTH = "jwt-auth"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,4 @@
|
||||
export { identityAuthToNameMap } from "./constants";
|
||||
export { IdentityAuthMethod } from "./enums";
|
||||
export {
|
||||
useAddIdentityAwsAuth,
|
||||
useAddIdentityAzureAuth,
|
||||
useAddIdentityGcpAuth,
|
||||
useAddIdentityJwtAuth,
|
||||
useAddIdentityKubernetesAuth,
|
||||
useAddIdentityOidcAuth,
|
||||
useAddIdentityTokenAuth,
|
||||
useAddIdentityUniversalAuth,
|
||||
useCreateIdentity,
|
||||
useCreateIdentityUniversalAuthClientSecret,
|
||||
useCreateTokenIdentityTokenAuth,
|
||||
useDeleteIdentity,
|
||||
useDeleteIdentityAwsAuth,
|
||||
useDeleteIdentityAzureAuth,
|
||||
useDeleteIdentityGcpAuth,
|
||||
useDeleteIdentityJwtAuth,
|
||||
useDeleteIdentityKubernetesAuth,
|
||||
useDeleteIdentityOidcAuth,
|
||||
useDeleteIdentityTokenAuth,
|
||||
useDeleteIdentityUniversalAuth,
|
||||
useRevokeIdentityTokenAuthToken,
|
||||
useRevokeIdentityUniversalAuthClientSecret,
|
||||
useUpdateIdentity,
|
||||
useUpdateIdentityAwsAuth,
|
||||
useUpdateIdentityAzureAuth,
|
||||
useUpdateIdentityGcpAuth,
|
||||
useUpdateIdentityJwtAuth,
|
||||
useUpdateIdentityKubernetesAuth,
|
||||
useUpdateIdentityOidcAuth,
|
||||
useUpdateIdentityTokenAuth,
|
||||
useUpdateIdentityTokenAuthToken,
|
||||
useUpdateIdentityUniversalAuth
|
||||
} from "./mutations";
|
||||
export {
|
||||
useGetIdentityAwsAuth,
|
||||
useGetIdentityAzureAuth,
|
||||
useGetIdentityById,
|
||||
useGetIdentityGcpAuth,
|
||||
useGetIdentityJwtAuth,
|
||||
useGetIdentityKubernetesAuth,
|
||||
useGetIdentityOidcAuth,
|
||||
useGetIdentityProjectMemberships,
|
||||
useGetIdentityTokenAuth,
|
||||
useGetIdentityTokensTokenAuth,
|
||||
useGetIdentityUniversalAuth,
|
||||
useGetIdentityUniversalAuthClientSecrets,
|
||||
useSearchIdentities
|
||||
} from "./queries";
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
AddIdentityGcpAuthDTO,
|
||||
AddIdentityJwtAuthDTO,
|
||||
AddIdentityKubernetesAuthDTO,
|
||||
AddIdentityLdapAuthDTO,
|
||||
AddIdentityOidcAuthDTO,
|
||||
AddIdentityTokenAuthDTO,
|
||||
AddIdentityUniversalAuthDTO,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
DeleteIdentityGcpAuthDTO,
|
||||
DeleteIdentityJwtAuthDTO,
|
||||
DeleteIdentityKubernetesAuthDTO,
|
||||
DeleteIdentityLdapAuthDTO,
|
||||
DeleteIdentityOidcAuthDTO,
|
||||
DeleteIdentityTokenAuthDTO,
|
||||
DeleteIdentityUniversalAuthClientSecretDTO,
|
||||
@@ -36,6 +38,7 @@ import {
|
||||
IdentityGcpAuth,
|
||||
IdentityJwtAuth,
|
||||
IdentityKubernetesAuth,
|
||||
IdentityLdapAuth,
|
||||
IdentityOidcAuth,
|
||||
IdentityTokenAuth,
|
||||
IdentityUniversalAuth,
|
||||
@@ -47,6 +50,7 @@ import {
|
||||
UpdateIdentityGcpAuthDTO,
|
||||
UpdateIdentityJwtAuthDTO,
|
||||
UpdateIdentityKubernetesAuthDTO,
|
||||
UpdateIdentityLdapAuthDTO,
|
||||
UpdateIdentityOidcAuthDTO,
|
||||
UpdateIdentityTokenAuthDTO,
|
||||
UpdateIdentityUniversalAuthDTO,
|
||||
@@ -1049,3 +1053,116 @@ export const useRevokeIdentityTokenAuthToken = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddIdentityLdapAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityLdapAuth, object, AddIdentityLdapAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
||||
{
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
return data.identityLdapAuth;
|
||||
},
|
||||
onSuccess: (_, { identityId, organizationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identitiesKeys.getIdentityLdapAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityLdapAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityLdapAuth, object, UpdateIdentityLdapAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
||||
{
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
return data.identityLdapAuth;
|
||||
},
|
||||
onSuccess: (_, { identityId, organizationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identitiesKeys.getIdentityLdapAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteIdentityLdapAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityLdapAuth, object, DeleteIdentityLdapAuthDTO>({
|
||||
mutationFn: async ({ identityId }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/auth/ldap-auth/identities/${identityId}`);
|
||||
return data.identityLdapAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId, identityId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identitiesKeys.getIdentityLdapAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
IdentityGcpAuth,
|
||||
IdentityJwtAuth,
|
||||
IdentityKubernetesAuth,
|
||||
IdentityLdapAuth,
|
||||
IdentityMembership,
|
||||
IdentityMembershipOrg,
|
||||
IdentityOidcAuth,
|
||||
@@ -34,6 +35,7 @@ export const identitiesKeys = {
|
||||
getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const,
|
||||
getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const,
|
||||
getIdentityJwtAuth: (identityId: string) => [{ identityId }, "identity-jwt-auth"] as const,
|
||||
getIdentityLdapAuth: (identityId: string) => [{ identityId }, "identity-ldap-auth"] as const,
|
||||
getIdentityTokensTokenAuth: (identityId: string) =>
|
||||
[{ identityId }, "identity-tokens-token-auth"] as const,
|
||||
getIdentityProjectMemberships: (identityId: string) =>
|
||||
@@ -231,6 +233,27 @@ export const useGetIdentityTokenAuth = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityLdapAuth = (
|
||||
identityId: string,
|
||||
options?: TReactQueryOptions["options"]
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: identitiesKeys.getIdentityLdapAuth(identityId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { identityLdapAuth }
|
||||
} = await apiRequest.get<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||
`/api/v1/auth/ldap-auth/identities/${identityId}`
|
||||
);
|
||||
return identityLdapAuth;
|
||||
},
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
...options,
|
||||
enabled: Boolean(identityId) && (options?.enabled ?? true)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityTokensTokenAuth = (identityId: string) => {
|
||||
return useQuery({
|
||||
enabled: Boolean(identityId),
|
||||
|
||||
@@ -425,6 +425,72 @@ export type IdentityTokenAuth = {
|
||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||
};
|
||||
|
||||
export type AddIdentityLdapAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
searchFilter: string;
|
||||
ldapCaCertificate?: string;
|
||||
allowedFields?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type UpdateIdentityLdapAuthDTO = {
|
||||
identityId: string;
|
||||
organizationId: string;
|
||||
url?: string;
|
||||
bindDN?: string;
|
||||
bindPass?: string;
|
||||
searchBase?: string;
|
||||
searchFilter?: string;
|
||||
ldapCaCertificate?: string;
|
||||
allowedFields?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type DeleteIdentityLdapAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export type IdentityLdapAuth = {
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
searchFilter: string;
|
||||
ldapCaCertificate?: string;
|
||||
allowedFields?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
|
||||
identityId: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||
};
|
||||
|
||||
export type AddIdentityTokenAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
|
||||
@@ -6,14 +6,17 @@ import {
|
||||
TProjectTemplate,
|
||||
TProjectTemplateResponse
|
||||
} from "@app/hooks/api/projectTemplates/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const projectTemplateKeys = {
|
||||
all: ["project-template"] as const,
|
||||
list: () => [...projectTemplateKeys.all, "list"] as const,
|
||||
list: (projectType?: ProjectType) =>
|
||||
[...projectTemplateKeys.all, "list", ...(projectType ? [projectType] : [])] as const,
|
||||
byId: (templateId: string) => [...projectTemplateKeys.all, templateId] as const
|
||||
};
|
||||
|
||||
export const useListProjectTemplates = (
|
||||
type?: ProjectType,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TProjectTemplate[],
|
||||
@@ -25,9 +28,11 @@ export const useListProjectTemplates = (
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: projectTemplateKeys.list(),
|
||||
queryKey: projectTemplateKeys.list(type),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates");
|
||||
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates", {
|
||||
params: { type }
|
||||
});
|
||||
|
||||
return data.projectTemplates;
|
||||
},
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export type TProjectTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ProjectType;
|
||||
description?: string;
|
||||
roles: Pick<TProjectRole, "slug" | "name" | "permissions">[];
|
||||
environments: { name: string; slug: string; position: number }[];
|
||||
environments?: { name: string; slug: string; position: number }[] | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -14,6 +16,7 @@ export type TListProjectTemplates = { projectTemplates: TProjectTemplate[] };
|
||||
export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate };
|
||||
|
||||
export type TCreateProjectTemplateDTO = {
|
||||
type: ProjectType;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,9 @@ export enum ProjectMembershipRole {
|
||||
Member = "member",
|
||||
Custom = "custom",
|
||||
Viewer = "viewer",
|
||||
NoAccess = "no-access"
|
||||
NoAccess = "no-access",
|
||||
SshHostBootstrapper = "ssh-host-bootstrapper",
|
||||
KmsCryptographicOperator = "cryptographic-operator"
|
||||
}
|
||||
|
||||
export type TGetProjectRolesDTO = {
|
||||
|
||||
@@ -10,15 +10,17 @@ import {
|
||||
} from "./types";
|
||||
|
||||
export const useCreateNewInstallationSession = () => {
|
||||
return useMutation<{ sessionId: string }, object, { organizationId: string }>({
|
||||
mutationFn: async (opt) => {
|
||||
const { data } = await apiRequest.post(
|
||||
"/api/v1/secret-scanning/create-installation-session/organization",
|
||||
opt
|
||||
);
|
||||
return data;
|
||||
return useMutation<{ sessionId: string; gitAppSlug: string }, object, { organizationId: string }>(
|
||||
{
|
||||
mutationFn: async (opt) => {
|
||||
const { data } = await apiRequest.post(
|
||||
"/api/v1/secret-scanning/create-installation-session/organization",
|
||||
opt
|
||||
);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdateRiskStatus = () => {
|
||||
|
||||
@@ -6,7 +6,8 @@ export enum LoginMappingSource {
|
||||
export type TLoginMapping = {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
usernames?: string[];
|
||||
groups?: string[];
|
||||
};
|
||||
source: LoginMappingSource;
|
||||
};
|
||||
|
||||
@@ -16,8 +16,11 @@ export const workspaceKeys = {
|
||||
type ? ["workspaces", { type }] : (["workspaces"] as const),
|
||||
getWorkspaceAuditLogs: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-audit-logs"] as const,
|
||||
getWorkspaceUsers: (workspaceId: string, includeGroupMembers?: boolean, roles?: string[]) =>
|
||||
[{ workspaceId, includeGroupMembers, roles }, "workspace-users"] as const,
|
||||
getWorkspaceUsers: (
|
||||
workspaceId: string,
|
||||
includeGroupMembers: boolean = false,
|
||||
roles: string[] = []
|
||||
) => [{ workspaceId, includeGroupMembers, roles }, "workspace-users"] as const,
|
||||
getWorkspaceUserDetails: (workspaceId: string, membershipId: string) =>
|
||||
[{ workspaceId, membershipId }, "workspace-user-details"] as const,
|
||||
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { useDebounce } from "./useDebounce";
|
||||
export * from "./useGetProjectTypeFromRoute";
|
||||
export { usePagination } from "./usePagination";
|
||||
export { usePersistentState } from "./usePersistentState";
|
||||
export { usePopUp } from "./usePopUp";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useMemo } from "react";
|
||||
import { useRouterState } from "@tanstack/react-router";
|
||||
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const useGetProjectTypeFromRoute = () => {
|
||||
const { location } = useRouterState();
|
||||
|
||||
return useMemo(() => {
|
||||
const segments = location.pathname.split("/");
|
||||
|
||||
let type: ProjectType | undefined;
|
||||
|
||||
// location of project type can vary in router path, so we need to check all possible values
|
||||
segments.forEach((segment) => {
|
||||
if (Object.values(ProjectType).includes(segment as ProjectType))
|
||||
type = segment as ProjectType;
|
||||
});
|
||||
|
||||
return type;
|
||||
}, [location]);
|
||||
};
|
||||
@@ -11,11 +11,12 @@ import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2";
|
||||
import { OrgPermissionSubjects, useOrgPermission, useServerConfig } from "@app/context";
|
||||
import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
||||
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
|
||||
import { SidebarHeader } from "./components/SidebarHeader";
|
||||
import { DefaultSideBar, SecretSharingSideBar } from "./ProductsSideBar";
|
||||
import { DefaultSideBar, ProjectOverviewSideBar, SecretSharingSideBar } from "./ProductsSideBar";
|
||||
|
||||
export const OrganizationLayout = () => {
|
||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
||||
@@ -45,21 +46,38 @@ export const OrganizationLayout = () => {
|
||||
] as string[]
|
||||
).includes(location.pathname);
|
||||
|
||||
const isProjectOverviewOrSettingsPage = (
|
||||
[
|
||||
linkOptions({ to: "/organization/secret-manager/overview" }).to,
|
||||
linkOptions({ to: "/organization/secret-manager/settings" }).to,
|
||||
linkOptions({ to: "/organization/cert-manager/overview" }).to,
|
||||
linkOptions({ to: "/organization/cert-manager/settings" }).to,
|
||||
linkOptions({ to: "/organization/kms/overview" }).to,
|
||||
linkOptions({ to: "/organization/kms/settings" }).to,
|
||||
linkOptions({ to: "/organization/ssh/overview" }).to,
|
||||
linkOptions({ to: "/organization/ssh/settings" }).to
|
||||
] as string[]
|
||||
).includes(location.pathname);
|
||||
|
||||
const shouldShowOrgSidebar =
|
||||
location.pathname.startsWith("/organization") &&
|
||||
(!isSecretSharingPage || shouldShowProductsSidebar) &&
|
||||
!(
|
||||
[
|
||||
linkOptions({ to: "/organization/secret-manager/overview" }).to,
|
||||
linkOptions({ to: "/organization/cert-manager/overview" }).to,
|
||||
linkOptions({ to: "/organization/ssh/overview" }).to,
|
||||
linkOptions({ to: "/organization/kms/overview" }).to,
|
||||
linkOptions({ to: "/organization/secret-scanning" }).to
|
||||
] as string[]
|
||||
).includes(location.pathname);
|
||||
!([linkOptions({ to: "/organization/secret-scanning" }).to] as string[]).includes(
|
||||
location.pathname
|
||||
);
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
let SideBarComponent = <DefaultSideBar />;
|
||||
|
||||
if (isSecretSharingPage) {
|
||||
SideBarComponent = <SecretSharingSideBar />;
|
||||
} else if (isProjectOverviewOrSettingsPage) {
|
||||
SideBarComponent = (
|
||||
<ProjectOverviewSideBar type={location.pathname.split("/")[2] as ProjectType} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Banner />
|
||||
@@ -80,10 +98,12 @@ export const OrganizationLayout = () => {
|
||||
className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div className="p-2 pt-3">
|
||||
<SidebarHeader />
|
||||
</div>
|
||||
{isSecretSharingPage ? <SecretSharingSideBar /> : <DefaultSideBar />}
|
||||
{!isProjectOverviewOrSettingsPage && (
|
||||
<div className="p-2 pt-3">
|
||||
<SidebarHeader />
|
||||
</div>
|
||||
)}
|
||||
{SideBarComponent}
|
||||
</nav>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useMatchRoute } from "@tanstack/react-router";
|
||||
|
||||
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type TProjectOverviewSideBarProps = {
|
||||
type: ProjectType;
|
||||
};
|
||||
|
||||
export const ProjectOverviewSideBar = ({ type }: TProjectOverviewSideBarProps) => {
|
||||
const matchRoute = useMatchRoute();
|
||||
|
||||
const isOverviewActive = !!matchRoute({
|
||||
to: `/organization/${type}/overview`,
|
||||
fuzzy: false
|
||||
});
|
||||
|
||||
let label: string;
|
||||
let icon: string;
|
||||
let link: string;
|
||||
|
||||
switch (type) {
|
||||
case ProjectType.CertificateManager:
|
||||
label = "Cert Management";
|
||||
icon = "note";
|
||||
link = "https://infisical.com/docs/documentation/platform/pki/overview";
|
||||
break;
|
||||
case ProjectType.SecretManager:
|
||||
label = "Secret Management";
|
||||
icon = "sliding-carousel";
|
||||
link = "https://infisical.com/docs/documentation/getting-started/introduction";
|
||||
break;
|
||||
case ProjectType.KMS:
|
||||
label = "KMS";
|
||||
icon = "unlock";
|
||||
link = "https://infisical.com/docs/documentation/platform/kms/overview";
|
||||
break;
|
||||
case ProjectType.SSH:
|
||||
label = "SSH";
|
||||
icon = "verified";
|
||||
link = "https://infisical.com/docs/documentation/platform/ssh/overview";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unknown project type");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-2 pt-3">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={link}
|
||||
className="flex w-full items-center rounded-md border border-mineshaft-600 p-2 pl-3 transition-all duration-150 hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-mineshaft-500 p-4">
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="h-3.5 w-3.5 text-mineshaft-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="-mt-1 flex flex-grow flex-col text-white">
|
||||
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
|
||||
Infisical Docs
|
||||
</div>
|
||||
<div className="text-xs leading-[10px] text-mineshaft-400">
|
||||
{type === ProjectType.SecretManager ? "Get Started" : `${label} Overview`}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<Menu>
|
||||
<MenuGroup title="Overview">
|
||||
<Link to={`/organization/${type}/overview`}>
|
||||
<MenuItem isSelected={isOverviewActive} icon={icon}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Other">
|
||||
<Link to={`/organization/${type}/settings`}>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="toggle-settings">
|
||||
Settings
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./DefaultSideBar";
|
||||
export * from "./ProjectOverviewSideBar";
|
||||
export * from "./SecretSharingSideBar";
|
||||
|
||||
+10
-4
@@ -269,7 +269,9 @@ export const MinimizedOrgSidebar = () => {
|
||||
<MenuIconButton
|
||||
isSelected={
|
||||
isActive ||
|
||||
window.location.pathname.startsWith(`/${ProjectType.SecretManager}`)
|
||||
window.location.pathname.startsWith(
|
||||
`/organization/${ProjectType.SecretManager}`
|
||||
)
|
||||
}
|
||||
icon="sliding-carousel"
|
||||
>
|
||||
@@ -282,7 +284,9 @@ export const MinimizedOrgSidebar = () => {
|
||||
<MenuIconButton
|
||||
isSelected={
|
||||
isActive ||
|
||||
window.location.pathname.startsWith(`/${ProjectType.CertificateManager}`)
|
||||
window.location.pathname.startsWith(
|
||||
`/organization/${ProjectType.CertificateManager}`
|
||||
)
|
||||
}
|
||||
icon="note"
|
||||
>
|
||||
@@ -294,7 +298,8 @@ export const MinimizedOrgSidebar = () => {
|
||||
{({ isActive }) => (
|
||||
<MenuIconButton
|
||||
isSelected={
|
||||
isActive || window.location.pathname.startsWith(`/${ProjectType.KMS}`)
|
||||
isActive ||
|
||||
window.location.pathname.startsWith(`/organization/${ProjectType.KMS}`)
|
||||
}
|
||||
icon="unlock"
|
||||
>
|
||||
@@ -306,7 +311,8 @@ export const MinimizedOrgSidebar = () => {
|
||||
{({ isActive }) => (
|
||||
<MenuIconButton
|
||||
isSelected={
|
||||
isActive || window.location.pathname.startsWith(`/${ProjectType.SSH}`)
|
||||
isActive ||
|
||||
window.location.pathname.startsWith(`/organization/${ProjectType.SSH}`)
|
||||
}
|
||||
icon="verified"
|
||||
>
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
||||
|
||||
import { AuthPanel } from "./components/AuthPanel";
|
||||
import { CachingPanel } from "./components/CachingPanel";
|
||||
import { EncryptionPanel } from "./components/EncryptionPanel";
|
||||
import { IntegrationPanel } from "./components/IntegrationPanel";
|
||||
import { UserPanel } from "./components/UserPanel";
|
||||
@@ -42,7 +43,8 @@ enum TabSections {
|
||||
Integrations = "integrations",
|
||||
Users = "users",
|
||||
Identities = "identities",
|
||||
Kmip = "kmip"
|
||||
Kmip = "kmip",
|
||||
Caching = "caching"
|
||||
}
|
||||
|
||||
enum SignUpModes {
|
||||
@@ -164,6 +166,7 @@ export const OverviewPage = () => {
|
||||
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
||||
<Tab value={TabSections.Users}>User Identities</Tab>
|
||||
<Tab value={TabSections.Identities}>Machine Identities</Tab>
|
||||
<Tab value={TabSections.Caching}>Caching</Tab>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Settings}>
|
||||
@@ -408,6 +411,9 @@ export const OverviewPage = () => {
|
||||
<TabPanel value={TabSections.Identities}>
|
||||
<IdentityPanel />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Caching}>
|
||||
<CachingPanel />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Badge, Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useInvalidateCache } from "@app/hooks/api";
|
||||
import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries";
|
||||
import { CacheType } from "@app/hooks/api/admin/types";
|
||||
|
||||
export const CachingPanel = () => {
|
||||
const { mutateAsync: invalidateCache } = useInvalidateCache();
|
||||
const { user } = useUser();
|
||||
|
||||
const [type, setType] = useState<CacheType | null>(null);
|
||||
const [shouldPoll, setShouldPoll] = useState(false);
|
||||
|
||||
const {
|
||||
data: invalidationStatus,
|
||||
isFetching,
|
||||
refetch
|
||||
} = useGetInvalidatingCacheStatus(shouldPoll);
|
||||
const isInvalidating = Boolean(shouldPoll && (isFetching || invalidationStatus));
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"invalidateCache"
|
||||
] as const);
|
||||
|
||||
const handleInvalidateCacheSubmit = async () => {
|
||||
if (!type || isInvalidating) return;
|
||||
|
||||
try {
|
||||
await invalidateCache({ type });
|
||||
createNotification({ text: `Began invalidating ${type} cache`, type: "success" });
|
||||
setShouldPoll(true);
|
||||
handlePopUpClose("invalidateCache");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isInvalidating) return;
|
||||
|
||||
if (shouldPoll) {
|
||||
setShouldPoll(false);
|
||||
createNotification({ text: "Successfully invalidated cache", type: "success" });
|
||||
}
|
||||
}, [isInvalidating, shouldPoll]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch().then((v) => setShouldPoll(v.data || false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 flex flex-wrap items-end justify-between gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center gap-3">
|
||||
<span className="text-xl font-semibold text-mineshaft-100">Secrets Cache</span>
|
||||
{isInvalidating && (
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
|
||||
Invalidating Cache
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="max-w-xl text-sm text-mineshaft-400">
|
||||
The encrypted secrets cache encompasses all secrets stored within the system and
|
||||
provides a temporary, secure storage location for frequently accessed credentials.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
onClick={() => {
|
||||
setType(CacheType.SECRETS);
|
||||
handlePopUpOpen("invalidateCache");
|
||||
}}
|
||||
isDisabled={!user.superAdmin || isInvalidating}
|
||||
>
|
||||
Invalidate Secrets Cache
|
||||
</Button>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.invalidateCache.isOpen}
|
||||
title={`Are you sure you want to invalidate ${type} cache?`}
|
||||
subTitle="This action is permanent and irreversible. The cache invalidation process may take several minutes to complete."
|
||||
onChange={(isOpen) => handlePopUpToggle("invalidateCache", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleInvalidateCacheSubmit}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -52,7 +52,7 @@ export const MicrosoftTeamsIntegrationForm = ({ adminIntegrationsConfig }: Props
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Updated admin Microsoft Teams configuration",
|
||||
text: "Updated admin Microsoft Teams configuration. It can take up to 5 minutes to take effect.",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
+12
@@ -13,6 +13,7 @@ import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
|
||||
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
|
||||
import { IdentityJwtAuthForm } from "./IdentityJwtAuthForm";
|
||||
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
|
||||
import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm";
|
||||
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
|
||||
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
|
||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||
@@ -46,6 +47,7 @@ const identityAuthMethods = [
|
||||
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
|
||||
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH },
|
||||
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH },
|
||||
{ label: "LDAP Auth", value: IdentityAuthMethod.LDAP_AUTH },
|
||||
{
|
||||
label: "JWT Auth",
|
||||
value: IdentityAuthMethod.JWT_AUTH
|
||||
@@ -186,6 +188,16 @@ export const IdentityAuthMethodModalContent = ({
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
[IdentityAuthMethod.LDAP_AUTH]: {
|
||||
render: () => (
|
||||
<IdentityLdapAuthForm
|
||||
identityId={identityAuthMethodData.identityId}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+608
@@ -0,0 +1,608 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { faPlus, faQuestionCircle, faXmark } 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,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityLdapAuth,
|
||||
useGetIdentityLdapAuth,
|
||||
useUpdateIdentityLdapAuth
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityFormTab } from "./types";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
url: z.string().min(1),
|
||||
bindDN: z.string(),
|
||||
bindPass: z.string(),
|
||||
searchBase: z.string(),
|
||||
searchFilter: z.string(), // defaults to (uid={{username}})
|
||||
ldapCaCertificate: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val || undefined),
|
||||
allowedFields: z
|
||||
.object({
|
||||
key: z.string().trim(),
|
||||
value: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((val) => val.replace(/\s/g, ""))
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
|
||||
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
||||
message: "Access Token TTL cannot be greater than 315360000"
|
||||
}),
|
||||
accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
||||
message: "Access Token Max TTL cannot be greater than 315360000"
|
||||
}),
|
||||
accessTokenNumUsesLimit: z.string(),
|
||||
accessTokenTrustedIps: z
|
||||
.array(
|
||||
z.object({
|
||||
ipAddress: z.string().max(50)
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
identityId?: string;
|
||||
isUpdate?: boolean;
|
||||
};
|
||||
|
||||
export const IdentityLdapAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityId,
|
||||
isUpdate
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityLdapAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth();
|
||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||
|
||||
const { data } = useGetIdentityLdapAuth(identityId ?? "", {
|
||||
enabled: isUpdate
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
searchFilter: "(uid={{username}})",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
fields: accessTokenTrustedIpsFields,
|
||||
append: appendAccessTokenTrustedIp,
|
||||
remove: removeAccessTokenTrustedIp
|
||||
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
||||
|
||||
const {
|
||||
fields: allowedFieldsFields,
|
||||
append: appendAllowedField,
|
||||
remove: removeAllowedField
|
||||
} = useFieldArray({ control, name: "allowedFields" });
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
url: data.url,
|
||||
bindDN: data.bindDN,
|
||||
bindPass: data.bindPass,
|
||||
searchBase: data.searchBase,
|
||||
searchFilter: data.searchFilter,
|
||||
ldapCaCertificate: data.ldapCaCertificate || undefined,
|
||||
allowedFields: data.allowedFields,
|
||||
accessTokenTTL: String(data.accessTokenTTL),
|
||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
|
||||
({ ipAddress, prefix }: IdentityTrustedIp) => {
|
||||
return {
|
||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||
};
|
||||
}
|
||||
)
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
searchFilter: "(uid={{username}})",
|
||||
ldapCaCertificate: undefined,
|
||||
allowedFields: [],
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!subscription?.ldap) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!identityId) return;
|
||||
|
||||
if (data) {
|
||||
await updateMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
} else {
|
||||
await addMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch {
|
||||
createNotification({
|
||||
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||
setTabValue(
|
||||
[
|
||||
"url",
|
||||
"bindDN",
|
||||
"bindPass",
|
||||
"searchBase",
|
||||
"searchFilter",
|
||||
"accessTokenTTL",
|
||||
"allowedFields",
|
||||
"accessTokenMaxTTL",
|
||||
"accessTokenNumUsesLimit"
|
||||
].includes(Object.keys(fields)[0])
|
||||
? IdentityFormTab.Configuration
|
||||
: IdentityFormTab.Advanced
|
||||
);
|
||||
})}
|
||||
>
|
||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||
<TabList>
|
||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IdentityFormTab.Configuration}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="url"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="LDAP URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="ldaps://domain-or-ip:636" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="bindDN"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Bind DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="bindPass"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Bind Pass"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="********" type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="searchBase"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Search Base / DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="ou=machines,dc=acme,dc=com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="searchFilter"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Search Filter"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="(uid={{username}})" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{allowedFieldsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`allowedFields.${index}.key`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
const isFirstField = index === 0;
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={isFirstField ? "Required Attributes" : undefined}
|
||||
icon={
|
||||
isFirstField ? (
|
||||
<Tooltip
|
||||
className="max-w-[420px]"
|
||||
content={
|
||||
<div className="max-h-[300px] space-y-4 overflow-y-auto text-sm">
|
||||
<p>
|
||||
Specify the fields that the user must contain in their LDAP entry
|
||||
in order to authenticate with this identity. If nothing is
|
||||
specified, all users in the configured LDAP directory will be able
|
||||
to authenticate.
|
||||
<p className="mt-2">
|
||||
You can specify multiple required attributes by separating them
|
||||
with a comma.
|
||||
</p>
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p>Example:</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
'uid' → 'user1,user2,user3'
|
||||
<br />
|
||||
'mail' → 'user@example.com'
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
The above example would allow users with the UID user1, user2, or
|
||||
user3 to authenticate but only if their emails also match
|
||||
user@example.com
|
||||
</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="uid"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`allowedFields.${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="userid1,userid2,userid3"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => removeAllowedField(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={() =>
|
||||
appendAllowedField({
|
||||
key: "",
|
||||
value: ""
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add Required Attribute
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time."
|
||||
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}
|
||||
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
>
|
||||
<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}
|
||||
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ldapCaCertificate"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
tooltipText="An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
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={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</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>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -30,7 +30,7 @@ export const Route = createFileRoute(
|
||||
link: linkOptions({ to: "/" })
|
||||
},
|
||||
{
|
||||
label: "access control"
|
||||
label: "Access Control"
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -38,7 +38,7 @@ import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import {
|
||||
eventToNameMap,
|
||||
secretEvents,
|
||||
userAgentTTypeoNameMap
|
||||
userAgentTypeToNameMap
|
||||
} from "@app/hooks/api/auditLogs/constants";
|
||||
import { EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { UserAgentType } from "@app/hooks/api/auth/types";
|
||||
@@ -47,7 +47,7 @@ import { LogFilterItem } from "./LogFilterItem";
|
||||
import { AuditLogFilterFormData, Presets } from "./types";
|
||||
|
||||
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
|
||||
const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({
|
||||
const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label]) => ({
|
||||
label,
|
||||
value
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
export const CertManagerSettingsPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Cert Management Settings</title>
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="Cert Management Settings" />
|
||||
<ProjectSettings />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { CertManagerSettingsPage } from "./CertManagerSettingsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/settings"
|
||||
)({
|
||||
component: CertManagerSettingsPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Products",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />
|
||||
},
|
||||
{
|
||||
label: "Cert Management",
|
||||
link: linkOptions({ to: "/organization/cert-manager/overview" })
|
||||
},
|
||||
{
|
||||
label: "Settings"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
+2
-11
@@ -6,9 +6,9 @@ import { format } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
export enum TabSections {
|
||||
@@ -26,15 +26,6 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
||||
if (role === ProjectMembershipRole.Admin) return "Admin";
|
||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
||||
if (role === ProjectMembershipRole.Viewer) return "Viewer";
|
||||
if (role === ProjectMembershipRole.NoAccess) return "No Access";
|
||||
return role;
|
||||
};
|
||||
|
||||
export const IdentityProjectRow = ({
|
||||
membership: { id, createdAt, identity, project, roles },
|
||||
handlePopUpOpen
|
||||
@@ -80,7 +71,7 @@ export const IdentityProjectRow = ({
|
||||
<Td>
|
||||
<Tag size="xs">{project.type}</Tag>
|
||||
</Td>
|
||||
<Td>{`${formatRoleName(roles[0].role, roles[0].customRoleName)}${
|
||||
<Td>{`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${
|
||||
roles.length > 1 ? ` (+${roles.length - 1})` : ""
|
||||
}`}</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
|
||||
+7
@@ -11,6 +11,7 @@ import {
|
||||
useDeleteIdentityGcpAuth,
|
||||
useDeleteIdentityJwtAuth,
|
||||
useDeleteIdentityKubernetesAuth,
|
||||
useDeleteIdentityLdapAuth,
|
||||
useDeleteIdentityOidcAuth,
|
||||
useDeleteIdentityTokenAuth,
|
||||
useDeleteIdentityUniversalAuth
|
||||
@@ -22,6 +23,7 @@ import { ViewIdentityAzureAuthContent } from "./ViewIdentityAzureAuthContent";
|
||||
import { ViewIdentityGcpAuthContent } from "./ViewIdentityGcpAuthContent";
|
||||
import { ViewIdentityJwtAuthContent } from "./ViewIdentityJwtAuthContent";
|
||||
import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthContent";
|
||||
import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent";
|
||||
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
|
||||
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
|
||||
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
|
||||
@@ -61,6 +63,7 @@ export const Content = ({
|
||||
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
|
||||
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
|
||||
const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth();
|
||||
const { mutateAsync: revokeLdapAuth } = useDeleteIdentityLdapAuth();
|
||||
|
||||
let Component: (props: ViewAuthMethodProps) => JSX.Element;
|
||||
let revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
|
||||
@@ -100,6 +103,10 @@ export const Content = ({
|
||||
revokeMethod = revokeJwtAuth;
|
||||
Component = ViewIdentityJwtAuthContent;
|
||||
break;
|
||||
case IdentityAuthMethod.LDAP_AUTH:
|
||||
revokeMethod = revokeLdapAuth;
|
||||
Component = ViewIdentityLdapAuthContent;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Auth Method: ${authMethod}`);
|
||||
}
|
||||
|
||||
+2
-2
@@ -47,10 +47,10 @@ export const ViewIdentityAwsAuthContent = ({
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
|
||||
+2
-2
@@ -47,10 +47,10 @@ export const ViewIdentityAzureAuthContent = ({
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
|
||||
+2
-2
@@ -47,10 +47,10 @@ export const ViewIdentityGcpAuthContent = ({
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
|
||||
+2
-2
@@ -49,10 +49,10 @@ export const ViewIdentityJwtAuthContent = ({
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
|
||||
+2
-2
@@ -51,10 +51,10 @@ export const ViewIdentityKubernetesAuthContent = ({
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useGetIdentityLdapAuth } from "@app/hooks/api";
|
||||
import { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm";
|
||||
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
|
||||
|
||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
||||
import { ViewAuthMethodProps } from "./types";
|
||||
|
||||
export const ViewIdentityLdapAuthContent = ({
|
||||
identityId,
|
||||
handlePopUpToggle,
|
||||
handlePopUpOpen,
|
||||
onDelete,
|
||||
popUp
|
||||
}: ViewAuthMethodProps) => {
|
||||
const { data, isPending } = useGetIdentityLdapAuth(identityId);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Spinner className="text-mineshaft-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<EmptyState icon={faBan} title="Could not find LDAP Auth associated with this Identity." />
|
||||
);
|
||||
}
|
||||
|
||||
if (popUp.identityAuthMethod.isOpen) {
|
||||
return (
|
||||
<IdentityLdapAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
{data.accessTokenNumUsesLimit}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="LDAP URL">{data.url}</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Bind DN">{data.bindDN}</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Bind Pass">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-2"
|
||||
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.bindPass}</p>}
|
||||
>
|
||||
<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>
|
||||
<IdentityAuthFieldDisplay label="Search Base / DN">
|
||||
{data.searchBase}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Search Filter">{data.searchFilter}</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="CA Certificate">
|
||||
{data.ldapCaCertificate && (
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-2"
|
||||
content={
|
||||
<p className="break-words rounded bg-mineshaft-600 p-2">{data.ldapCaCertificate}</p>
|
||||
}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -48,10 +48,10 @@ export const ViewIdentityOidcAuthContent = ({
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
|
||||
+2
-2
@@ -50,10 +50,10 @@ export const ViewIdentityTokenAuthContent = ({
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
|
||||
+2
-2
@@ -62,10 +62,10 @@ export const ViewIdentityUniversalAuthContent = ({
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
|
||||
@@ -20,7 +20,7 @@ export const Route = createFileRoute(
|
||||
link: linkOptions({ to: "/organization/access-management" })
|
||||
},
|
||||
{
|
||||
label: "identities"
|
||||
label: "Identities"
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
export const KmsSettingsPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>KMS Settings</title>
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="KMS Settings" />
|
||||
<ProjectSettings />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { KmsSettingsPage } from "./KmsSettingsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/kms/settings"
|
||||
)({
|
||||
component: KmsSettingsPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Products",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />
|
||||
},
|
||||
{
|
||||
label: "KMS",
|
||||
link: linkOptions({ to: "/organization/kms/overview" })
|
||||
},
|
||||
{
|
||||
label: "Settings"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
@@ -117,12 +117,8 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text =
|
||||
error?.response?.data?.message ??
|
||||
`Failed to ${popUp?.role?.data ? "update" : "create"} role`;
|
||||
} catch {
|
||||
const text = `Failed to ${popUp?.role?.data ? "update" : "create"} role`;
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ export const ProjectListToggle = ({ value, onChange }: Props) => {
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="absolute left-0 top-0 h-full w-full cursor-pointer opacity-0"
|
||||
dropdownContainerClassName="mt-10 ml-5"
|
||||
>
|
||||
<SelectItem value={ProjectListView.MyProjects}>My Projects</SelectItem>
|
||||
<SelectItem value={ProjectListView.AllProjects}>All Projects</SelectItem>
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
export const SecretManagerSettingsPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Secret Management Settings</title>
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="Secret Management Settings" />
|
||||
<ProjectSettings />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { SecretManagerSettingsPage } from "./SecretManagerSettingsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/settings"
|
||||
)({
|
||||
component: SecretManagerSettingsPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Products",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />
|
||||
},
|
||||
{
|
||||
label: "Secret Management",
|
||||
link: linkOptions({ to: "/organization/secret-manager/overview" })
|
||||
},
|
||||
{
|
||||
label: "Settings"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
@@ -108,7 +108,7 @@ export const SecretScanningPage = withPermission(
|
||||
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession({ organizationId });
|
||||
window.location.href = `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`;
|
||||
window.location.href = `https://github.com/apps/${session.gitAppSlug}/installations/new?state=${session.sessionId}`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+62
-3
@@ -1,8 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
import { Link, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
||||
import { ImportTab } from "../ImportTab";
|
||||
@@ -11,7 +14,6 @@ import { OrgAuthTab } from "../OrgAuthTab";
|
||||
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
|
||||
import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
|
||||
|
||||
export const OrgTabGroup = () => {
|
||||
const search = useSearch({
|
||||
@@ -28,7 +30,64 @@ export const OrgTabGroup = () => {
|
||||
},
|
||||
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
|
||||
{ name: "Import", key: "tab-import", component: ImportTab },
|
||||
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab },
|
||||
{
|
||||
name: "Project Templates",
|
||||
key: "project-templates",
|
||||
// scott: temporary, remove once users have adjusted
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
component: () => (
|
||||
<div>
|
||||
<NoticeBannerV2
|
||||
className="mx-auto"
|
||||
titleClassName="text-base"
|
||||
title="Project Templates Relocated"
|
||||
>
|
||||
<p className="mt-1 text-mineshaft-300">
|
||||
Project templates have been relocated and are now product specific:
|
||||
</p>
|
||||
<ul className="mb-1 flex gap-x-4 text-mineshaft-200">
|
||||
{[
|
||||
{
|
||||
type: ProjectType.SecretManager,
|
||||
label: "Secret Management",
|
||||
icon: "sliding-carousel"
|
||||
},
|
||||
{
|
||||
type: ProjectType.CertificateManager,
|
||||
label: "Certificate Management",
|
||||
icon: "note"
|
||||
},
|
||||
{
|
||||
type: ProjectType.KMS,
|
||||
label: "KMS",
|
||||
icon: "unlock"
|
||||
},
|
||||
{
|
||||
type: ProjectType.SSH,
|
||||
label: "SSH",
|
||||
icon: "verified"
|
||||
}
|
||||
].map(({ label, type, icon }, index) => (
|
||||
<li key={`project-template-${type}`}>
|
||||
<Link
|
||||
to={`/organization/${type}/settings`}
|
||||
className="mt-1 flex items-center gap-x-2 hover:text-mineshaft-100"
|
||||
>
|
||||
{index !== 0 && <span className="text-mineshaft-300">•</span>}
|
||||
<DotLottieReact
|
||||
src={`/lotties/${icon}.json`}
|
||||
loop
|
||||
className="mt-0.5 h-5 w-5"
|
||||
/>{" "}
|
||||
<span className="underline underline-offset-2">{label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</NoticeBannerV2>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{ name: "KMIP", key: "kmip", component: KmipTab }
|
||||
];
|
||||
|
||||
|
||||
@@ -1,33 +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 { 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 { SettingsPage } from "./SettingsPage";
|
||||
import { SettingsPage } from './SettingsPage'
|
||||
|
||||
const SettingsPageQueryParams = z.object({
|
||||
selectedTab: z.string().catch("")
|
||||
});
|
||||
selectedTab: z.string().catch(''),
|
||||
})
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/settings/',
|
||||
)({
|
||||
component: SettingsPage,
|
||||
validateSearch: zodValidator(SettingsPageQueryParams),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ selectedTab: "" })]
|
||||
middlewares: [stripSearchParams({ selectedTab: '' })],
|
||||
},
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Home",
|
||||
label: 'Home',
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />,
|
||||
link: linkOptions({ to: "/" })
|
||||
link: linkOptions({ to: '/' }),
|
||||
},
|
||||
{
|
||||
label: "Settings"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
label: 'Settings',
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
export const SshSettingsPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>SSH Settings</title>
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="SSH Settings" />
|
||||
<ProjectSettings />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { SshSettingsPage } from "./SshSettingsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/ssh/settings"
|
||||
)({
|
||||
component: SshSettingsPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Products",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />
|
||||
},
|
||||
{
|
||||
label: "SSH",
|
||||
link: linkOptions({ to: "/organization/ssh/overview" })
|
||||
},
|
||||
{
|
||||
label: "Settings"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
+2
-11
@@ -5,8 +5,8 @@ import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { OrgAccessControlTabSections } from "@app/types/org";
|
||||
@@ -19,15 +19,6 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
||||
if (role === ProjectMembershipRole.Admin) return "Admin";
|
||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
||||
if (role === ProjectMembershipRole.Viewer) return "Viewer";
|
||||
if (role === ProjectMembershipRole.NoAccess) return "No Access";
|
||||
return role;
|
||||
};
|
||||
|
||||
export const UserProjectRow = ({
|
||||
membership: { id, project, user, roles },
|
||||
handlePopUpOpen
|
||||
@@ -73,7 +64,7 @@ export const UserProjectRow = ({
|
||||
<Td>
|
||||
<Tag size="xs">{project.type}</Tag>
|
||||
</Td>
|
||||
<Td>{`${formatRoleName(roles[0].role, roles[0].customRoleName)}${
|
||||
<Td>{`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${
|
||||
roles.length > 1 ? ` (+${roles.length - 1})` : ""
|
||||
}`}</Td>
|
||||
<Td>
|
||||
|
||||
+3
-9
@@ -24,10 +24,10 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetProjectRoles, useUpdateGroupWorkspaceRole } from "@app/hooks/api";
|
||||
import { TGroupMembership } from "@app/hooks/api/groups/types";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
|
||||
import { groupBy } from "@app/lib/fn/array";
|
||||
|
||||
@@ -260,12 +260,6 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
|
||||
}
|
||||
};
|
||||
|
||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
||||
return role;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{roles
|
||||
@@ -275,7 +269,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
|
||||
return (
|
||||
<Tag key={id} className="capitalize">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>{formatRoleName(role, customRoleName)}</div>
|
||||
<div>{formatProjectRoleName(role, customRoleName)}</div>
|
||||
{isTemporary && (
|
||||
<div>
|
||||
<Tooltip content={isExpired ? "Expired Temporary Access" : "Temporary Access"}>
|
||||
@@ -303,7 +297,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
|
||||
return (
|
||||
<Tag key={id} className="capitalize">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>{formatRoleName(role, customRoleName)}</div>
|
||||
<div>{formatProjectRoleName(role, customRoleName)}</div>
|
||||
{isTemporary && (
|
||||
<div>
|
||||
<Tooltip
|
||||
|
||||
@@ -41,11 +41,11 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectIdentityOrderBy } from "@app/hooks/api/workspace/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -53,12 +53,6 @@ import { IdentityModal } from "./components/IdentityModal";
|
||||
|
||||
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||
|
||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
||||
if (role === ProjectMembershipRole.NoAccess) return "No access";
|
||||
return role;
|
||||
};
|
||||
export const IdentityTab = withProjectPermission(
|
||||
() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -286,7 +280,7 @@ export const IdentityTab = withProjectPermission(
|
||||
<Tag key={roleId}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="capitalize">
|
||||
{formatRoleName(role, customRoleName)}
|
||||
{formatProjectRoleName(role, customRoleName)}
|
||||
</div>
|
||||
{isTemporary && (
|
||||
<div>
|
||||
@@ -331,7 +325,9 @@ export const IdentityTab = withProjectPermission(
|
||||
return (
|
||||
<Tag key={roleId} className="capitalize">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>{formatRoleName(role, customRoleName)}</div>
|
||||
<div>
|
||||
{formatProjectRoleName(role, customRoleName)}
|
||||
</div>
|
||||
{isTemporary && (
|
||||
<div>
|
||||
<Tooltip
|
||||
|
||||
+3
-9
@@ -50,19 +50,13 @@ import {
|
||||
useUser,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useGetProjectRoles, useGetWorkspaceUsers } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
||||
if (role === ProjectMembershipRole.NoAccess) return "No access";
|
||||
return role;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
@@ -343,7 +337,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Tag key={id}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="capitalize">
|
||||
{formatRoleName(role, customRoleName)}
|
||||
{formatProjectRoleName(role, customRoleName)}
|
||||
</div>
|
||||
{isTemporary && (
|
||||
<div>
|
||||
@@ -386,7 +380,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
return (
|
||||
<Tag key={id} className="capitalize">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>{formatRoleName(role, customRoleName)}</div>
|
||||
<div>{formatProjectRoleName(role, customRoleName)}</div>
|
||||
{isTemporary && (
|
||||
<div>
|
||||
<Tooltip
|
||||
|
||||
+4
-2
@@ -24,7 +24,7 @@ import {
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
|
||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types";
|
||||
import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal";
|
||||
|
||||
export const ProjectRoleList = () => {
|
||||
@@ -86,7 +86,9 @@ export const ProjectRoleList = () => {
|
||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||
{roles?.map((role) => {
|
||||
const { id, name, slug } = role;
|
||||
const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
|
||||
const isNonMutatable = Object.values(ProjectMembershipRole).includes(
|
||||
slug as ProjectMembershipRole
|
||||
);
|
||||
|
||||
return (
|
||||
<Tr
|
||||
|
||||
+4
-22
@@ -10,10 +10,6 @@ import { AxiosError } from "axios";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -28,7 +24,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useCreateServiceToken, useGetUserWsKey } from "@app/hooks/api";
|
||||
import { useCreateServiceToken } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const apiTokenExpiry = [
|
||||
@@ -97,7 +93,6 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [newToken, setToken] = useState("");
|
||||
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
||||
|
||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?.id ?? "");
|
||||
const createServiceToken = useCreateServiceToken();
|
||||
const hasServiceToken = Boolean(newToken);
|
||||
|
||||
@@ -118,26 +113,13 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
if (!latestFileKey) return;
|
||||
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestFileKey.encryptedKey,
|
||||
nonce: latestFileKey.nonce,
|
||||
publicKey: latestFileKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||
});
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: key,
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
const { serviceToken } = await createServiceToken.mutateAsync({
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
encryptedKey: "",
|
||||
iv: "",
|
||||
tag: "",
|
||||
scopes,
|
||||
expiresIn: Number(expiresIn),
|
||||
name,
|
||||
|
||||
+20
-63
@@ -18,10 +18,6 @@ import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
@@ -37,6 +33,7 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateIdentityProjectAdditionalPrivilege,
|
||||
useGetIdentityProjectPrivilegeDetails,
|
||||
@@ -45,9 +42,9 @@ import {
|
||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types";
|
||||
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
|
||||
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
isConditionalSubjects,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
projectRoleFormSchema,
|
||||
rolePermission2Form
|
||||
@@ -100,6 +97,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
|
||||
|
||||
const form = useForm<TFormSchema>({
|
||||
values: privilegeDetails
|
||||
@@ -194,30 +192,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
}
|
||||
}
|
||||
|
||||
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
|
||||
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
|
||||
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
|
||||
form.setValue(
|
||||
`permissions.${selectedSubject}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[...rootPolicyValue, {}],
|
||||
{ shouldDirty: true, shouldTouch: true }
|
||||
);
|
||||
} else {
|
||||
form.setValue(
|
||||
`permissions.${selectedSubject}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[{}],
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
@@ -249,46 +223,25 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
|
||||
className={twMerge(
|
||||
"h-10 rounded-r-none border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
New policy
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
|
||||
{Object.keys(PROJECT_PERMISSION_OBJECT)
|
||||
.sort((a, b) =>
|
||||
PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
PROJECT_PERMISSION_OBJECT[
|
||||
b as keyof typeof PROJECT_PERMISSION_OBJECT
|
||||
].title.toLowerCase()
|
||||
)
|
||||
)
|
||||
.map((permissionSubject) => (
|
||||
<DropdownMenuItem
|
||||
key={`permission-create-${permissionSubject}`}
|
||||
className="py-3"
|
||||
onClick={() => onNewPolicy(permissionSubject as ProjectPermissionSub)}
|
||||
>
|
||||
{PROJECT_PERMISSION_OBJECT[permissionSubject as ProjectPermissionSub].title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpToggle("addPolicy")}
|
||||
>
|
||||
Add Policies
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -429,6 +382,10 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<PolicySelectionModal
|
||||
isOpen={popUp.addPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||
/>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
|
||||
+21
-63
@@ -17,10 +17,6 @@ import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
@@ -36,6 +32,7 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateProjectUserAdditionalPrivilege,
|
||||
useGetProjectUserPrivilegeDetails,
|
||||
@@ -44,9 +41,9 @@ import {
|
||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types";
|
||||
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
|
||||
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
isConditionalSubjects,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
projectRoleFormSchema,
|
||||
rolePermission2Form
|
||||
@@ -86,6 +83,8 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
projectMembershipId,
|
||||
isDisabled
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
|
||||
|
||||
const isCreate = !privilegeId;
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
@@ -166,30 +165,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
|
||||
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
|
||||
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
|
||||
form.setValue(
|
||||
`permissions.${selectedSubject}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[...rootPolicyValue, {}],
|
||||
{ shouldDirty: true, shouldTouch: true }
|
||||
);
|
||||
} else {
|
||||
form.setValue(
|
||||
`permissions.${selectedSubject}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[{}],
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const privilegeTemporaryAccess = form.watch("temporaryAccess");
|
||||
const isTemporary = privilegeTemporaryAccess?.isTemporary;
|
||||
const isExpired =
|
||||
@@ -245,46 +220,25 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
|
||||
className={twMerge(
|
||||
"h-10 rounded-r-none border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
New policy
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
|
||||
{Object.keys(PROJECT_PERMISSION_OBJECT)
|
||||
.sort((a, b) =>
|
||||
PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
PROJECT_PERMISSION_OBJECT[
|
||||
b as keyof typeof PROJECT_PERMISSION_OBJECT
|
||||
].title.toLowerCase()
|
||||
)
|
||||
)
|
||||
.map((subject) => (
|
||||
<DropdownMenuItem
|
||||
key={`permission-create-${subject}`}
|
||||
className="py-3"
|
||||
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
|
||||
>
|
||||
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpToggle("addPolicy")}
|
||||
>
|
||||
Add Policies
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -423,6 +377,10 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
</div>
|
||||
<PolicySelectionModal
|
||||
isOpen={popUp.addPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||
/>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
@@ -77,7 +78,9 @@ const Page = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? "");
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
|
||||
(data?.slug ?? "") as ProjectMembershipRole
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
ModalClose,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
|
||||
import {
|
||||
isConditionalSubjects,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
projectRoleFormSchema,
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const NewPermissionRule = ({ onClose }: Props) => {
|
||||
const rootForm = useFormContext<TFormSchema>();
|
||||
|
||||
const form = useForm<{
|
||||
type: ProjectPermissionSub;
|
||||
permissions: NonNullable<TFormSchema["permissions"]>;
|
||||
}>({
|
||||
resolver: zodResolver(
|
||||
projectRoleFormSchema
|
||||
.pick({ permissions: true })
|
||||
.extend({ type: z.nativeEnum(ProjectPermissionSub) })
|
||||
),
|
||||
defaultValues: {
|
||||
type: ProjectPermissionSub.Secrets
|
||||
}
|
||||
});
|
||||
|
||||
const selectedSubject = form.watch("type");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="type"
|
||||
defaultValue={ProjectPermissionSub.Secrets}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Subject" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{Object.keys(PROJECT_PERMISSION_OBJECT).map((subject) => (
|
||||
<SelectItem value={subject} key={`permission-create-${subject}`}>
|
||||
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel label="Actions" className="my-2" />
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||
{PROJECT_PERMISSION_OBJECT?.[selectedSubject]?.actions?.map(({ label, value }) => (
|
||||
<Controller
|
||||
key={`create-permission-${selectedSubject}-${label}`}
|
||||
name={`permissions.${selectedSubject}.0.${value as any}` as any}
|
||||
control={form.control}
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`new-permissions.${selectedSubject}.0.${String(value)}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Button
|
||||
onClick={form.handleSubmit((el) => {
|
||||
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
|
||||
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
|
||||
rootForm.setValue(
|
||||
`permissions.${el.type}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[...rootPolicyValue, ...(el?.permissions[el.type] || [])],
|
||||
{ shouldDirty: true, shouldTouch: true }
|
||||
);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
rootForm.setValue(`permissions.${el.type}`, el?.permissions?.[el.type], {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
})}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||
import { faCheck, faSearch, faXmark, faXmarkCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { useGetProjectTypeFromRoute } from "@app/hooks";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import {
|
||||
isConditionalSubjects,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
ProjectTypePermissionSubjects,
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type TForm = { permissions: Record<ProjectPermissionSub, boolean> };
|
||||
|
||||
const Content = ({ onClose }: ContentProps) => {
|
||||
const rootForm = useFormContext<TFormSchema>();
|
||||
const [search, setSearch] = useState("");
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isDirty },
|
||||
setValue,
|
||||
reset
|
||||
} = useForm<TForm>({
|
||||
defaultValues: {
|
||||
permissions: Object.fromEntries(
|
||||
Object.values(ProjectPermissionSub).map((subject) => [subject, false])
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
const projectType = useGetProjectTypeFromRoute();
|
||||
|
||||
const filteredPolicies = Object.entries(PROJECT_PERMISSION_OBJECT)
|
||||
.filter(
|
||||
([subject, { title }]) =>
|
||||
ProjectTypePermissionSubjects[projectType ?? ProjectType.SecretManager][
|
||||
subject as ProjectPermissionSub
|
||||
] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true)
|
||||
)
|
||||
.sort((a, b) => a[1].title.localeCompare(b[1].title))
|
||||
.map(([subject]) => subject);
|
||||
|
||||
const onSubmit = () =>
|
||||
handleSubmit((form) => {
|
||||
Object.entries(form.permissions).forEach(([subject, add]) => {
|
||||
if (!add) return;
|
||||
|
||||
const type = subject as ProjectPermissionSub;
|
||||
|
||||
const rootPolicyValue = rootForm.getValues("permissions")?.[type];
|
||||
|
||||
if (rootPolicyValue && isConditionalSubjects(subject as ProjectPermissionSub)) {
|
||||
rootForm.setValue(
|
||||
`permissions.${type}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[...rootPolicyValue, {}],
|
||||
{ shouldDirty: true, shouldTouch: true }
|
||||
);
|
||||
} else if (!rootPolicyValue?.length) {
|
||||
rootForm.setValue(
|
||||
`permissions.${type}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[{}],
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
placeholder="Search policies..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||
rightIcon={
|
||||
search ? (
|
||||
<IconButton ariaLabel="Clear search" variant="plain" onClick={() => setSearch("")}>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<TableContainer className="thin-scrollbar mt-4 max-h-[28rem]">
|
||||
<div className="sticky top-0 z-30 flex justify-between border-b border-b-mineshaft-600 bg-mineshaft-800 py-3 pl-5 pr-4 font-inter text-sm font-medium text-bunker-300">
|
||||
<span>Resource</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="plain"
|
||||
className="p-0 text-mineshaft-400"
|
||||
size="xs"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
setValue(
|
||||
"permissions",
|
||||
Object.fromEntries(
|
||||
filteredPolicies.map((subject) => [subject, true])
|
||||
) as TForm["permissions"],
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
>
|
||||
Select All
|
||||
</Button>
|
||||
<Tooltip content="Clear selection">
|
||||
<IconButton
|
||||
ariaLabel="Clear selection"
|
||||
onClick={() => reset()}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
className={`text-mineshaft-400 ${!isDirty ? "pointer-events-none opacity-50" : ""} hover:text-red`}
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmarkCircle} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TBody>
|
||||
{filteredPolicies.map((subject) => (
|
||||
<Controller
|
||||
control={control}
|
||||
key={`permission-add-${subject}`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Tr
|
||||
className={`${value ? "bg-mineshaft-600/30" : ""} cursor-pointer hover:bg-mineshaft-700`}
|
||||
onClick={() => onChange(!value)}
|
||||
>
|
||||
<Td className={`${value ? "text-mineshaft-100" : "text-mineshaft-300"} w-full`}>
|
||||
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||
</Td>
|
||||
<Td>
|
||||
{value ? <FontAwesomeIcon className="text-green" icon={faCheck} /> : null}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
name={`permissions.${subject as ProjectPermissionSub}`}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!filteredPolicies.length && (
|
||||
<EmptyState
|
||||
iconSize="2x"
|
||||
icon={faSearch}
|
||||
className="!pb-4 !pt-8"
|
||||
title="No policies match search"
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Button isDisabled={!isDirty} onClick={onSubmit}>
|
||||
Add Policies
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PolicySelectionModal = ({ isOpen, onOpenChange }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Add Policies"
|
||||
subTitle="Select one or more policies to add to this role."
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<Content onClose={() => onOpenChange(false)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+87
-2
@@ -25,6 +25,7 @@ import {
|
||||
TPermissionConditionOperators
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { TProjectPermission } from "@app/hooks/api/roles/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const GeneralPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
@@ -912,7 +913,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
actions: [{ label: "Modify", value: "edit" }]
|
||||
},
|
||||
[ProjectPermissionSub.Integrations]: {
|
||||
title: "Integrations",
|
||||
title: "Native Integrations",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
@@ -1122,7 +1123,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SecretApproval]: {
|
||||
title: "Secret Protect policy",
|
||||
title: "Secret Approval Policies",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
@@ -1195,3 +1196,87 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const SharedPermissionSubjects = {
|
||||
[ProjectPermissionSub.AuditLogs]: true,
|
||||
[ProjectPermissionSub.Groups]: true,
|
||||
[ProjectPermissionSub.Member]: true,
|
||||
[ProjectPermissionSub.Identity]: true,
|
||||
[ProjectPermissionSub.Project]: true,
|
||||
[ProjectPermissionSub.Role]: true,
|
||||
[ProjectPermissionSub.Settings]: true
|
||||
};
|
||||
|
||||
const SecretsManagerPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.SecretFolders]: enabled,
|
||||
[ProjectPermissionSub.SecretImports]: enabled,
|
||||
[ProjectPermissionSub.DynamicSecrets]: enabled,
|
||||
[ProjectPermissionSub.Secrets]: enabled,
|
||||
[ProjectPermissionSub.SecretApproval]: enabled,
|
||||
[ProjectPermissionSub.Integrations]: enabled,
|
||||
[ProjectPermissionSub.SecretSyncs]: enabled,
|
||||
[ProjectPermissionSub.Kms]: enabled,
|
||||
[ProjectPermissionSub.Environments]: enabled,
|
||||
[ProjectPermissionSub.Tags]: enabled,
|
||||
[ProjectPermissionSub.Webhooks]: enabled,
|
||||
[ProjectPermissionSub.IpAllowList]: enabled,
|
||||
[ProjectPermissionSub.SecretRollback]: enabled,
|
||||
[ProjectPermissionSub.SecretRotation]: enabled,
|
||||
[ProjectPermissionSub.ServiceTokens]: enabled
|
||||
});
|
||||
|
||||
const KmsPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.Cmek]: enabled,
|
||||
[ProjectPermissionSub.Kmip]: enabled
|
||||
});
|
||||
|
||||
const CertificateManagerPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.PkiCollections]: enabled,
|
||||
[ProjectPermissionSub.PkiAlerts]: enabled,
|
||||
[ProjectPermissionSub.CertificateAuthorities]: enabled,
|
||||
[ProjectPermissionSub.CertificateTemplates]: enabled,
|
||||
[ProjectPermissionSub.Certificates]: enabled
|
||||
});
|
||||
|
||||
const SshPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.SshCertificateAuthorities]: enabled,
|
||||
[ProjectPermissionSub.SshCertificates]: enabled,
|
||||
[ProjectPermissionSub.SshCertificateTemplates]: enabled,
|
||||
[ProjectPermissionSub.SshHosts]: enabled,
|
||||
[ProjectPermissionSub.SshHostGroups]: enabled
|
||||
});
|
||||
|
||||
// scott: this structure ensures we don't forget to add project permissions to their relevant project type
|
||||
export const ProjectTypePermissionSubjects: Record<
|
||||
ProjectType,
|
||||
Record<ProjectPermissionSub, boolean>
|
||||
> = {
|
||||
[ProjectType.SecretManager]: {
|
||||
...SharedPermissionSubjects,
|
||||
...SecretsManagerPermissionSubjects(true),
|
||||
...KmsPermissionSubjects(),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects()
|
||||
},
|
||||
[ProjectType.KMS]: {
|
||||
...SharedPermissionSubjects,
|
||||
...KmsPermissionSubjects(true),
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects()
|
||||
},
|
||||
[ProjectType.CertificateManager]: {
|
||||
...SharedPermissionSubjects,
|
||||
...CertificateManagerPermissionSubjects(true),
|
||||
...KmsPermissionSubjects(),
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects()
|
||||
},
|
||||
[ProjectType.SSH]: {
|
||||
...SharedPermissionSubjects,
|
||||
...SshPermissionSubjects(true),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...KmsPermissionSubjects(),
|
||||
...SecretsManagerPermissionSubjects()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useGetProjectRoleBySlug } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -21,7 +22,9 @@ export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data } = useGetProjectRoleBySlug(currentWorkspace?.id ?? "", roleSlug as string);
|
||||
|
||||
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? "");
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
|
||||
(data?.slug ?? "") as ProjectMembershipRole
|
||||
);
|
||||
|
||||
return data ? (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
|
||||
@@ -115,12 +115,8 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text =
|
||||
error?.response?.data?.message ??
|
||||
`Failed to ${popUp?.role?.data ? "update" : "create"} role`;
|
||||
} catch {
|
||||
const text = `Failed to ${popUp?.role?.data ? "update" : "create"} role`;
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
|
||||
+40
-79
@@ -8,18 +8,15 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { AccessTree } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
import { evaluatePermissionsAbility } from "@app/helpers/permissions";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
|
||||
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
|
||||
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
||||
@@ -31,6 +28,7 @@ import {
|
||||
isConditionalSubjects,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
projectRoleFormSchema,
|
||||
ProjectTypePermissionSubjects,
|
||||
rolePermission2Form,
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
@@ -88,6 +86,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
|
||||
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
||||
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
|
||||
|
||||
const onSubmit = async (el: TFormSchema) => {
|
||||
try {
|
||||
if (!projectId || !role?.id) return;
|
||||
@@ -104,31 +104,9 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(role?.slug ?? "");
|
||||
|
||||
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
|
||||
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
|
||||
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
|
||||
form.setValue(
|
||||
`permissions.${selectedSubject}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[...rootPolicyValue, {}],
|
||||
{ shouldDirty: true, shouldTouch: true }
|
||||
);
|
||||
} else {
|
||||
form.setValue(
|
||||
`permissions.${selectedSubject}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[{}],
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
|
||||
(role?.slug ?? "") as ProjectMembershipRole
|
||||
);
|
||||
|
||||
const permissions = form.watch("permissions");
|
||||
|
||||
@@ -172,48 +150,25 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
|
||||
className={twMerge(
|
||||
"h-10 rounded-r-none border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
New policy
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
|
||||
{Object.keys(PROJECT_PERMISSION_OBJECT)
|
||||
.sort((a, b) =>
|
||||
PROJECT_PERMISSION_OBJECT[
|
||||
a as keyof typeof PROJECT_PERMISSION_OBJECT
|
||||
].title
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
PROJECT_PERMISSION_OBJECT[
|
||||
b as keyof typeof PROJECT_PERMISSION_OBJECT
|
||||
].title.toLowerCase()
|
||||
)
|
||||
)
|
||||
.map((subject) => (
|
||||
<DropdownMenuItem
|
||||
key={`permission-create-${subject}`}
|
||||
className="py-3"
|
||||
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
|
||||
>
|
||||
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpToggle("addPolicy")}
|
||||
>
|
||||
Add Policy
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -221,18 +176,24 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
</div>
|
||||
<div className="py-4">
|
||||
{!isPending && <PermissionEmptyState />}
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[])
|
||||
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject])
|
||||
.map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
</div>
|
||||
<PolicySelectionModal
|
||||
isOpen={popUp.addPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||
/>
|
||||
</FormProvider>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props)
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} />
|
||||
<Input {...field} onChange={(e) => field.onChange(e.target.value.trim())} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -251,7 +251,7 @@ export const AccessPolicyForm = ({
|
||||
label="Policy Type"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
tooltipText="Change polices govern secret changes within a given environment and secret path. Access polices allow underprivileged user to request access to environment/secret path."
|
||||
tooltipText="Change policies govern secret changes within a given environment and secret path. Access policies allow underprivileged user to request access to environment/secret path."
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
|
||||
+2
-2
@@ -163,8 +163,8 @@ export const CreateSecretImportForm = ({
|
||||
onValueChange={(val) => onChange(val === "true")}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
<SelectItem value="false">Ignore secret approval polices</SelectItem>
|
||||
<SelectItem value="true">Respect secret approval polices</SelectItem>
|
||||
<SelectItem value="false">Ignore secret approval policies</SelectItem>
|
||||
<SelectItem value="true">Respect secret approval policies</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
useCreateSshHostGroup,
|
||||
useGetSshHostGroupById,
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceGroups,
|
||||
useListWorkspaceSshHostGroups,
|
||||
useUpdateSshHostGroup
|
||||
} from "@app/hooks/api";
|
||||
@@ -38,7 +39,14 @@ const schema = z
|
||||
loginMappings: z
|
||||
.object({
|
||||
loginUser: z.string().trim().min(1),
|
||||
allowedPrincipals: z.array(z.string().trim()).default([])
|
||||
allowedPrincipals: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(["user", "group"]),
|
||||
value: z.string().trim().min(1)
|
||||
})
|
||||
)
|
||||
.default([])
|
||||
})
|
||||
.array()
|
||||
.default([])
|
||||
@@ -49,9 +57,10 @@ export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
const { data: sshHostGroups } = useListWorkspaceSshHostGroups(currentWorkspace.id);
|
||||
const projectId = currentWorkspace.id;
|
||||
const { data: sshHostGroups } = useListWorkspaceSshHostGroups(projectId);
|
||||
const { data: members = [] } = useGetWorkspaceUsers(projectId);
|
||||
const { data: groups = [] } = useListWorkspaceGroups(projectId);
|
||||
const [expandedMappings, setExpandedMappings] = useState<Record<number, boolean>>({});
|
||||
|
||||
const { data: sshHostGroup } = useGetSshHostGroupById(
|
||||
@@ -87,7 +96,16 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
name: sshHostGroup.name,
|
||||
loginMappings: sshHostGroup.loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: allowedPrincipals.usernames
|
||||
allowedPrincipals: [
|
||||
...(allowedPrincipals.usernames || []).map((username) => ({
|
||||
type: "user" as const,
|
||||
value: username
|
||||
})),
|
||||
...(allowedPrincipals.groups || []).map((group) => ({
|
||||
type: "group" as const,
|
||||
value: group
|
||||
}))
|
||||
]
|
||||
}))
|
||||
});
|
||||
|
||||
@@ -118,27 +136,35 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformedLoginMappings = loginMappings.map(({ loginUser, allowedPrincipals }) => {
|
||||
const usernames = allowedPrincipals
|
||||
.filter((p) => p.type === "user" && p.value)
|
||||
.map((p) => p.value);
|
||||
|
||||
const groupNames = allowedPrincipals
|
||||
.filter((p) => p.type === "group" && p.value)
|
||||
.map((p) => p.value);
|
||||
|
||||
return {
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames,
|
||||
groups: groupNames
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (sshHostGroup) {
|
||||
await updateMutateAsync({
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
name,
|
||||
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: allowedPrincipals
|
||||
}
|
||||
}))
|
||||
loginMappings: transformedLoginMappings
|
||||
});
|
||||
} else {
|
||||
await createMutateAsync({
|
||||
projectId,
|
||||
name,
|
||||
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: allowedPrincipals
|
||||
}
|
||||
}))
|
||||
loginMappings: transformedLoginMappings
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,6 +191,15 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const isPrincipalDuplicate = (
|
||||
mappingIndex: number,
|
||||
principalType: string,
|
||||
principalValue: string
|
||||
) => {
|
||||
const principals = getValues(`loginMappings.${mappingIndex}.allowedPrincipals`) || [];
|
||||
return principals.some((p) => p.type === principalType && p.value === principalValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.sshHostGroup?.isOpen}
|
||||
@@ -203,7 +238,10 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const newIndex = loginMappingsFormFields.fields.length;
|
||||
loginMappingsFormFields.append({ loginUser: "", allowedPrincipals: [""] });
|
||||
loginMappingsFormFields.append({
|
||||
loginUser: "",
|
||||
allowedPrincipals: [{ type: "user", value: "" }]
|
||||
});
|
||||
setExpandedMappings((prev) => ({
|
||||
...prev,
|
||||
[newIndex]: true
|
||||
@@ -299,7 +337,10 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const current = getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
|
||||
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
|
||||
setValue(`loginMappings.${i}.allowedPrincipals`, [
|
||||
...current,
|
||||
{ type: "user", value: "" }
|
||||
]);
|
||||
}}
|
||||
>
|
||||
Add Principal
|
||||
@@ -310,40 +351,69 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
name={`loginMappings.${i}.allowedPrincipals`}
|
||||
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
|
||||
<div className="flex flex-col space-y-2">
|
||||
{(value.length === 0 ? [""] : value).map(
|
||||
(principal: string, principalIndex: number) => (
|
||||
<div
|
||||
key={`${metadataFieldId}-principal-${principal}`}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={principal}
|
||||
onValueChange={(newValue) => {
|
||||
if (value.includes(newValue)) {
|
||||
createNotification({
|
||||
text: "This principal is already added",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newPrincipals = [...value];
|
||||
newPrincipals[principalIndex] = newValue;
|
||||
onChange(newPrincipals);
|
||||
}}
|
||||
placeholder="Select a member"
|
||||
className="w-full"
|
||||
>
|
||||
{members.map((member) => (
|
||||
<SelectItem
|
||||
key={member.user.id}
|
||||
value={member.user.username}
|
||||
>
|
||||
{member.user.username}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{value.map((principal, principalIndex) => (
|
||||
<div
|
||||
key={`principal-${i + 1}-${principalIndex + 1}-${principal.type}`}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<div className="mr-2">
|
||||
<Select
|
||||
className="w-24"
|
||||
value={principal.type}
|
||||
onValueChange={(newType) => {
|
||||
const newPrincipals = [...value];
|
||||
newPrincipals[principalIndex] = {
|
||||
type: newType as "user" | "group",
|
||||
value: ""
|
||||
};
|
||||
onChange(newPrincipals);
|
||||
}}
|
||||
>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="group">Group</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={principal.value}
|
||||
onValueChange={(newValue) => {
|
||||
if (isPrincipalDuplicate(i, principal.type, newValue)) {
|
||||
createNotification({
|
||||
text: `This ${principal.type} is already added`,
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newPrincipals = [...value];
|
||||
newPrincipals[principalIndex] = {
|
||||
type: principal.type as "user" | "group",
|
||||
value: newValue
|
||||
};
|
||||
onChange(newPrincipals);
|
||||
}}
|
||||
placeholder={`Select a ${principal.type}`}
|
||||
className="w-full"
|
||||
>
|
||||
{principal.type === "user"
|
||||
? members.map((member) => (
|
||||
<SelectItem
|
||||
key={member.user.id}
|
||||
value={member.user.username}
|
||||
>
|
||||
{member.user.username}
|
||||
</SelectItem>
|
||||
))
|
||||
: groups.map((group) => (
|
||||
<SelectItem
|
||||
key={group.group.slug}
|
||||
value={group.group.slug}
|
||||
>
|
||||
{group.group.slug}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex w-10 justify-center">
|
||||
<IconButton
|
||||
size="sm"
|
||||
ariaLabel="delete principal"
|
||||
@@ -353,14 +423,14 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const newPrincipals = value.filter(
|
||||
(_, idx) => idx !== principalIndex
|
||||
);
|
||||
onChange(newPrincipals);
|
||||
onChange(newPrincipals.length ? newPrincipals : []);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{error && <span className="text-sm text-red-500">{error.message}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
@@ -56,22 +56,38 @@ export const SshHostGroupsSection = () => {
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Host Groups</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="button"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddSshHostGroupModal()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Group
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<div className="flex justify-end">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/ssh/host-groups"
|
||||
>
|
||||
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||
Documentation{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.06rem] ml-1 text-xs"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SshHostGroups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="button"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddSshHostGroupModal()}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-4"
|
||||
>
|
||||
Add Group
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<SshHostGroupsTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { faEllipsis, faPencil, faServer, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faEllipsis,
|
||||
faPencil,
|
||||
faServer,
|
||||
faTrash,
|
||||
faUser,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -77,9 +85,40 @@ export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
group.loginMappings.map(({ loginUser, allowedPrincipals }) => (
|
||||
<div key={`${group.id}-${loginUser}`} className="mb-2">
|
||||
<div className="text-mineshaft-200">{loginUser}</div>
|
||||
{allowedPrincipals.usernames.map((username) => (
|
||||
<div key={`${group.id}-${loginUser}-${username}`} className="ml-4">
|
||||
└─ {username}
|
||||
{allowedPrincipals.usernames?.map((username) => (
|
||||
<div
|
||||
key={`${loginUser}-${username}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="text-gray-400">└─</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<FontAwesomeIcon
|
||||
icon={faUser}
|
||||
className="text-xs text-yellow/80"
|
||||
/>
|
||||
<span>{username}</span>
|
||||
<Badge variant="primary">user</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{allowedPrincipals.groups?.map((allowedGroup) => (
|
||||
<div
|
||||
key={`${loginUser}-${allowedGroup}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="text-gray-400">└─</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<FontAwesomeIcon
|
||||
icon={faUsers}
|
||||
className="text-xs text-green/80"
|
||||
/>
|
||||
<span>{allowedGroup}</span>
|
||||
<Badge variant="success">group</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
useCreateSshHost,
|
||||
useGetSshHostById,
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceGroups,
|
||||
useListWorkspaceSshHosts,
|
||||
useUpdateSshHost
|
||||
} from "@app/hooks/api";
|
||||
@@ -49,7 +50,14 @@ const schema = z
|
||||
loginMappings: z
|
||||
.object({
|
||||
loginUser: z.string().trim().min(1),
|
||||
allowedPrincipals: z.array(z.string().trim()).default([]),
|
||||
allowedPrincipals: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(["user", "group"]),
|
||||
value: z.string().trim().min(1)
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
source: z.nativeEnum(LoginMappingSource)
|
||||
})
|
||||
.array()
|
||||
@@ -64,6 +72,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
const { data: sshHosts } = useListWorkspaceSshHosts(currentWorkspace.id);
|
||||
const { data: members = [] } = useGetWorkspaceUsers(projectId);
|
||||
const { data: groups = [] } = useListWorkspaceGroups(projectId);
|
||||
const [expandedMappings, setExpandedMappings] = useState<Record<number, boolean>>({});
|
||||
|
||||
const { data: sshHost } = useGetSshHostById(
|
||||
@@ -103,7 +112,16 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
userCertTtl: sshHost.userCertTtl,
|
||||
loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals, source }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: allowedPrincipals.usernames,
|
||||
allowedPrincipals: [
|
||||
...(allowedPrincipals.usernames || []).map((username) => ({
|
||||
type: "user" as const,
|
||||
value: username
|
||||
})),
|
||||
...(allowedPrincipals.groups || []).map((group) => ({
|
||||
type: "group" as const,
|
||||
value: group
|
||||
}))
|
||||
],
|
||||
source
|
||||
}))
|
||||
});
|
||||
@@ -159,18 +177,31 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const transformedLoginMappings = hostLoginMappings.map(({ loginUser, allowedPrincipals }) => {
|
||||
const usernames = allowedPrincipals
|
||||
.filter((p) => p.type === "user" && p.value)
|
||||
.map((p) => p.value);
|
||||
|
||||
const groupNames = allowedPrincipals
|
||||
.filter((p) => p.type === "group" && p.value)
|
||||
.map((p) => p.value);
|
||||
|
||||
return {
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames,
|
||||
groups: groupNames
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (sshHost) {
|
||||
await updateMutateAsync({
|
||||
sshHostId: sshHost.id,
|
||||
hostname,
|
||||
alias: trimmedAlias,
|
||||
userCertTtl,
|
||||
loginMappings: hostLoginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: allowedPrincipals
|
||||
}
|
||||
}))
|
||||
loginMappings: transformedLoginMappings
|
||||
});
|
||||
} else {
|
||||
await createMutateAsync({
|
||||
@@ -178,12 +209,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
hostname,
|
||||
alias: trimmedAlias,
|
||||
userCertTtl,
|
||||
loginMappings: hostLoginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: allowedPrincipals
|
||||
}
|
||||
}))
|
||||
loginMappings: transformedLoginMappings
|
||||
});
|
||||
}
|
||||
|
||||
@@ -210,6 +236,15 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const isPrincipalDuplicate = (
|
||||
mappingIndex: number,
|
||||
principalType: string,
|
||||
principalValue: string
|
||||
) => {
|
||||
const principals = getValues(`loginMappings.${mappingIndex}.allowedPrincipals`) || [];
|
||||
return principals.some((p) => p.type === principalType && p.value === principalValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.sshHost?.isOpen}
|
||||
@@ -275,7 +310,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const newIndex = loginMappingsFormFields.fields.length;
|
||||
loginMappingsFormFields.append({
|
||||
loginUser: "",
|
||||
allowedPrincipals: [""],
|
||||
allowedPrincipals: [],
|
||||
source: LoginMappingSource.HOST
|
||||
});
|
||||
setExpandedMappings((prev) => ({
|
||||
@@ -397,7 +432,10 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
onClick={() => {
|
||||
const current =
|
||||
getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
|
||||
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
|
||||
setValue(`loginMappings.${i}.allowedPrincipals`, [
|
||||
...current,
|
||||
{ type: "user", value: "" }
|
||||
]);
|
||||
}}
|
||||
>
|
||||
Add Principal
|
||||
@@ -409,50 +447,82 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
name={`loginMappings.${i}.allowedPrincipals`}
|
||||
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
|
||||
<div className="flex flex-col space-y-2">
|
||||
{(value.length === 0 ? [""] : value).map(
|
||||
(principal: string, principalIndex: number) => (
|
||||
<div
|
||||
key={`${metadataFieldId}-principal-${principal}`}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={principal}
|
||||
onValueChange={(newValue) => {
|
||||
if (
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP
|
||||
)
|
||||
return;
|
||||
|
||||
if (value.includes(newValue)) {
|
||||
createNotification({
|
||||
text: "This principal is already added",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newPrincipals = [...value];
|
||||
newPrincipals[principalIndex] = newValue;
|
||||
onChange(newPrincipals);
|
||||
}}
|
||||
placeholder="Select a member"
|
||||
className="w-full"
|
||||
isDisabled={
|
||||
{value.map((principal, principalIndex) => (
|
||||
<div
|
||||
key={`principal-${i + 1}-${principalIndex + 1}-${principal.type}`}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<div className="mr-2">
|
||||
<Select
|
||||
className="w-24"
|
||||
value={principal.type}
|
||||
onValueChange={(newType) => {
|
||||
const newPrincipals = [...value];
|
||||
newPrincipals[principalIndex] = {
|
||||
type: newType as "user" | "group",
|
||||
value: ""
|
||||
};
|
||||
onChange(newPrincipals);
|
||||
}}
|
||||
isDisabled={
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP
|
||||
}
|
||||
>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="group">Group</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={principal.value}
|
||||
onValueChange={(newValue) => {
|
||||
if (
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP
|
||||
)
|
||||
return;
|
||||
if (isPrincipalDuplicate(i, principal.type, newValue)) {
|
||||
createNotification({
|
||||
text: `This ${principal.type} is already added`,
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
>
|
||||
{members.map((member) => (
|
||||
<SelectItem
|
||||
key={member.user.id}
|
||||
value={member.user.username}
|
||||
>
|
||||
{member.user.username}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
const newPrincipals = [...value];
|
||||
newPrincipals[principalIndex] = {
|
||||
type: principal.type,
|
||||
value: newValue
|
||||
};
|
||||
onChange(newPrincipals);
|
||||
}}
|
||||
placeholder={`Select a ${principal.type}`}
|
||||
className="w-full"
|
||||
isDisabled={
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
LoginMappingSource.HOST_GROUP
|
||||
}
|
||||
>
|
||||
{principal.type === "user"
|
||||
? members.map((member) => (
|
||||
<SelectItem
|
||||
key={member.user.id}
|
||||
value={member.user.username}
|
||||
>
|
||||
{member.user.username}
|
||||
</SelectItem>
|
||||
))
|
||||
: groups.map((group) => (
|
||||
<SelectItem
|
||||
key={group.group.slug}
|
||||
value={group.group.slug}
|
||||
>
|
||||
{group.group.slug}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex w-10 justify-center">
|
||||
<IconButton
|
||||
size="sm"
|
||||
ariaLabel="delete principal"
|
||||
@@ -468,7 +538,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const newPrincipals = value.filter(
|
||||
(_, idx) => idx !== principalIndex
|
||||
);
|
||||
onChange(newPrincipals);
|
||||
onChange([...newPrincipals]);
|
||||
}}
|
||||
isDisabled={
|
||||
loginMappingsFormFields.fields[i].source ===
|
||||
@@ -478,8 +548,8 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{error && <span className="text-sm text-red-500">{error.message}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const SshHostsSection = () => {
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Hosts</p>
|
||||
<div className="flex w-full justify-end">
|
||||
<div className="flex justify-end">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
faEllipsis,
|
||||
faPencil,
|
||||
faServer,
|
||||
faTrash
|
||||
faTrash,
|
||||
faUser,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import FileSaver from "file-saver";
|
||||
@@ -12,6 +14,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -101,43 +104,53 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
||||
|
||||
const hostLoginUserToPrincipals = hostMappings.reduce(
|
||||
(acc, { loginUser, allowedPrincipals }) => {
|
||||
acc[loginUser] = new Set(allowedPrincipals.usernames);
|
||||
acc[loginUser] = {
|
||||
users: new Set(allowedPrincipals.usernames),
|
||||
groups: new Set(allowedPrincipals.groups)
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Set<string>>
|
||||
{} as Record<string, { users: Set<string>; groups: Set<string> }>
|
||||
);
|
||||
|
||||
const entriesFromHost = hostMappings.map(
|
||||
({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
source: LoginMappingSource.HOST,
|
||||
usernames: allowedPrincipals.usernames
|
||||
users: allowedPrincipals.usernames,
|
||||
groups: allowedPrincipals.groups
|
||||
})
|
||||
);
|
||||
|
||||
const entriesFromGroup = groupMappings
|
||||
.map(({ loginUser, allowedPrincipals }) => {
|
||||
const existing = hostLoginUserToPrincipals[loginUser] || new Set();
|
||||
const filteredUsernames = allowedPrincipals.usernames.filter(
|
||||
(u) => !existing.has(u)
|
||||
const existing = hostLoginUserToPrincipals[loginUser] || {};
|
||||
const filteredUsernames = allowedPrincipals.usernames?.filter(
|
||||
(u) => !existing.users?.has(u)
|
||||
);
|
||||
return filteredUsernames.length > 0
|
||||
const filteredGroups = allowedPrincipals.groups?.filter(
|
||||
(g) => !existing.groups?.has(g)
|
||||
);
|
||||
return ((filteredGroups?.length || filteredUsernames?.length) ?? 0) >
|
||||
0
|
||||
? {
|
||||
loginUser,
|
||||
source: LoginMappingSource.HOST_GROUP,
|
||||
usernames: filteredUsernames
|
||||
users: filteredUsernames,
|
||||
groups: filteredGroups
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter(Boolean) as {
|
||||
loginUser: string;
|
||||
source: LoginMappingSource;
|
||||
usernames: string[];
|
||||
users: string[];
|
||||
groups: string[];
|
||||
}[];
|
||||
|
||||
return [...entriesFromHost, ...entriesFromGroup]
|
||||
.sort((a, b) => a.loginUser.localeCompare(b.loginUser))
|
||||
.map(({ loginUser, usernames, source }) => (
|
||||
.map(({ loginUser, users, groups, source }) => (
|
||||
<div key={`${host.id}-${loginUser}-${source}`} className="mb-2">
|
||||
<div className="text-mineshaft-200">
|
||||
{loginUser}
|
||||
@@ -147,12 +160,40 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{usernames.map((username) => (
|
||||
{users?.map((username) => (
|
||||
<div
|
||||
key={`${host.id}-${loginUser}-${source}-${username}`}
|
||||
className="ml-4"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
└─ {username}
|
||||
<div className="flex items-center">
|
||||
<span className="text-gray-400">└─</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<FontAwesomeIcon
|
||||
icon={faUser}
|
||||
className="text-xs text-yellow/80"
|
||||
/>
|
||||
<span>{username}</span>
|
||||
<Badge variant="primary">user</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{groups?.map((group) => (
|
||||
<div
|
||||
key={`${host.id}-${loginUser}-${source}-${group}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="text-gray-400">└─</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<FontAwesomeIcon
|
||||
icon={faUsers}
|
||||
className="text-xs text-green/80"
|
||||
/>
|
||||
<span>{group}</span>
|
||||
<Badge variant="success">group</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user