mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 13:22:08 +00:00
Add groups to ssh hosts allowed principals
This commit is contained in:
@@ -9,6 +9,7 @@ export type TSshHost = {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
groups: string[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
@@ -23,6 +24,7 @@ export type TCreateSshHostDTO = {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
groups: string[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
@@ -37,6 +39,7 @@ export type TUpdateSshHostDTO = {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
groups: string[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
useCreateSshHost,
|
||||
useGetSshHostById,
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceGroups,
|
||||
useListWorkspaceSshHosts,
|
||||
useUpdateSshHost
|
||||
} from "@app/hooks/api";
|
||||
@@ -48,7 +49,14 @@ const schema = z
|
||||
loginMappings: z
|
||||
.object({
|
||||
loginUser: z.string().trim().min(1),
|
||||
allowedPrincipals: z.array(z.string().trim()).default([])
|
||||
principals: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(["user", "group"]),
|
||||
value: z.string().trim()
|
||||
})
|
||||
)
|
||||
.default([])
|
||||
})
|
||||
.array()
|
||||
.default([])
|
||||
@@ -62,6 +70,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(
|
||||
@@ -101,7 +110,16 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
userCertTtl: sshHost.userCertTtl,
|
||||
loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: allowedPrincipals.usernames
|
||||
principals: [
|
||||
...(allowedPrincipals.usernames || []).map((username) => ({
|
||||
type: "user" as const,
|
||||
value: username
|
||||
})),
|
||||
...(allowedPrincipals.groups || []).map((group) => ({
|
||||
type: "group" as const,
|
||||
value: group
|
||||
}))
|
||||
]
|
||||
}))
|
||||
});
|
||||
|
||||
@@ -151,18 +169,28 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const transformedLoginMappings = loginMappings.map(({ loginUser, principals }) => {
|
||||
const usernames = principals.filter((p) => p.type === "user").map((p) => p.value);
|
||||
|
||||
const groupNames = principals.filter((p) => p.type === "group").map((p) => p.value);
|
||||
|
||||
return {
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames,
|
||||
groups: groupNames
|
||||
}
|
||||
};
|
||||
});
|
||||
console.log(transformedLoginMappings);
|
||||
|
||||
if (sshHost) {
|
||||
await updateMutateAsync({
|
||||
sshHostId: sshHost.id,
|
||||
hostname,
|
||||
alias: trimmedAlias,
|
||||
userCertTtl,
|
||||
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: allowedPrincipals
|
||||
}
|
||||
}))
|
||||
loginMappings: transformedLoginMappings
|
||||
});
|
||||
} else {
|
||||
await createMutateAsync({
|
||||
@@ -170,12 +198,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
hostname,
|
||||
alias: trimmedAlias,
|
||||
userCertTtl,
|
||||
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: allowedPrincipals
|
||||
}
|
||||
}))
|
||||
loginMappings: transformedLoginMappings
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,6 +225,15 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const isPrincipalDuplicate = (
|
||||
mappingIndex: number,
|
||||
principalType: string,
|
||||
principalValue: string
|
||||
) => {
|
||||
const principals = getValues(`loginMappings.${mappingIndex}.principals`) || [];
|
||||
return principals.some((p) => p.type === principalType && p.value === principalValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.sshHost?.isOpen}
|
||||
@@ -265,7 +297,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const newIndex = loginMappingsFormFields.fields.length;
|
||||
loginMappingsFormFields.append({ loginUser: "", allowedPrincipals: [""] });
|
||||
loginMappingsFormFields.append({ loginUser: "", principals: [] });
|
||||
setExpandedMappings((prev) => ({
|
||||
...prev,
|
||||
[newIndex]: true
|
||||
@@ -360,8 +392,11 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const current = getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
|
||||
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
|
||||
const current = getValues(`loginMappings.${i}.principals`) ?? [];
|
||||
setValue(`loginMappings.${i}.principals`, [
|
||||
...current,
|
||||
{ type: "user", value: "" }
|
||||
]);
|
||||
}}
|
||||
>
|
||||
Add Principal
|
||||
@@ -369,43 +404,72 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`loginMappings.${i}.allowedPrincipals`}
|
||||
name={`loginMappings.${i}.principals`}
|
||||
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={`${metadataFieldId}-principal-${principalIndex + 1}`}
|
||||
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,
|
||||
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"
|
||||
@@ -421,8 +485,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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
@@ -91,13 +94,46 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
||||
<span className="italic text-mineshaft-400">None</span>
|
||||
) : (
|
||||
host.loginMappings.map(({ loginUser, allowedPrincipals }) => (
|
||||
<div key={`${host.id}-${loginUser}`} className="mb-2">
|
||||
<div className="text-mineshaft-200">{loginUser}</div>
|
||||
{allowedPrincipals.usernames.map((username) => (
|
||||
<div key={`${host.id}-${loginUser}-${username}`} className="ml-4">
|
||||
└─ {username}
|
||||
</div>
|
||||
))}
|
||||
<div key={`${host.id}-${loginUser}`} className="mb-3">
|
||||
<div className="mb-1 text-mineshaft-200">{loginUser}</div>
|
||||
<div className="ml-4 flex flex-col gap-1">
|
||||
{allowedPrincipals.usernames.map((username) => (
|
||||
<div
|
||||
key={`${host.id}-${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((group) => (
|
||||
<div
|
||||
key={`${host.id}-${loginUser}-${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>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user