"use client"; import Button from "@components/Button"; import FancyToggleSwitch from "@components/FancyToggleSwitch"; import HelpText from "@components/HelpText"; import InlineLink from "@components/InlineLink"; import { Input } from "@components/Input"; import { Label } from "@components/Label"; import { Modal, ModalClose, ModalContent, ModalFooter, ModalTrigger, } from "@components/modal/Modal"; import ModalHeader from "@components/modal/ModalHeader"; import { notify } from "@components/Notification"; import Paragraph from "@components/Paragraph"; import { PeerGroupSelector } from "@components/PeerGroupSelector"; import { PortSelector } from "@components/PortSelector"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@components/Select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs"; import { Textarea } from "@components/Textarea"; import PolicyDirection, { Direction } from "@components/ui/PolicyDirection"; import useFetchApi, { useApiCall } from "@utils/api"; import { cn } from "@utils/helpers"; import { uniqBy } from "lodash"; import { ArrowRightLeft, ExternalLinkIcon, FolderDown, FolderInput, PlusCircle, Power, Share2, Shield, Text, } from "lucide-react"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { useSWRConfig } from "swr"; import AccessControlIcon from "@/assets/icons/AccessControlIcon"; import { usePolicies } from "@/contexts/PoliciesProvider"; import { Group } from "@/interfaces/Group"; import { Policy, Protocol } from "@/interfaces/Policy"; import { PostureCheck } from "@/interfaces/PostureCheck"; import useGroupHelper from "@/modules/groups/useGroupHelper"; import { PostureCheckTab } from "@/modules/posture-checks/ui/PostureCheckTab"; import { PostureCheckTabTrigger } from "@/modules/posture-checks/ui/PostureCheckTabTrigger"; type Props = { children?: React.ReactNode; }; type UpdateModalProps = { policy: Policy; open: boolean; onOpenChange?: (open: boolean) => void; cell?: string; }; export default function AccessControlModal({ children }: Props) { const [modal, setModal] = useState(false); return ( <> {children && {children}} {modal && ( setModal(false)} /> )} > ); } export function AccessControlUpdateModal({ policy, open, onOpenChange, cell, }: UpdateModalProps) { return ( <> {open && ( onOpenChange && onOpenChange(false)} policy={policy} cell={cell} /> )} > ); } type ModalProps = { onSuccess?: (p: Policy) => void; policy?: Policy; cell?: string; }; export function AccessControlModalContent({ onSuccess, policy, cell, }: ModalProps) { const { data: allPostureChecks, isLoading: isPostureChecksLoading } = useFetchApi("/posture-checks"); const { updatePolicy } = usePolicies(); const firstRule = policy?.rules ? policy.rules[0] : undefined; const [tab, setTab] = useState(() => { if (!cell) return "policy"; if (cell == "posture_checks") return "posture_checks"; if (cell == "name") return "general"; return "policy"; }); const [enabled, setEnabled] = useState(policy?.enabled ?? true); const [ports, setPorts] = useState(() => { if (!firstRule) return []; if (firstRule.ports == undefined) return []; if (firstRule.ports.length > 0) { return firstRule.ports.map((p) => Number(p)); } return []; }); const [protocol, setProtocol] = useState( firstRule ? firstRule.protocol : "all", ); const [direction, setDirection] = useState(() => { if (firstRule && firstRule?.bidirectional) return "bi"; if (firstRule && firstRule?.bidirectional == false) return "in"; return "bi"; }); const [name, setName] = useState(policy?.name || ""); const [description, setDescription] = useState(policy?.description || ""); const { mutate } = useSWRConfig(); const policyRequest = useApiCall("/policies"); const [ sourceGroups, setSourceGroups, { getGroupsToUpdate: getSourceGroupsToUpdate }, ] = useGroupHelper({ initial: firstRule ? (firstRule.sources as Group[]) : [], }); const [ destinationGroups, setDestinationGroups, { getGroupsToUpdate: getDestinationGroupsToUpdate }, ] = useGroupHelper({ initial: firstRule ? (firstRule.destinations as Group[]) : [], }); const submit = async () => { const g1 = getSourceGroupsToUpdate(); const g2 = getDestinationGroupsToUpdate(); const createOrUpdateGroups = uniqBy([...g1, ...g2], "name").map( (g) => g.promise, ); const groups = await Promise.all( createOrUpdateGroups.map((call) => call()), ); let sources = sourceGroups .map((g) => { const find = groups.find((group) => group.name === g.name); return find?.id; }) .filter((g) => g !== undefined) as string[]; let destinations = destinationGroups .map((g) => { const find = groups.find((group) => group.name === g.name); return find?.id; }) .filter((g) => g !== undefined) as string[]; if (direction == "out") { const tmp = sources; sources = destinations; destinations = tmp; } const policyObj = { name, description, enabled, source_posture_checks: postureChecks ? postureChecks.map((c) => c.id) : undefined, rules: [ { bidirectional: direction == "bi", description, name, action: "accept", protocol, enabled, sources, destinations, ports: ports.length > 0 ? ports.map((p) => p.toString()) : undefined, }, ], } as Policy; if (policy) { updatePolicy( policy, policyObj, () => { mutate("/policies"); onSuccess && onSuccess(policy); }, "The policy was successfully saved", ); } else { notify({ title: "Create Access Control Policy", description: "Policy was created successfully.", loadingMessage: "Creating your policy...", promise: policyRequest.post(policyObj).then((policy) => { mutate("/policies"); onSuccess && onSuccess(policy); }), }); } }; const portAndDirectionDisabled = protocol == "icmp" || protocol == "all"; const [postureChecks, setPostureChecks] = useState([]); const postureChecksLoaded = useRef(false); const initialPostureChecks = useMemo(() => { return ( allPostureChecks?.filter((check) => { if (policy?.source_posture_checks) { return policy.source_posture_checks.includes(check.id); } return false; }) || [] ); }, [policy, allPostureChecks]); useEffect(() => { if (postureChecksLoaded.current) return; if (initialPostureChecks.length > 0) { postureChecksLoaded.current = true; setPostureChecks(initialPostureChecks); } }, [initialPostureChecks]); const continuePostureChecksDisabled = useMemo(() => { if (sourceGroups.length == 0 || destinationGroups.length == 0) return true; if (direction != "bi" && ports.length == 0) return true; }, [sourceGroups, destinationGroups, direction, ports]); const submitDisabled = useMemo(() => { if (name.length == 0) return true; if (continuePostureChecksDisabled) return true; }, [name, continuePostureChecksDisabled]); const handleProtocolChange = (p: Protocol) => { setProtocol(p); if (p == "icmp") { setPorts([]); } if (p == "all") { setPorts([]); } }; return ( } title={ policy ? "Update Access Control Policy" : "Create New Access Control Policy" } description={ "Use this policy to restrict access to groups of resources." } color={"netbird"} /> setTab(v)} value={tab}> Policy Name & Description Protocol Allow only specified network protocols. To change traffic direction and ports, select{" "} TCP or{" "} UDP protocol. handleProtocolChange(v as Protocol)} > ALL TCP UDP ICMP Source Destination Ports Allow network traffic and access only to specified ports. Select ports between 1 and 65535. Enable Policy > } helpText={"Use this switch to enable or disable the policy."} /> Name of the Rule Set an easily identifiable name for your policy. setName(e.target.value)} placeholder={"e.g., Devs to Servers"} /> Description (optional) Write a short description to add more context to this policy. setDescription(e.target.value)} placeholder={ "e.g., Devs are allowed to access servers and servers are allowed to access Devs." } rows={3} /> Learn more about Access Controls {!policy ? ( <> {tab == "policy" && ( Cancel )} {tab == "posture_checks" && ( setTab("policy")}> Back )} {tab == "policy" && ( setTab("posture_checks")} disabled={continuePostureChecksDisabled} > Continue )} {tab == "posture_checks" && ( setTab("general")} disabled={continuePostureChecksDisabled} > Continue )} {tab == "general" && ( <> setTab("posture_checks")} > Back Add Policy > )} > ) : ( <> Cancel Save Changes > )} ); }