Merge pull request #3535 from Infisical/feat/addGroupsToSshHosts

feat(ssh-hosts): Add groups to ssh hosts allowed principals
This commit is contained in:
carlosmonastyrski
2025-05-09 22:52:35 -03:00
committed by GitHub
26 changed files with 639 additions and 208 deletions
+2 -1
View File
@@ -6,7 +6,8 @@ export enum LoginMappingSource {
export type TLoginMapping = {
loginUser: string;
allowedPrincipals: {
usernames: string[];
usernames?: string[];
groups?: string[];
};
source: LoginMappingSource;
};
@@ -16,8 +16,11 @@ export const workspaceKeys = {
type ? ["workspaces", { type }] : (["workspaces"] as const),
getWorkspaceAuditLogs: (workspaceId: string) =>
[{ workspaceId }, "workspace-audit-logs"] as const,
getWorkspaceUsers: (workspaceId: string, includeGroupMembers?: boolean, roles?: string[]) =>
[{ workspaceId, includeGroupMembers, roles }, "workspace-users"] as const,
getWorkspaceUsers: (
workspaceId: string,
includeGroupMembers: boolean = false,
roles: string[] = []
) => [{ workspaceId, includeGroupMembers, roles }, "workspace-users"] as const,
getWorkspaceUserDetails: (workspaceId: string, membershipId: string) =>
[{ workspaceId, membershipId }, "workspace-user-details"] as const,
getWorkspaceIdentityMemberships: (workspaceId: string) =>
@@ -1,33 +1,37 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { faHome } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
createFileRoute,
linkOptions,
stripSearchParams,
} from '@tanstack/react-router'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
import { SettingsPage } from "./SettingsPage";
import { SettingsPage } from './SettingsPage'
const SettingsPageQueryParams = z.object({
selectedTab: z.string().catch("")
});
selectedTab: z.string().catch(''),
})
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
'/_authenticate/_inject-org-details/_org-layout/organization/settings/',
)({
component: SettingsPage,
validateSearch: zodValidator(SettingsPageQueryParams),
search: {
middlewares: [stripSearchParams({ selectedTab: "" })]
middlewares: [stripSearchParams({ selectedTab: '' })],
},
context: () => ({
breadcrumbs: [
{
label: "Home",
label: 'Home',
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
link: linkOptions({ to: '/' }),
},
{
label: "Settings"
}
]
})
});
label: 'Settings',
},
],
}),
})
@@ -125,7 +125,7 @@ export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props)
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
<Input {...field} onChange={(e) => field.onChange(e.target.value.trim())} />
</FormControl>
)}
/>
@@ -22,6 +22,7 @@ import {
useCreateSshHostGroup,
useGetSshHostGroupById,
useGetWorkspaceUsers,
useListWorkspaceGroups,
useListWorkspaceSshHostGroups,
useUpdateSshHostGroup
} from "@app/hooks/api";
@@ -38,7 +39,14 @@ const schema = z
loginMappings: z
.object({
loginUser: z.string().trim().min(1),
allowedPrincipals: z.array(z.string().trim()).default([])
allowedPrincipals: z
.array(
z.object({
type: z.enum(["user", "group"]),
value: z.string().trim().min(1)
})
)
.default([])
})
.array()
.default([])
@@ -49,9 +57,10 @@ export type FormData = z.infer<typeof schema>;
export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data: sshHostGroups } = useListWorkspaceSshHostGroups(currentWorkspace.id);
const projectId = currentWorkspace.id;
const { data: sshHostGroups } = useListWorkspaceSshHostGroups(projectId);
const { data: members = [] } = useGetWorkspaceUsers(projectId);
const { data: groups = [] } = useListWorkspaceGroups(projectId);
const [expandedMappings, setExpandedMappings] = useState<Record<number, boolean>>({});
const { data: sshHostGroup } = useGetSshHostGroupById(
@@ -87,7 +96,16 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
name: sshHostGroup.name,
loginMappings: sshHostGroup.loginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser,
allowedPrincipals: allowedPrincipals.usernames
allowedPrincipals: [
...(allowedPrincipals.usernames || []).map((username) => ({
type: "user" as const,
value: username
})),
...(allowedPrincipals.groups || []).map((group) => ({
type: "group" as const,
value: group
}))
]
}))
});
@@ -118,27 +136,35 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
return;
}
const transformedLoginMappings = loginMappings.map(({ loginUser, allowedPrincipals }) => {
const usernames = allowedPrincipals
.filter((p) => p.type === "user" && p.value)
.map((p) => p.value);
const groupNames = allowedPrincipals
.filter((p) => p.type === "group" && p.value)
.map((p) => p.value);
return {
loginUser,
allowedPrincipals: {
usernames,
groups: groupNames
}
};
});
if (sshHostGroup) {
await updateMutateAsync({
sshHostGroupId: sshHostGroup.id,
name,
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser,
allowedPrincipals: {
usernames: allowedPrincipals
}
}))
loginMappings: transformedLoginMappings
});
} else {
await createMutateAsync({
projectId,
name,
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser,
allowedPrincipals: {
usernames: allowedPrincipals
}
}))
loginMappings: transformedLoginMappings
});
}
@@ -165,6 +191,15 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
}));
};
const isPrincipalDuplicate = (
mappingIndex: number,
principalType: string,
principalValue: string
) => {
const principals = getValues(`loginMappings.${mappingIndex}.allowedPrincipals`) || [];
return principals.some((p) => p.type === principalType && p.value === principalValue);
};
return (
<Modal
isOpen={popUp?.sshHostGroup?.isOpen}
@@ -203,7 +238,10 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
variant="outline_bg"
onClick={() => {
const newIndex = loginMappingsFormFields.fields.length;
loginMappingsFormFields.append({ loginUser: "", allowedPrincipals: [""] });
loginMappingsFormFields.append({
loginUser: "",
allowedPrincipals: [{ type: "user", value: "" }]
});
setExpandedMappings((prev) => ({
...prev,
[newIndex]: true
@@ -299,7 +337,10 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
variant="outline_bg"
onClick={() => {
const current = getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
setValue(`loginMappings.${i}.allowedPrincipals`, [
...current,
{ type: "user", value: "" }
]);
}}
>
Add Principal
@@ -310,40 +351,69 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
name={`loginMappings.${i}.allowedPrincipals`}
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
<div className="flex flex-col space-y-2">
{(value.length === 0 ? [""] : value).map(
(principal: string, principalIndex: number) => (
<div
key={`${metadataFieldId}-principal-${principal}`}
className="flex items-center space-x-2"
>
<div className="flex-1">
<Select
value={principal}
onValueChange={(newValue) => {
if (value.includes(newValue)) {
createNotification({
text: "This principal is already added",
type: "error"
});
return;
}
const newPrincipals = [...value];
newPrincipals[principalIndex] = newValue;
onChange(newPrincipals);
}}
placeholder="Select a member"
className="w-full"
>
{members.map((member) => (
<SelectItem
key={member.user.id}
value={member.user.username}
>
{member.user.username}
</SelectItem>
))}
</Select>
</div>
{value.map((principal, principalIndex) => (
<div
key={`principal-${i + 1}-${principalIndex + 1}-${principal.type}`}
className="flex items-center space-x-2"
>
<div className="mr-2">
<Select
className="w-24"
value={principal.type}
onValueChange={(newType) => {
const newPrincipals = [...value];
newPrincipals[principalIndex] = {
type: newType as "user" | "group",
value: ""
};
onChange(newPrincipals);
}}
>
<SelectItem value="user">User</SelectItem>
<SelectItem value="group">Group</SelectItem>
</Select>
</div>
<div className="flex-1">
<Select
value={principal.value}
onValueChange={(newValue) => {
if (isPrincipalDuplicate(i, principal.type, newValue)) {
createNotification({
text: `This ${principal.type} is already added`,
type: "error"
});
return;
}
const newPrincipals = [...value];
newPrincipals[principalIndex] = {
type: principal.type as "user" | "group",
value: newValue
};
onChange(newPrincipals);
}}
placeholder={`Select a ${principal.type}`}
className="w-full"
>
{principal.type === "user"
? members.map((member) => (
<SelectItem
key={member.user.id}
value={member.user.username}
>
{member.user.username}
</SelectItem>
))
: groups.map((group) => (
<SelectItem
key={group.group.slug}
value={group.group.slug}
>
{group.group.slug}
</SelectItem>
))}
</Select>
</div>
<div className="flex w-10 justify-center">
<IconButton
size="sm"
ariaLabel="delete principal"
@@ -353,14 +423,14 @@ export const SshHostGroupModal = ({ popUp, handlePopUpToggle }: Props) => {
const newPrincipals = value.filter(
(_, idx) => idx !== principalIndex
);
onChange(newPrincipals);
onChange(newPrincipals.length ? newPrincipals : []);
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
)
)}
</div>
))}
{error && <span className="text-sm text-red-500">{error.message}</span>}
</div>
)}
@@ -1,4 +1,4 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -56,22 +56,38 @@ export const SshHostGroupsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Host Groups</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SshHostGroups}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddSshHostGroupModal()}
isDisabled={!isAllowed}
>
Add Group
</Button>
)}
</ProjectPermissionCan>
<div className="flex justify-end">
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/ssh/host-groups"
>
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</span>
</a>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SshHostGroups}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddSshHostGroupModal()}
isDisabled={!isAllowed}
className="ml-4"
>
Add Group
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
<SshHostGroupsTable handlePopUpOpen={handlePopUpOpen} />
<SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
@@ -1,10 +1,18 @@
import { faEllipsis, faPencil, faServer, faTrash } from "@fortawesome/free-solid-svg-icons";
import {
faEllipsis,
faPencil,
faServer,
faTrash,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -77,9 +85,40 @@ export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => {
group.loginMappings.map(({ loginUser, allowedPrincipals }) => (
<div key={`${group.id}-${loginUser}`} className="mb-2">
<div className="text-mineshaft-200">{loginUser}</div>
{allowedPrincipals.usernames.map((username) => (
<div key={`${group.id}-${loginUser}-${username}`} className="ml-4">
└─ {username}
{allowedPrincipals.usernames?.map((username) => (
<div
key={`${loginUser}-${username}`}
className="flex items-center gap-2"
>
<div className="flex items-center">
<span className="text-gray-400">└─</span>
</div>
<div className="flex items-center gap-1.5">
<FontAwesomeIcon
icon={faUser}
className="text-xs text-yellow/80"
/>
<span>{username}</span>
<Badge variant="primary">user</Badge>
</div>
</div>
))}
{allowedPrincipals.groups?.map((allowedGroup) => (
<div
key={`${loginUser}-${allowedGroup}`}
className="flex items-center gap-2"
>
<div className="flex items-center">
<span className="text-gray-400">└─</span>
</div>
<div className="flex items-center gap-1.5">
<FontAwesomeIcon
icon={faUsers}
className="text-xs text-green/80"
/>
<span>{allowedGroup}</span>
<Badge variant="success">group</Badge>
</div>
</div>
))}
</div>
@@ -23,6 +23,7 @@ import {
useCreateSshHost,
useGetSshHostById,
useGetWorkspaceUsers,
useListWorkspaceGroups,
useListWorkspaceSshHosts,
useUpdateSshHost
} from "@app/hooks/api";
@@ -49,7 +50,14 @@ const schema = z
loginMappings: z
.object({
loginUser: z.string().trim().min(1),
allowedPrincipals: z.array(z.string().trim()).default([]),
allowedPrincipals: z
.array(
z.object({
type: z.enum(["user", "group"]),
value: z.string().trim().min(1)
})
)
.default([]),
source: z.nativeEnum(LoginMappingSource)
})
.array()
@@ -64,6 +72,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
const projectId = currentWorkspace?.id || "";
const { data: sshHosts } = useListWorkspaceSshHosts(currentWorkspace.id);
const { data: members = [] } = useGetWorkspaceUsers(projectId);
const { data: groups = [] } = useListWorkspaceGroups(projectId);
const [expandedMappings, setExpandedMappings] = useState<Record<number, boolean>>({});
const { data: sshHost } = useGetSshHostById(
@@ -103,7 +112,16 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
userCertTtl: sshHost.userCertTtl,
loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals, source }) => ({
loginUser,
allowedPrincipals: allowedPrincipals.usernames,
allowedPrincipals: [
...(allowedPrincipals.usernames || []).map((username) => ({
type: "user" as const,
value: username
})),
...(allowedPrincipals.groups || []).map((group) => ({
type: "group" as const,
value: group
}))
],
source
}))
});
@@ -159,18 +177,31 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
}
}
const transformedLoginMappings = hostLoginMappings.map(({ loginUser, allowedPrincipals }) => {
const usernames = allowedPrincipals
.filter((p) => p.type === "user" && p.value)
.map((p) => p.value);
const groupNames = allowedPrincipals
.filter((p) => p.type === "group" && p.value)
.map((p) => p.value);
return {
loginUser,
allowedPrincipals: {
usernames,
groups: groupNames
}
};
});
if (sshHost) {
await updateMutateAsync({
sshHostId: sshHost.id,
hostname,
alias: trimmedAlias,
userCertTtl,
loginMappings: hostLoginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser,
allowedPrincipals: {
usernames: allowedPrincipals
}
}))
loginMappings: transformedLoginMappings
});
} else {
await createMutateAsync({
@@ -178,12 +209,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
hostname,
alias: trimmedAlias,
userCertTtl,
loginMappings: hostLoginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser,
allowedPrincipals: {
usernames: allowedPrincipals
}
}))
loginMappings: transformedLoginMappings
});
}
@@ -210,6 +236,15 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
}));
};
const isPrincipalDuplicate = (
mappingIndex: number,
principalType: string,
principalValue: string
) => {
const principals = getValues(`loginMappings.${mappingIndex}.allowedPrincipals`) || [];
return principals.some((p) => p.type === principalType && p.value === principalValue);
};
return (
<Modal
isOpen={popUp?.sshHost?.isOpen}
@@ -275,7 +310,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
const newIndex = loginMappingsFormFields.fields.length;
loginMappingsFormFields.append({
loginUser: "",
allowedPrincipals: [""],
allowedPrincipals: [],
source: LoginMappingSource.HOST
});
setExpandedMappings((prev) => ({
@@ -397,7 +432,10 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
onClick={() => {
const current =
getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
setValue(`loginMappings.${i}.allowedPrincipals`, [
...current,
{ type: "user", value: "" }
]);
}}
>
Add Principal
@@ -409,50 +447,82 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
name={`loginMappings.${i}.allowedPrincipals`}
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
<div className="flex flex-col space-y-2">
{(value.length === 0 ? [""] : value).map(
(principal: string, principalIndex: number) => (
<div
key={`${metadataFieldId}-principal-${principal}`}
className="flex items-center space-x-2"
>
<div className="flex-1">
<Select
value={principal}
onValueChange={(newValue) => {
if (
loginMappingsFormFields.fields[i].source ===
LoginMappingSource.HOST_GROUP
)
return;
if (value.includes(newValue)) {
createNotification({
text: "This principal is already added",
type: "error"
});
return;
}
const newPrincipals = [...value];
newPrincipals[principalIndex] = newValue;
onChange(newPrincipals);
}}
placeholder="Select a member"
className="w-full"
isDisabled={
{value.map((principal, principalIndex) => (
<div
key={`principal-${i + 1}-${principalIndex + 1}-${principal.type}`}
className="flex items-center space-x-2"
>
<div className="mr-2">
<Select
className="w-24"
value={principal.type}
onValueChange={(newType) => {
const newPrincipals = [...value];
newPrincipals[principalIndex] = {
type: newType as "user" | "group",
value: ""
};
onChange(newPrincipals);
}}
isDisabled={
loginMappingsFormFields.fields[i].source ===
LoginMappingSource.HOST_GROUP
}
>
<SelectItem value="user">User</SelectItem>
<SelectItem value="group">Group</SelectItem>
</Select>
</div>
<div className="flex-1">
<Select
value={principal.value}
onValueChange={(newValue) => {
if (
loginMappingsFormFields.fields[i].source ===
LoginMappingSource.HOST_GROUP
)
return;
if (isPrincipalDuplicate(i, principal.type, newValue)) {
createNotification({
text: `This ${principal.type} is already added`,
type: "error"
});
return;
}
>
{members.map((member) => (
<SelectItem
key={member.user.id}
value={member.user.username}
>
{member.user.username}
</SelectItem>
))}
</Select>
</div>
const newPrincipals = [...value];
newPrincipals[principalIndex] = {
type: principal.type,
value: newValue
};
onChange(newPrincipals);
}}
placeholder={`Select a ${principal.type}`}
className="w-full"
isDisabled={
loginMappingsFormFields.fields[i].source ===
LoginMappingSource.HOST_GROUP
}
>
{principal.type === "user"
? members.map((member) => (
<SelectItem
key={member.user.id}
value={member.user.username}
>
{member.user.username}
</SelectItem>
))
: groups.map((group) => (
<SelectItem
key={group.group.slug}
value={group.group.slug}
>
{group.group.slug}
</SelectItem>
))}
</Select>
</div>
<div className="flex w-10 justify-center">
<IconButton
size="sm"
ariaLabel="delete principal"
@@ -468,7 +538,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
const newPrincipals = value.filter(
(_, idx) => idx !== principalIndex
);
onChange(newPrincipals);
onChange([...newPrincipals]);
}}
isDisabled={
loginMappingsFormFields.fields[i].source ===
@@ -478,8 +548,8 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
)
)}
</div>
))}
{error && <span className="text-sm text-red-500">{error.message}</span>}
</div>
)}
@@ -42,7 +42,7 @@ export const SshHostsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Hosts</p>
<div className="flex w-full justify-end">
<div className="flex justify-end">
<a
target="_blank"
rel="noopener noreferrer"
@@ -3,7 +3,9 @@ import {
faEllipsis,
faPencil,
faServer,
faTrash
faTrash,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import FileSaver from "file-saver";
@@ -12,6 +14,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -101,43 +104,53 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
const hostLoginUserToPrincipals = hostMappings.reduce(
(acc, { loginUser, allowedPrincipals }) => {
acc[loginUser] = new Set(allowedPrincipals.usernames);
acc[loginUser] = {
users: new Set(allowedPrincipals.usernames),
groups: new Set(allowedPrincipals.groups)
};
return acc;
},
{} as Record<string, Set<string>>
{} as Record<string, { users: Set<string>; groups: Set<string> }>
);
const entriesFromHost = hostMappings.map(
({ loginUser, allowedPrincipals }) => ({
loginUser,
source: LoginMappingSource.HOST,
usernames: allowedPrincipals.usernames
users: allowedPrincipals.usernames,
groups: allowedPrincipals.groups
})
);
const entriesFromGroup = groupMappings
.map(({ loginUser, allowedPrincipals }) => {
const existing = hostLoginUserToPrincipals[loginUser] || new Set();
const filteredUsernames = allowedPrincipals.usernames.filter(
(u) => !existing.has(u)
const existing = hostLoginUserToPrincipals[loginUser] || {};
const filteredUsernames = allowedPrincipals.usernames?.filter(
(u) => !existing.users?.has(u)
);
return filteredUsernames.length > 0
const filteredGroups = allowedPrincipals.groups?.filter(
(g) => !existing.groups?.has(g)
);
return ((filteredGroups?.length || filteredUsernames?.length) ?? 0) >
0
? {
loginUser,
source: LoginMappingSource.HOST_GROUP,
usernames: filteredUsernames
users: filteredUsernames,
groups: filteredGroups
}
: null;
})
.filter(Boolean) as {
loginUser: string;
source: LoginMappingSource;
usernames: string[];
users: string[];
groups: string[];
}[];
return [...entriesFromHost, ...entriesFromGroup]
.sort((a, b) => a.loginUser.localeCompare(b.loginUser))
.map(({ loginUser, usernames, source }) => (
.map(({ loginUser, users, groups, source }) => (
<div key={`${host.id}-${loginUser}-${source}`} className="mb-2">
<div className="text-mineshaft-200">
{loginUser}
@@ -147,12 +160,40 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
</span>
)}
</div>
{usernames.map((username) => (
{users?.map((username) => (
<div
key={`${host.id}-${loginUser}-${source}-${username}`}
className="ml-4"
className="flex items-center gap-2"
>
└─ {username}
<div className="flex items-center">
<span className="text-gray-400">└─</span>
</div>
<div className="flex items-center gap-1.5">
<FontAwesomeIcon
icon={faUser}
className="text-xs text-yellow/80"
/>
<span>{username}</span>
<Badge variant="primary">user</Badge>
</div>
</div>
))}
{groups?.map((group) => (
<div
key={`${host.id}-${loginUser}-${source}-${group}`}
className="flex items-center gap-2"
>
<div className="flex items-center">
<span className="text-gray-400">└─</span>
</div>
<div className="flex items-center gap-1.5">
<FontAwesomeIcon
icon={faUsers}
className="text-xs text-green/80"
/>
<span>{group}</span>
<Badge variant="success">group</Badge>
</div>
</div>
))}
</div>