-
-
- {peer.ssh_enabled ? "Disable" : "Enable"} SSH Access
+ {showSSHButton && (
+
+ peer.ssh_enabled
+ ? disableDashboardSSH()
+ : setSSHInstructionsModal(true)
+ }
+ disabled={!permission.peers.update}
+ >
+
+
+
+ {peer.ssh_enabled ? "Disable" : "Enable"} SSH Access
+
-
-
+
+ )}
diff --git a/src/modules/peers/PeerConnectButton.tsx b/src/modules/peers/PeerConnectButton.tsx
index 05b60c80..5717ea33 100644
--- a/src/modules/peers/PeerConnectButton.tsx
+++ b/src/modules/peers/PeerConnectButton.tsx
@@ -6,12 +6,12 @@ import {
import FullTooltip from "@components/FullTooltip";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import { IconChevronDown } from "@tabler/icons-react";
-import { cn } from "@utils/helpers";
import * as React from "react";
import { usePeer } from "@/contexts/PeerProvider";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
+import { cn } from "@utils/helpers";
export const PeerConnectButton = () => {
const { peer } = usePeer();
diff --git a/src/modules/peers/PeerGroupCell.tsx b/src/modules/peers/PeerGroupCell.tsx
index 2e7a2f86..5c78f7c1 100644
--- a/src/modules/peers/PeerGroupCell.tsx
+++ b/src/modules/peers/PeerGroupCell.tsx
@@ -27,8 +27,13 @@ export default function PeerGroupCell() {
const groupIDs = useMemo(() => {
return peerGroups
- ?.map((group) => group.id)
- .filter((id) => id !== undefined) as string[];
+ ?.map((group) => {
+ if (group?.name === "All") return;
+ return group.id;
+ })
+ .filter((id) => {
+ return id !== undefined;
+ }) as string[];
}, [peerGroups]);
return (
@@ -37,6 +42,7 @@ export default function PeerGroupCell() {
description={"Use groups to control what this peer can access"}
groups={groupIDs || []}
hideAllGroup={true}
+ showAddGroupButton={true}
disabled={!permission.groups.update}
onSave={handleSave}
modal={modal}
diff --git a/src/modules/posture-checks/table/cells/PostureCheckPolicyUsageCell.tsx b/src/modules/posture-checks/table/cells/PostureCheckPolicyUsageCell.tsx
index f4849310..d43a6bb7 100644
--- a/src/modules/posture-checks/table/cells/PostureCheckPolicyUsageCell.tsx
+++ b/src/modules/posture-checks/table/cells/PostureCheckPolicyUsageCell.tsx
@@ -2,81 +2,101 @@ import Badge from "@components/Badge";
import Button from "@components/Button";
import FullTooltip from "@components/FullTooltip";
import { cn } from "@utils/helpers";
-import { ArrowUpRightSquareIcon } from "lucide-react";
+import { ArrowUpRightIcon, ShieldIcon, SquarePenIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import * as React from "react";
-import AccessControlIcon from "@/assets/icons/AccessControlIcon";
+import { useState } from "react";
+import CircleIcon from "@/assets/icons/CircleIcon";
+import { usePolicies } from "@/contexts/PoliciesProvider";
import { Policy } from "@/interfaces/Policy";
import { PostureCheck } from "@/interfaces/PostureCheck";
type Props = {
check: PostureCheck;
};
+
export const PostureCheckPolicyUsageCell = ({ check }: Props) => {
const router = useRouter();
+ const [tooltipOpen, setTooltipOpen] = useState(false);
+ const policyCount = check?.policies?.length || 0;
+ const policies = check?.policies;
+ const { openEditPolicyModal } = usePolicies();
return (
-
0)}
- content={
-
-
- Assigned
- {check.policies && check.policies?.length > 1
- ? " Policies"
- : " Policy"}
-
-
- {check.policies &&
- check.policies?.length > 0 &&
- check.policies?.map((policy: Policy, index: number) => {
- return (
-
0 && (
+
+ {policies?.map((policy: Policy) => {
+ const rule = policy?.rules?.[0];
+ if (!rule) return;
+ return (
+
+ );
+ })}
-
- }
- interactive={false}
- >
- {
- e.stopPropagation();
- router.push("/access-control");
- }}
- variant={"gray"}
- useHover={!!(check.policies && check.policies?.length > 0)}
- className={cn(
- "min-w-[110px] font-medium cursor-pointer",
- check.policies &&
- check.policies.length == 0 &&
- "opacity-30 pointer-events-none",
- )}
+ }
+ interactive={true}
+ align={"start"}
+ alignOffset={0}
+ sideOffset={14}
>
-
-
-
- {check.policies && check.policies?.length > 0
- ? check.policies && check.policies?.length
- : ""}
- {" "}
- {check.policies && check.policies?.length == 0
- ? "No Policies"
- : check.policies && check.policies?.length > 1
- ? "Policies"
- : "Policy"}
-
-
-
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ if (!tooltipOpen) setTooltipOpen(true);
+ }}
+ >
+
+
+ {policyCount}
+
+
+
+ )}
@@ -93,8 +113,8 @@ export const PostureCheckPolicyUsageCell = ({ check }: Props) => {
onClick={() => router.push("/access-control")}
>
<>
-
Go to Policies
+
>
diff --git a/src/modules/remote-access/rdp/RDPCredentialsModal.tsx b/src/modules/remote-access/rdp/RDPCredentialsModal.tsx
index c201e658..ce20f2ec 100644
--- a/src/modules/remote-access/rdp/RDPCredentialsModal.tsx
+++ b/src/modules/remote-access/rdp/RDPCredentialsModal.tsx
@@ -1,13 +1,8 @@
-import Button from "@components/Button";
-import HelpText from "@components/HelpText";
-import InlineLink from "@components/InlineLink";
-import { Input } from "@components/Input";
-import { Label } from "@components/Label";
+import * as React from "react";
+import { useCallback, useMemo, useState } from "react";
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
import ModalHeader from "@components/modal/ModalHeader";
-import Paragraph from "@components/Paragraph";
-import Separator from "@components/Separator";
-import { IconLoader2 } from "@tabler/icons-react";
+import { Peer } from "@/interfaces/Peer";
import {
ChevronsLeftRightEllipsis,
ExternalLinkIcon,
@@ -15,13 +10,18 @@ import {
MonitorIcon,
User2,
} from "lucide-react";
-import * as React from "react";
-import { useCallback, useMemo, useState } from "react";
-import { Peer } from "@/interfaces/Peer";
+import Separator from "@components/Separator";
+import Paragraph from "@components/Paragraph";
+import InlineLink from "@components/InlineLink";
+import Button from "@components/Button";
+import { Label } from "@components/Label";
+import HelpText from "@components/HelpText";
+import { Input } from "@components/Input";
import {
RDP_DOCS_LINK,
RDPCredentials,
} from "@/modules/remote-access/rdp/useRemoteDesktop";
+import { IconLoader2 } from "@tabler/icons-react";
type Props = {
open: boolean;
diff --git a/src/modules/remote-access/ssh/SSHButton.tsx b/src/modules/remote-access/ssh/SSHButton.tsx
index d2c198dd..b4196702 100644
--- a/src/modules/remote-access/ssh/SSHButton.tsx
+++ b/src/modules/remote-access/ssh/SSHButton.tsx
@@ -1,14 +1,14 @@
import Button from "@components/Button";
import { DropdownMenuItem } from "@components/DropdownMenu";
-import { getOperatingSystem } from "@hooks/useOperatingSystem";
import { CircleHelpIcon, TerminalIcon } from "lucide-react";
import * as React from "react";
import { useState } from "react";
import { usePermissions } from "@/contexts/PermissionsProvider";
-import { OperatingSystem } from "@/interfaces/OperatingSystem";
import { Peer } from "@/interfaces/Peer";
import { SSHCredentialsModal } from "@/modules/remote-access/ssh/SSHCredentialsModal";
import { SSHTooltip } from "@/modules/remote-access/ssh/SSHTooltip";
+import { getOperatingSystem } from "@hooks/useOperatingSystem";
+import { OperatingSystem } from "@/interfaces/OperatingSystem";
type Props = {
peer: Peer;
@@ -19,8 +19,9 @@ export const SSHButton = ({ peer, isDropdown = false }: Props) => {
const [modal, setModal] = useState(false);
const { permission } = usePermissions();
- const disabled =
- !peer.connected || !peer.ssh_enabled || !permission.peers.update;
+ const isSSHEnabled =
+ peer?.local_flags?.server_ssh_allowed || peer?.ssh_enabled;
+ const disabled = !peer.connected || !permission.peers.update || !isSSHEnabled;
const hasPermission = permission.peers.update;
@@ -42,7 +43,7 @@ export const SSHButton = ({ peer, isDropdown = false }: Props) => {
diff --git a/src/modules/remote-access/ssh/SSHTooltip.tsx b/src/modules/remote-access/ssh/SSHTooltip.tsx
index 93a18b94..108cc98d 100644
--- a/src/modules/remote-access/ssh/SSHTooltip.tsx
+++ b/src/modules/remote-access/ssh/SSHTooltip.tsx
@@ -78,7 +78,8 @@ const SSHDisabledText = ({
SSH Access is currently disabled for this peer. Please enable SSH access
- for this peer and make sure SSH is allowed in the NetBird Client.
+ for this peer and make sure to add an explicit access control policy
+ allowing SSH access.
{
{
name,
wg_pub_key: keyPairs.publicKey,
- rules: rules ?? ["tcp/22022", "tcp/3389", "tcp/44338"],
+ rules: rules ?? [
+ "tcp/22022",
+ "tcp/3389",
+ "tcp/44338",
+ "netbird-ssh/22",
+ ],
},
`/${peerId}/temporary-access`,
);
diff --git a/src/modules/route-group/NetworkRoutesTable.tsx b/src/modules/route-group/NetworkRoutesTable.tsx
index 6088a758..d6f8ea39 100644
--- a/src/modules/route-group/NetworkRoutesTable.tsx
+++ b/src/modules/route-group/NetworkRoutesTable.tsx
@@ -175,7 +175,7 @@ export default function NetworkRoutesTable({
wrapperProps={isGroupPage ? { className: "mt-6 w-full" } : undefined}
paginationPaddingClassName={isGroupPage ? "px-0 pt-8" : undefined}
tableClassName={isGroupPage ? "mt-0 mb-2" : undefined}
- inset={!isGroupPage}
+ inset={false}
minimal={isGroupPage}
keepStateInLocalStorage={!isGroupPage}
searchPlaceholder={"Search by network, range, name or groups..."}
diff --git a/src/modules/routes/RoutePeerCell.tsx b/src/modules/routes/RoutePeerCell.tsx
index 82c9596f..3d473dc8 100644
--- a/src/modules/routes/RoutePeerCell.tsx
+++ b/src/modules/routes/RoutePeerCell.tsx
@@ -1,6 +1,6 @@
import DescriptionWithTooltip from "@components/ui/DescriptionWithTooltip";
import GroupBadge from "@components/ui/GroupBadge";
-import PeerBadge from "@components/ui/PeerBadge";
+import PeerCountBadge from "@components/ui/PeerCountBadge";
import { ArrowRightIcon } from "lucide-react";
import * as React from "react";
import { useMemo } from "react";
@@ -44,9 +44,13 @@ export default function RoutePeerCell({ route }: Props) {
{group && (
<>
-
+
- {group.peers_count} Peer(s)
+
>
)}
diff --git a/src/modules/setup-keys/SetupKeysTable.tsx b/src/modules/setup-keys/SetupKeysTable.tsx
index 179e2f7c..ca615684 100644
--- a/src/modules/setup-keys/SetupKeysTable.tsx
+++ b/src/modules/setup-keys/SetupKeysTable.tsx
@@ -168,7 +168,7 @@ export default function SetupKeysTable({
wrapperProps={isGroupPage ? { className: "mt-6 w-full" } : undefined}
paginationPaddingClassName={isGroupPage ? "px-0 pt-8" : undefined}
tableClassName={isGroupPage ? "mt-0 mb-2" : undefined}
- inset={!isGroupPage}
+ inset={false}
minimal={isGroupPage}
keepStateInLocalStorage={!isGroupPage}
text={"Setup Keys"}
diff --git a/src/modules/users/HorizontalUsersStack.tsx b/src/modules/users/HorizontalUsersStack.tsx
index 90f5aaa2..f1dc5153 100644
--- a/src/modules/users/HorizontalUsersStack.tsx
+++ b/src/modules/users/HorizontalUsersStack.tsx
@@ -4,6 +4,7 @@ import TextWithTooltip from "@components/ui/TextWithTooltip";
import { cn, generateColorFromString } from "@utils/helpers";
import { orderBy } from "lodash";
import * as React from "react";
+import { useMemo } from "react";
import { User } from "@/interfaces/User";
import { SmallUserAvatar } from "@/modules/users/SmallUserAvatar";
@@ -12,6 +13,7 @@ type Props = {
max?: number;
avatarClassName?: string;
side?: "left" | "right" | "top" | "bottom";
+ isAllGroup?: boolean;
};
export const HorizontalUsersStack = ({
@@ -19,9 +21,15 @@ export const HorizontalUsersStack = ({
max = 3,
avatarClassName,
side = "top",
+ isAllGroup = false,
}: Props) => {
let usersToDisplay = orderBy(users?.slice(0, max) || [], ["name"]);
+ const userCountText = useMemo(() => {
+ if (isAllGroup) return "All Users";
+ return `${users?.length || 0} User(s)`;
+ }, [users, isAllGroup]);
+
return (
@@ -97,7 +105,7 @@ export const HorizontalUsersStack = ({
users.length > 0 && "group-hover/user-stack:text-nb-gray-200 ",
)}
>
- {users?.length || 0} User(s)
+ {userCountText}
diff --git a/src/modules/users/UserPeersSection.tsx b/src/modules/users/UserPeersSection.tsx
new file mode 100644
index 00000000..9252259b
--- /dev/null
+++ b/src/modules/users/UserPeersSection.tsx
@@ -0,0 +1,73 @@
+import * as React from "react";
+import { Suspense, useMemo } from "react";
+import { usePortalElement } from "@hooks/usePortalElement";
+import SkeletonTable, {
+ SkeletonTableHeader,
+} from "@components/skeletons/SkeletonTable";
+import { User } from "@/interfaces/User";
+import useFetchApi from "@utils/api";
+import { Peer } from "@/interfaces/Peer";
+import MinimalPeersTable from "@/modules/peer/MinimalPeersTable";
+import NoResults from "@components/ui/NoResults";
+import PeerIcon from "@/assets/icons/PeerIcon";
+import Paragraph from "@components/Paragraph";
+
+type Props = {
+ user: User;
+};
+
+export const UserPeersSection = ({ user }: Props) => {
+ const { ref: headingRef, portalTarget } =
+ usePortalElement
();
+
+ const { data: peers, isLoading: isPeersLoading } =
+ useFetchApi("/peers");
+
+ const userPeers = useMemo(() => {
+ return (
+ peers?.filter((peer) => {
+ return peer?.user_id === user.id;
+ }) || []
+ );
+ }, [user, peers]);
+
+ return (
+
+
+
+
+
Peers
+
View all peers registered by this user.
+
+
+
+
+
+
+
+
+
+ }
+ >
+
}
+ />
+ }
+ />
+
+
+
+ );
+};
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 58992b92..b212fa90 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -244,8 +244,12 @@ export const getBrowserInfo = () => {
}
};
-export const singularize = (word: string, count?: number) => {
- if (!count) return word;
+export const singularize = (
+ word: string,
+ count?: number,
+ showZero?: boolean,
+) => {
+ if (!count) return showZero ? `0 ${word}` : word;
if (word.endsWith("ies") && count === 1) {
return count + " " + word.slice(0, -3) + "y";
} else if (word.endsWith("s") && count === 1) {
diff --git a/src/utils/version.ts b/src/utils/version.ts
index 16616e57..3b38a45d 100644
--- a/src/utils/version.ts
+++ b/src/utils/version.ts
@@ -84,3 +84,13 @@ export const isNativeSSHSupported = (version: string) => {
if (version == "development") return true;
return compareVersions(version, "0.60.0");
};
+
+/**
+ * Check if NetBird SSH protocol is supported.
+ * Supported starting from NetBird v0.61.0+.
+ * @param version
+ */
+export const isNetbirdSSHProtocolSupported = (version: string) => {
+ if (version == "development") return true;
+ return compareVersions(version, "0.61.0");
+};