diff --git a/ui/src/pages/Devices.tsx b/ui/src/pages/Devices.tsx index b5370b74f..fc06fa1fb 100644 --- a/ui/src/pages/Devices.tsx +++ b/ui/src/pages/Devices.tsx @@ -15,7 +15,9 @@ import { } from "react-admin"; import OnlineIcon from "@mui/icons-material/CheckCircleOutline"; import HighlightOffIcon from "@mui/icons-material/HighlightOff"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; import { useTheme } from "@mui/material/styles"; +import { Tooltip } from "@mui/material"; const DeviceListBulkActions = () => (
@@ -131,56 +133,16 @@ const DeviceShowLayout: FC = () => { reference="users" link="show" /> - +
+ + + + +
); }; - -// -// export const DeviceShow: FC = () => ( -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// ); diff --git a/ui/src/pages/SecurityGroups/SecurityGroupEditRules.tsx b/ui/src/pages/SecurityGroups/SecurityGroupEditRules.tsx index 72bafd0ea..2df029ac0 100644 --- a/ui/src/pages/SecurityGroups/SecurityGroupEditRules.tsx +++ b/ui/src/pages/SecurityGroups/SecurityGroupEditRules.tsx @@ -1,3 +1,4 @@ +import React, { useEffect, useState } from "react"; import { Button, ButtonGroup, @@ -11,19 +12,17 @@ import { MenuItem, Tooltip, } from "@mui/material"; +import * as Mui from "@mui/material"; +import Autocomplete from "@mui/material/Autocomplete"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; import { - IpProtocol, + ProtocolAliases, SecurityGroup, SecurityRule, UpdateSecurityGroup, } from "./SecurityGroupStructs"; import { fetchJson, backend } from "../../common/Api"; -import React, { useEffect, useState } from "react"; import Notifications from "../../common/Notifications"; -import * as Mui from "@mui/material"; -import Autocomplete from "@mui/material/Autocomplete"; -import { validateProtocolAndIpRange } from "../../common/IpHelpers"; -import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; interface EditRulesProps { groupName: string; @@ -90,6 +89,19 @@ const EditRules: React.FC = ({ "success" | "error" | "info" | null >(null); + const [ipRangeInputValue, setIpRangeInputValue] = useState(""); + + useEffect(() => { + // Initialize tempPortValues whenever secRule changes + const initialPortValues = secRule.map( + (rule) => + `${rule.from_port}${ + rule.to_port !== rule.from_port ? `-${rule.to_port}` : "" + }`, + ); + setTempPortValues(initialPortValues); + }, [secRule]); + // Message box errors for table edits const [fieldErrors, setFieldErrors] = useState< { from_port?: string; to_port?: string }[] @@ -179,24 +191,6 @@ const EditRules: React.FC = ({ }; // TODO: Implement for v4/v6 sanity checks against protocol and family. Tracked in issue #1445 - const handleIpRangeChange = (newValue: string[], index: number) => { - const updatedRule = { - ...secRule[index], - ip_ranges: newValue, - }; - - try { - validateProtocolAndIpRange(updatedRule.ip_protocol, newValue); - } catch (error: any) { - if (error instanceof Error) { - setNotificationType("error"); - setNotificationMessage(error.message); - } - return; // Skip updating the rule if validation fails - } - - onRuleChange(index, updatedRule); - }; const validatePortRange = ( port: number, @@ -209,79 +203,49 @@ const EditRules: React.FC = ({ const [tempPortValues, setTempPortValues] = useState([]); - const handleProtocolChange = ( - e: Mui.SelectChangeEvent, - index: number, - ) => { - const aliasProtocol = e.target.value as IpProtocol; - let updatedRule = { ...secRule[index], ip_protocol: aliasProtocol }; - switch (aliasProtocol) { - case "SSH": - updatedRule = { - ...updatedRule, - from_port: 22, - to_port: 22, - ip_protocol: "tcp", - }; - break; - case "HTTP": - updatedRule = { - ...updatedRule, - from_port: 80, - to_port: 80, - ip_protocol: "tcp", - }; - break; - case "HTTPS": - updatedRule = { - ...updatedRule, - from_port: 443, - to_port: 443, - ip_protocol: "tcp", - }; - break; - case "PostgreSQL": - updatedRule = { - ...updatedRule, - from_port: 5432, - to_port: 5432, - ip_protocol: "tcp", - }; - break; - case "MySQL": - updatedRule = { - ...updatedRule, - from_port: 3306, - to_port: 3306, - ip_protocol: "tcp", - }; - break; - case "SMB": - updatedRule = { - ...updatedRule, - from_port: 445, - to_port: 445, - ip_protocol: "tcp", - }; - break; - case "icmpv4": - case "icmpv6": - updatedRule = { ...updatedRule, from_port: 0, to_port: 0 }; - break; - case "icmp": // ALL ICMP - updatedRule = { - ...updatedRule, - from_port: 0, - to_port: 0, - ip_ranges: [], - }; - break; + const handleProtocolChange = (e: Mui.SelectChangeEvent, index: number) => { + const selectedProtocol = e.target.value; + let updatedRule = { ...secRule[index], ip_protocol: selectedProtocol }; + + if (ProtocolAliases[selectedProtocol]) { + const { port, type } = ProtocolAliases[selectedProtocol]; + updatedRule = { + ...updatedRule, + from_port: port, + to_port: port, + ip_protocol: type, + }; + } else if (["icmpv4", "icmpv6"].includes(selectedProtocol)) { + updatedRule = { ...updatedRule, from_port: 0, to_port: 0 }; } onRuleChange(index, updatedRule); }; - const isPredefinedRule = (protocol: IpProtocol): boolean => { + const getProtocolNameByPorts = ( + from_port: number, + to_port: number, + ip_protocol: string, + ): string | null => { + console.debug( + `Looking for protocol with from_port: ${from_port}, to_port: ${to_port}, ip_protocol: ${ip_protocol}`, + ); + // Loop through the ProtocolAliases to find a match, either a proto with a defined set + // of ports e.g., SSH or a protocol such as TCP with a port of 0-0, should always match + for (const [key, value] of Object.entries(ProtocolAliases)) { + if ( + value.port === from_port && + value.port === to_port && + value.type === ip_protocol + ) { + console.log(`Found match: ${key}`); + return key; + } + } + return null; + }; + + const isPredefinedRule = (protocol: string): boolean => { return [ "SSH", "HTTP", @@ -293,26 +257,12 @@ const EditRules: React.FC = ({ "icmpv4", "icmpv6", "ip", + "TCP", + "UDP", ].includes(protocol); }; - // map predefined protocol names to their corresponding port ranges for display render only, rules get sent with ip_protocol:tcp - const getPredefinedProtocolName = ( - from_port: number, - to_port: number, - ): IpProtocol | undefined => { - const predefinedProtocols: { [key: string]: IpProtocol } = { - "22-22": "SSH", - "80-80": "HTTP", - "443-443": "HTTPS", - "5432-5432": "PostgreSQL", - "3306-3306": "MySQL", - "445-445": "SMB", - }; - return predefinedProtocols[`${from_port}-${to_port}`]; - }; - - const isUnmodifiableIpRange = (protocol: IpProtocol): boolean => { + const isUnmodifiableIpRange = (protocol: string): boolean => { return ["ip", "icmp"].includes(protocol); }; @@ -341,17 +291,36 @@ const EditRules: React.FC = ({ - + IP Protocol - - Port Range - - - IP Ranges + +
+ Port Range + + + +
- Defined IP Ranges +
+ IP Ranges + + + +
Action @@ -372,12 +341,13 @@ const EditRules: React.FC = ({ > - {/* From Port - Starting Port Range */} + {/* Port Range Column*/} { const newTempPortValues = [...tempPortValues]; newTempPortValues[index] = `${rule.from_port}${ @@ -463,85 +420,87 @@ const EditRules: React.FC = ({ disabled={isPredefinedRule(rule.ip_protocol)} /> - {/*User Defined IP Ranges*/} - - { - const newValue = e.target.value - .split(",") - .map((item) => item.trim()); - const updatedRule = { ...rule, ip_ranges: newValue }; - onRuleChange(index, updatedRule); - }} - style={{ - backgroundColor: isUnmodifiableIpRange(rule.ip_protocol) - ? "#E8F4F9" - : "transparent", - }} - disabled={isUnmodifiableIpRange(rule.ip_protocol)} - /> - - {/*PreDefined IP Ranges*/} + {/*IP Ranges Column*/} - - typeof option === "string" ? option : option.title - } - getOptionDisabled={(option) => - isUnmodifiableIpRange(rule.ip_protocol) - } - value={rule.ip_ranges || []} - onChange={(_, newValue) => { - const updatedRule = { - ...rule, - ip_ranges: newValue.map((item) => - typeof item === "string" ? item : item.value, - ), - }; - onRuleChange(index, updatedRule); - }} - renderInput={(params) => ( - - )} - /> + + { + console.debug( + "onInputChange - newInputValue:", + newInputValue, + ); + setIpRangeInputValue(newInputValue); + }} + disabled={isUnmodifiableIpRange(rule.ip_protocol)} + options={[ + "::/0", + "0.0.0.0/0", + { + title: "Organization IPv4", + value: "100.64.0.0/10", + }, + { title: "Organization IPv6", value: "0200::/8" }, + ]} + getOptionLabel={(option) => + typeof option === "string" ? option : option.title + } + getOptionDisabled={(option) => + isUnmodifiableIpRange(rule.ip_protocol) + } + value={rule.ip_ranges || []} + freeSolo + autoHighlight // Highlight the first match as the user types + onChange={(_, newValue) => { + console.debug("onChange - newValue:", newValue); + // If the protocol is unmodifiable, clear the IP ranges for this rule + if (isUnmodifiableIpRange(rule.ip_protocol)) { + const updatedRule = { ...rule, ip_ranges: [] }; + onRuleChange(index, updatedRule); + setIpRangeInputValue(""); + return; + } + // Otherwise, update the IP ranges as normal + const updatedIpRanges = newValue.map((item) => + typeof item === "string" || typeof item === "number" + ? item + : item.value, + ); + const updatedRule = { + ...rule, + ip_ranges: updatedIpRanges, + }; + onRuleChange(index, updatedRule); + setIpRangeInputValue(""); // Clear the input value + }} + renderInput={(params) => ( + + )} + /> + + {/*Actions Column*/} = ({ const newRules = [...rules]; if (field === "ip_protocol" && typeof value === "string") { - newRules[index].ip_protocol = value as IpProtocol; + newRules[index].ip_protocol = value; } else if ( (field === "from_port" || field === "to_port") && typeof value === "number"