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..9fe7167e5 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); }; @@ -325,13 +275,19 @@ const EditRules: React.FC = ({
- + - + @@ -341,17 +297,36 @@ const EditRules: React.FC = ({ - + IP Protocol - - Port Range - - - IP Ranges + +
+ Port Range + + + +
- - Defined IP Ranges + +
+ IP Ranges + + + +
Action @@ -372,12 +347,13 @@ const EditRules: React.FC = ({ > - {/* From Port - Starting Port Range */} + {/* Port Range Column*/} { const newTempPortValues = [...tempPortValues]; newTempPortValues[index] = `${rule.from_port}${ @@ -463,50 +426,36 @@ 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*/} { + console.debug( + "onInputChange - newInputValue:", + newInputValue, + ); + // Update state for the individual row + setIpRangeInputValue((prev) => { + const newArr = [...prev]; + newArr[index] = newInputValue; + return newArr; + }); + }} disabled={isUnmodifiableIpRange(rule.ip_protocol)} options={[ "::/0", "0.0.0.0/0", { - title: "Nexodus Private IPv4 CIDR", + title: "Organization IPv4", value: "100.64.0.0/10", }, - { title: "Nexodus Private IPv6 CIDR", value: "0200::/8" }, + { title: "Organization IPv6", value: "0200::/8" }, ]} getOptionLabel={(option) => typeof option === "string" ? option : option.title @@ -515,14 +464,35 @@ const EditRules: React.FC = ({ 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 (isUnmodifiableIpRange(rule.ip_protocol)) { + const updatedRule = { ...rule, ip_ranges: [] }; + onRuleChange(index, updatedRule); + setIpRangeInputValue((prev) => { + const newArr = [...prev]; + newArr[index] = ""; + return newArr; + }); + return; + } + const updatedIpRanges = newValue.map((item) => + typeof item === "string" || typeof item === "number" + ? item + : item.value, + ); const updatedRule = { ...rule, - ip_ranges: newValue.map((item) => - typeof item === "string" ? item : item.value, - ), + ip_ranges: updatedIpRanges, }; onRuleChange(index, updatedRule); + setIpRangeInputValue((prev) => { + const newArr = [...prev]; + newArr[index] = ""; + return newArr; + }); // Clear the input value }} renderInput={(params) => ( = ({ )} /> + + {/*Actions Column*/} void; + inboundRules: SecurityRule[]; + outboundRules: SecurityRule[]; } - const SecurityGroupTable: React.FC = ({ data, type, editable = false, updateData, }) => { - const derivedRules = + const derivedRules: SecurityRule[] = type === "inbound_rules" ? data.inbound_rules || [] : data.outbound_rules || []; @@ -43,17 +39,22 @@ const SecurityGroupTable: React.FC = ({ useEffect(() => { console.log("Setting rules in useEffect:", derivedRules); - if (!rules.length) { - // Only update if the rules are empty (TODO: not sure this is needed) - setRules(derivedRules); - } + // Force update the rules from derivedRules + setRules(derivedRules); }, [type, data]); - const handleDeleteRule = (index: number) => { + const handleDeleteRule = (index: number): void => { const newRules = [...rules]; newRules.splice(index, 1); setRules(newRules); - updateData && updateData(type, newRules); + + if (updateData) { + updateData(type, newRules); + } else { + console.warn( + "updateData function is not provided, changes won't propagate.", + ); + } }; const handleRuleChange = ( @@ -64,7 +65,7 @@ const SecurityGroupTable: React.FC = ({ 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" diff --git a/ui/src/pages/SecurityGroups/SecurityGroups.tsx b/ui/src/pages/SecurityGroups/SecurityGroups.tsx index 958b875cf..17664d7e0 100644 --- a/ui/src/pages/SecurityGroups/SecurityGroups.tsx +++ b/ui/src/pages/SecurityGroups/SecurityGroups.tsx @@ -11,23 +11,21 @@ import { Tab, Button, } from "@mui/material"; -import { SecurityGroup, SecurityRule } from "./SecurityGroupStructs"; +import { + Organization, + SecurityGroup, + SecurityRule, +} from "./SecurityGroupStructs"; import EditRules from "./SecurityGroupEditRules"; import { backend, fetchJson } from "../../common/Api"; import Notifications from "../../common/Notifications"; -type OrgType = { - id: string; - name: string; - security_group_id: string; -}; - export const SecurityGroups = () => { const [organizationId, setOrganizationId] = useState(null); const [securityGroupId, setSecurityGroupId] = useState(null); - const [orgs, setOrgs] = useState([]); + const [orgs, setOrgs] = useState([]); const [securityGroups, setSecurityGroups] = useState([]); - const [selectedOrg, setSelectedOrg] = useState(null); + const [selectedOrg, setSelectedOrg] = useState(null); const [selectedSecurityGroup, setSelectedSecurityGroup] = useState(null); @@ -38,7 +36,6 @@ export const SecurityGroups = () => { const [editedRules, setEditedRules] = useState([]); const [inboundRules, setInboundRules] = useState([]); const [outboundRules, setOutboundRules] = useState([]); - const [apiError, setApiError] = useState(null); // Snackbar notifications in common/Notifications.tsx const [notificationMessage, setNotificationMessage] = useState( @@ -48,27 +45,34 @@ export const SecurityGroups = () => { "success" | "error" | "info" | null >(null); - const fetchData = () => { - fetchJson(`${backend}/api/organizations`) - .then((orgs: OrgType[]) => { - setOrgs(orgs); - // Store the organizationId and securityGroupId for later use - setOrganizationId(orgs[0].id); // TODO: using the first org's id - setSecurityGroupId(orgs[0].security_group_id); // TODO: using the first org's security group id - return Promise.all( - orgs.map((org) => - fetchJson( - `${backend}/api/organizations/${org.id}/security_group/${org.security_group_id}`, - ), - ), - ); - }) - .then((securityGroupsData) => { - setSecurityGroups(securityGroupsData); - }) - .catch((error) => { - console.error("Error:", error); - }); + const fetchSecurityGroup = async (orgId: string, securityGroupId: string) => { + return await fetchJson( + `${backend}/api/organizations/${orgId}/security_group/${securityGroupId}`, + ); + }; + + const fetchData = async () => { + try { + const orgs: Organization[] = await fetchJson( + `${backend}/api/organizations`, + ); + setOrgs(orgs); + // Set the organizationId and securityGroupId based on the selected organization + if (orgs.length > 0) { + // TODO: handle an array of security groups + setOrganizationId(orgs[0].id); + setSecurityGroupId(orgs[0].security_group_id); + } + // Fetch security groups data + const securityGroupPromises = orgs.map((org) => + fetchSecurityGroup(org.id, org.security_group_id), + ); + + const securityGroupsData = await Promise.all(securityGroupPromises); + setSecurityGroups(securityGroupsData); + } catch (error) { + console.error("Error:", error); + } }; useEffect(() => { @@ -83,6 +87,25 @@ export const SecurityGroups = () => { } }, [selectedSecurityGroup]); + const handleExitEditMode = async () => { + setIsEditing(false); + if (selectedOrg) { + try { + const updatedSecurityGroupData = await fetchSecurityGroup( + selectedOrg.id, + selectedOrg.security_group_id, + ); + setSelectedSecurityGroup(updatedSecurityGroupData); + } catch (error) { + console.error("Error:", error); + setNotificationType("error"); + setNotificationMessage( + "Unable to fetch data from the Nexodus api-server.", + ); + } + } + }; + const handleEditClick = () => { setIsEditing(true); const rulesToEdit = @@ -93,15 +116,17 @@ export const SecurityGroups = () => { setEditedRules(rulesToEdit); }; - const selectOrganization = (org: OrgType) => { + const selectOrganization = (org: Organization) => { setSelectedOrg(org); + // Updating state for the selected organization and security group + setOrganizationId(org.id); + setSecurityGroupId(org.security_group_id); + const matchingSecurityGroup = securityGroups.find( (sg) => sg.id === org.security_group_id, ); - console.log("Setting selectedSecurityGroup:", matchingSecurityGroup); setSelectedSecurityGroup(matchingSecurityGroup || null); }; - const handleTabChange = ( event: React.ChangeEvent<{}>, newValue: "inbound_rules" | "outbound_rules", @@ -120,34 +145,9 @@ export const SecurityGroups = () => { } }; - // TODO: this isn't updating on cancel, you have to click another tab and come back to see the latest change from a save - const handleExitEditMode = async () => { - setIsEditing(false); - if (selectedOrg) { - try { - // Reset notification state - setNotificationType(null); - setNotificationMessage(null); - - // setApiError(null); - const updatedSecurityGroupData = await fetchJson( - `${backend}/api/organizations/${selectedOrg.id}/security_group/${selectedOrg.security_group_id}`, - ); - setSelectedSecurityGroup(updatedSecurityGroupData); - } catch (error) { - console.error("Error:", error); - // Use Snackbar notifications for displaying the error - setNotificationType("error"); - setNotificationMessage( - "Unable to fetch data from the Nexodus api-server.", - ); - } - } - }; - return (
- {/* Listen to notification state */} + {/* Listen for notification state */}
@@ -252,6 +252,8 @@ export const SecurityGroups = () => { data={selectedSecurityGroup} type={activeTab} updateData={updateDataInParent} + inboundRules={inboundRules} + outboundRules={outboundRules} /> )}