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}
/>
)}
>