Revise PR based on review

This commit is contained in:
Tuan Dang
2025-05-02 12:16:51 -07:00
parent 83df0850ce
commit 6a5748150a
12 changed files with 130 additions and 22 deletions

View File

@@ -4,7 +4,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
import { sanitizedSshHostGroup } from "@app/ee/services/ssh-host-group/ssh-host-group-schema";
import { EHostGroupMembershipFilter } from "@app/ee/services/ssh-host-group/ssh-host-group-types";
import { SSH_HOST_GROUPS } from "@app/lib/api-docs";
import { ApiDocsTags, SSH_HOST_GROUPS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -18,6 +18,9 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Get SSH Host Group",
params: z.object({
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.GET.sshHostGroupId)
}),
@@ -60,6 +63,8 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Create SSH Host Group",
body: z.object({
projectId: z.string().describe(SSH_HOST_GROUPS.CREATE.projectId),
@@ -107,6 +112,8 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Update SSH Host Group",
params: z.object({
sshHostGroupId: z.string().trim().describe(SSH_HOST_GROUPS.UPDATE.sshHostGroupId)
@@ -155,6 +162,9 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Delete SSH Host Group",
params: z.object({
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.DELETE.sshHostGroupId)
}),
@@ -197,6 +207,9 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Get SSH Hosts in a Host Group",
params: z.object({
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.GET.sshHostGroupId)
}),
@@ -256,6 +269,9 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Add an SSH Host to a Host Group",
params: z.object({
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.ADD_HOST.sshHostGroupId),
hostId: z.string().describe(SSH_HOST_GROUPS.ADD_HOST.hostId)
@@ -301,6 +317,9 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
description: "Remove an SSH Host from a Host Group",
params: z.object({
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.DELETE_HOST.sshHostGroupId),
hostId: z.string().describe(SSH_HOST_GROUPS.DELETE_HOST.hostId)

View File

@@ -5,7 +5,7 @@ import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certif
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
import { LoginMappingSource } from "@app/ee/services/ssh-host/ssh-host-types";
import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
import { SSH_HOSTS } from "@app/lib/api-docs";
import { ApiDocsTags, SSH_HOSTS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
@@ -22,6 +22,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
response: {
200: z.array(
sanitizedSshHost.extend({
@@ -54,6 +56,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
params: z.object({
sshHostId: z.string().describe(SSH_HOSTS.GET.sshHostId)
}),
@@ -100,7 +104,9 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit
},
schema: {
description: "Add an SSH Host",
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Register SSH Host",
body: z.object({
projectId: z.string().describe(SSH_HOSTS.CREATE.projectId),
hostname: z
@@ -176,6 +182,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Update SSH Host",
params: z.object({
sshHostId: z.string().trim().describe(SSH_HOSTS.UPDATE.sshHostId)
@@ -252,6 +260,9 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Delete SSH Host",
params: z.object({
sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId)
}),
@@ -299,6 +310,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Issue SSH certificate for user",
params: z.object({
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId)
@@ -371,6 +384,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Issue SSH certificate for host",
params: z.object({
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.sshHostId)
@@ -435,6 +450,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: publicSshCaLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Get public key of the user SSH CA linked to the host",
params: z.object({
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId)
@@ -456,6 +473,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
rateLimit: publicSshCaLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
description: "Get public key of the host SSH CA linked to the host",
params: z.object({
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId)

View File

@@ -90,15 +90,19 @@ export const sshHostDALFactory = (db: TDbClient) => {
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } =
hostRows[0];
const loginMappingGrouped = groupBy(hostRows, (r) => `${r.loginUser}|${r.source}`);
const loginMappings = Object.entries(loginMappingGrouped).map(([key]) => {
const [loginUser, source] = key.split("|");
const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, mappings]) => {
// Prefer HOST source over HOST_GROUP
const preferredMapping =
mappings.find((m) => m.source === LoginMappingSource.HOST) ||
mappings.find((m) => m.source === LoginMappingSource.HOST_GROUP);
return {
loginUser,
allowedPrincipals: {
usernames: [user.username]
},
source: source as LoginMappingSource
source: preferredMapping!.source
};
});

View File

@@ -48,6 +48,8 @@ export enum ApiDocsTags {
SshCertificates = "SSH Certificates",
SshCertificateAuthorities = "SSH Certificate Authorities",
SshCertificateTemplates = "SSH Certificate Templates",
SshHosts = "SSH Hosts",
SshHostGroups = "SSH Host Groups",
KmsKeys = "KMS Keys",
KmsEncryption = "KMS Encryption",
KmsSigning = "KMS Signing"

View File

@@ -43,6 +43,9 @@ export const useUpdateSshHostGroup = () => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId)
});
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceSshHosts(projectId)
});
queryClient.invalidateQueries({
queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId)
});
@@ -63,6 +66,9 @@ export const useDeleteSshHostGroup = () => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId)
});
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceSshHosts(projectId)
});
queryClient.invalidateQueries({
queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId)
});

View File

@@ -95,7 +95,8 @@ const Page = () => {
)}
onClick={() =>
handlePopUpOpen("deleteSshHostGroup", {
groupId: data.id
groupId: data.id,
name: data.name
})
}
disabled={!isAllowed}
@@ -123,7 +124,9 @@ const Page = () => {
<SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteSshHostGroup.isOpen}
title="Are you sure want to remove the SSH group from the project?"
title={`Are you sure want to remove the SSH group: ${
(popUp?.deleteSshHostGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteSshHostGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>

View File

@@ -88,9 +88,13 @@ export const SshHostGroupHostsSection = ({ sshHostGroupId }: Props) => {
<AddHostGroupMemberModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.removeHostFromSshHostGroup.isOpen}
title={`Are you sure want to remove the host ${
(popUp?.removeHostFromSshHostGroup?.data as { hostname: string })?.hostname || ""
} from this SSH group?`}
title={`Are you sure want to remove ${
(popUp?.removeHostFromSshHostGroup?.data as { hostname: string; alias?: string })
?.alias ||
(popUp?.removeHostFromSshHostGroup?.data as { hostname: string; alias?: string })
?.hostname ||
""
} from this host group?`}
onChange={(isOpen) => handlePopUpToggle("removeHostFromSshHostGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>

View File

@@ -1,4 +1,4 @@
import { faServer, faUserMinus } from "@fortawesome/free-solid-svg-icons";
import { faServer, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
@@ -68,13 +68,14 @@ export const SshHostGroupHostsTable = ({ sshHostGroupId, handlePopUpOpen }: Prop
onClick={() =>
handlePopUpOpen("removeHostFromSshHostGroup", {
sshHostId: host.id,
alias: host.alias,
hostname: host.hostname
})
}
variant="plain"
colorSchema="danger"
>
<FontAwesomeIcon icon={faUserMinus} />
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
)}

View File

@@ -77,7 +77,7 @@ export const SshHostGroupsSection = () => {
<SshHostGroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteSshHostGroup.isOpen}
title="Are you sure you want to remove the SSH host group?"
title={`Are you sure you want to remove the SSH host group: ${popUp?.deleteSshHostGroup?.data?.name}?`}
onChange={(isOpen) => handlePopUpToggle("deleteSshHostGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>

View File

@@ -114,7 +114,33 @@ export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => {
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPencil} />}
>
Edit SSH host group
Edit Host Group
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SshHostGroups}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
navigate({
to: `/${ProjectType.SSH}/$projectId/ssh-host-groups/$sshHostGroupId` as const,
params: {
projectId: currentWorkspace.id,
sshHostGroupId: group.id
}
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faServer} />}
>
Manage Hosts
</DropdownMenuItem>
)}
</ProjectPermissionCan>
@@ -130,13 +156,14 @@ export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => {
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteSshHostGroup", {
sshHostGroupId: group.id
sshHostGroupId: group.id,
name: group.name
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete SSH host group
Delete Host Group
</DropdownMenuItem>
)}
</ProjectPermissionCan>

View File

@@ -362,7 +362,10 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
const newValue = e.target.value;
const loginMappings = getValues("loginMappings");
const isDuplicate = loginMappings.some(
(mapping, index) => index !== i && mapping.loginUser === newValue
(mapping, index) =>
index !== i &&
mapping.loginUser === newValue &&
mapping.source === LoginMappingSource.HOST
);
if (isDuplicate) {

View File

@@ -91,7 +91,27 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
{host.loginMappings.length === 0 ? (
<span className="italic text-mineshaft-400">None</span>
) : (
host.loginMappings.map(({ loginUser, allowedPrincipals, source }) => (
Object.entries(
host.loginMappings.reduce(
(acc, mapping) => {
const { loginUser, source } = mapping;
const existing = acc[loginUser];
if (!existing) {
acc[loginUser] = mapping;
} else if (
existing.source === LoginMappingSource.HOST_GROUP &&
source === LoginMappingSource.HOST
) {
// Prefer HOST over HOST_GROUP
acc[loginUser] = mapping;
}
return acc;
},
{} as Record<string, (typeof host.loginMappings)[number]>
)
).map(([loginUser, { allowedPrincipals, source }]) => (
<div key={`${host.id}-${loginUser}`} className="mb-2">
<div className="text-mineshaft-200">
{loginUser}
@@ -147,7 +167,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPencil} />}
>
Edit SSH host
Edit Host
</DropdownMenuItem>
)}
</ProjectPermissionCan>
@@ -169,7 +189,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete SSH host
Delete Host
</DropdownMenuItem>
)}
</ProjectPermissionCan>