Revise PR based on coderabbit, greptile review

This commit is contained in:
Tuan Dang
2025-04-30 19:39:02 -07:00
parent 7811178261
commit ae43435509
19 changed files with 66 additions and 77 deletions
@@ -3,9 +3,6 @@ import { Knex } from "knex";
import { TableName } from "../schemas"; import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
// TODO: can attach default SSH login mappings to a host group
// TODO: can attach default user SSH CA and host SSH CA (convert existing project level ones to a group)
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.SshHostGroup))) { if (!(await knex.schema.hasTable(TableName.SshHostGroup))) {
await knex.schema.createTable(TableName.SshHostGroup, (t) => { await knex.schema.createTable(TableName.SshHostGroup, (t) => {
@@ -36,6 +33,7 @@ export async function up(knex: Knex): Promise<void> {
if (!hasGroupColumn) { if (!hasGroupColumn) {
await knex.schema.alterTable(TableName.SshHostLoginUser, (t) => { await knex.schema.alterTable(TableName.SshHostLoginUser, (t) => {
t.uuid("sshHostGroupId").nullable(); t.uuid("sshHostGroupId").nullable();
t.foreign("sshHostGroupId").references("id").inTable(TableName.SshHostGroup).onDelete("CASCADE");
t.uuid("sshHostId").nullable().alter(); t.uuid("sshHostId").nullable().alter();
}); });
} }
+1 -1
View File
@@ -27,7 +27,7 @@ export const ProjectsSchema = z.object({
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
type: z.string(), type: z.string(),
enforceCapitalization: z.boolean().default(false), enforceCapitalization: z.boolean().default(false),
hasDeleteProtection: z.boolean().default(false).nullable().optional() hasDeleteProtection: z.boolean().default(true).nullable().optional()
}); });
export type TProjects = z.infer<typeof ProjectsSchema>; export type TProjects = z.infer<typeof ProjectsSchema>;
@@ -63,7 +63,7 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
description: "Create SSH Host Group", description: "Create SSH Host Group",
body: z.object({ body: z.object({
projectId: z.string().describe(SSH_HOST_GROUPS.CREATE.projectId), projectId: z.string().describe(SSH_HOST_GROUPS.CREATE.projectId),
name: slugSchema({ min: 0, max: 64, field: "name" }).describe(SSH_HOST_GROUPS.CREATE.name), name: slugSchema({ min: 1, max: 64, field: "name" }).describe(SSH_HOST_GROUPS.CREATE.name),
loginMappings: z.array(loginMappingSchema).default([]).describe(SSH_HOST_GROUPS.CREATE.loginMappings) loginMappings: z.array(loginMappingSchema).default([]).describe(SSH_HOST_GROUPS.CREATE.loginMappings)
}), }),
response: { response: {
@@ -107,12 +107,12 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Update SSH Host", description: "Update SSH Host Group",
params: z.object({ params: z.object({
sshHostGroupId: z.string().trim().describe(SSH_HOST_GROUPS.UPDATE.sshHostGroupId) sshHostGroupId: z.string().trim().describe(SSH_HOST_GROUPS.UPDATE.sshHostGroupId)
}), }),
body: z.object({ body: z.object({
name: slugSchema({ min: 0, max: 64, field: "name" }).describe(SSH_HOST_GROUPS.UPDATE.name).optional(), name: slugSchema({ min: 1, max: 64, field: "name" }).describe(SSH_HOST_GROUPS.UPDATE.name).optional(),
loginMappings: z.array(loginMappingSchema).optional().describe(SSH_HOST_GROUPS.UPDATE.loginMappings) loginMappings: z.array(loginMappingSchema).optional().describe(SSH_HOST_GROUPS.UPDATE.loginMappings)
}), }),
response: { response: {
@@ -239,7 +239,8 @@ export const registerSshHostGroupRouter = async (server: FastifyZodProvider) =>
event: { event: {
type: EventType.GET_SSH_HOST_GROUP_HOSTS, type: EventType.GET_SSH_HOST_GROUP_HOSTS,
metadata: { metadata: {
sshHostGroupId: req.params.sshHostGroupId sshHostGroupId: req.params.sshHostGroupId,
name: sshHostGroup.name
} }
} }
}); });
@@ -1603,6 +1603,7 @@ interface GetSshHostGroupHostsEvent {
type: EventType.GET_SSH_HOST_GROUP_HOSTS; type: EventType.GET_SSH_HOST_GROUP_HOSTS;
metadata: { metadata: {
sshHostGroupId: string; sshHostGroupId: string;
name: string;
}; };
} }
@@ -22,7 +22,7 @@ export const getDefaultOnPremFeatures = () => {
samlSSO: false, samlSSO: false,
scim: false, scim: false,
ldap: false, ldap: false,
groups: true, groups: false,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
@@ -35,7 +35,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
oidcSSO: false, oidcSSO: false,
scim: false, scim: false,
ldap: false, ldap: false,
groups: true, groups: false,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
@@ -52,7 +52,7 @@ export type TFeatureSet = {
secretAccessInsights: false; secretAccessInsights: false;
scim: false; scim: false;
ldap: false; ldap: false;
groups: true; groups: false;
status: null; status: null;
trial_end: null; trial_end: null;
has_used_trial: true; has_used_trial: true;
@@ -2,7 +2,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn"; import { groupBy, unique } from "@app/lib/fn";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
@@ -155,7 +155,9 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
.first(); .first();
if (!sshHostGroup) { if (!sshHostGroup) {
throw new Error(`SSH host group with ID ${sshHostGroupId} not found`); throw new BadRequestError({
message: `SSH host group with ID ${sshHostGroupId} not found`
});
} }
const query = db const query = db
@@ -89,18 +89,24 @@ export const sshHostGroupServiceFactory = ({
const newSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => { const newSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
// (dangtony98): room to optimize check to ensure that // (dangtony98): room to optimize check to ensure that
// the SSH host group name is unique across the whole org // the SSH host group name is unique across the whole org
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId, tx);
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
const projects = await projectDAL.find({ const projects = await projectDAL.find(
orgId: project.orgId {
}); orgId: project.orgId
},
{ tx }
);
const existingSshHostGroup = await sshHostGroupDAL.find({ const existingSshHostGroup = await sshHostGroupDAL.find(
name, {
$in: { name,
projectId: projects.map((p) => p.id) $in: {
} projectId: projects.map((p) => p.id)
}); }
},
{ tx }
);
if (existingSshHostGroup.length) { if (existingSshHostGroup.length) {
throw new BadRequestError({ throw new BadRequestError({
@@ -307,7 +313,7 @@ export const sshHostGroupServiceFactory = ({
} }
if (sshHostGroup.projectId !== sshHost.projectId) { if (sshHostGroup.projectId !== sshHost.projectId) {
throw new NotFoundError({ throw new BadRequestError({
message: `SSH host with ID ${hostId} not found in project ${sshHostGroup.projectId}` message: `SSH host with ID ${hostId} not found in project ${sshHostGroup.projectId}`
}); });
} }
@@ -347,7 +353,7 @@ export const sshHostGroupServiceFactory = ({
} }
if (sshHostGroup.projectId !== sshHost.projectId) { if (sshHostGroup.projectId !== sshHost.projectId) {
throw new NotFoundError({ throw new BadRequestError({
message: `SSH host with ID ${hostId} not found in project ${sshHostGroup.projectId}` message: `SSH host with ID ${hostId} not found in project ${sshHostGroup.projectId}`
}); });
} }
@@ -7,6 +7,7 @@ import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecre
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { generateCacheKeyFromData } from "@app/lib/crypto/cache"; import { generateCacheKeyFromData } from "@app/lib/crypto/cache";
import { applyJitter } from "@app/lib/dates";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { import {
buildFindFilter, buildFindFilter,
@@ -22,7 +23,6 @@ import type {
TFindSecretsByFolderIdsFilter, TFindSecretsByFolderIdsFilter,
TGetSecretsDTO TGetSecretsDTO
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { applyJitter } from "@app/lib/dates";
export const SecretServiceCacheKeys = { export const SecretServiceCacheKeys = {
get productKey() { get productKey() {
@@ -9,7 +9,7 @@ description: "Learn how to organize SSH hosts into groups and manage access poli
Infisical SSH lets you configure host groups to organize and manage multiple SSH hosts with shared access configuration. Infisical SSH lets you configure host groups to organize and manage multiple SSH hosts with shared access configuration.
These host groups can be created based on environments (`development`, `staging`, `production`), geographical regions (`us-east`, `eu-west`, `ap-northeast`), or functions (`web-servers`, `database-servers`, `worker-nodes`) to streamline access management across your infrastructure. These host groups can be created based on environments (`development`, `staging`, `production`), geographical regions (`us-east`, `eu-west`, `ap-northeast`), or functions (`web-servers`, `database-servers`, `worker-nodes`) to streamline access management across your infrastructure.
Using a host group, you can define login mappings at the group level and have them be applied to all hosts assigned to that group. For example, you can specify that `john@acme.com` can login as `ubuntu` on all hosts assigned to the `production` host group. Using a host group, you can define login mappings at the group level and have them be applied to all hosts assigned to that group. For example, you can specify that `john@example.com` can login as `ubuntu` on all hosts assigned to the `production` host group.
## Workflow ## Workflow
+11 -19
View File
@@ -3,6 +3,14 @@ export enum LoginMappingSource {
HOST_GROUP = "hostGroup" HOST_GROUP = "hostGroup"
} }
export type TLoginMapping = {
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
source: LoginMappingSource;
};
export type TSshHost = { export type TSshHost = {
id: string; id: string;
projectId: string; projectId: string;
@@ -10,13 +18,7 @@ export type TSshHost = {
alias: string | null; alias: string | null;
userCertTtl: string; userCertTtl: string;
hostCertTtl: string; hostCertTtl: string;
loginMappings: { loginMappings: TLoginMapping[];
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
source: LoginMappingSource;
}[];
}; };
export type TCreateSshHostDTO = { export type TCreateSshHostDTO = {
@@ -25,12 +27,7 @@ export type TCreateSshHostDTO = {
alias?: string; alias?: string;
userCertTtl?: string; userCertTtl?: string;
hostCertTtl?: string; hostCertTtl?: string;
loginMappings: { loginMappings: TLoginMapping[];
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
}; };
export type TUpdateSshHostDTO = { export type TUpdateSshHostDTO = {
@@ -39,12 +36,7 @@ export type TUpdateSshHostDTO = {
alias?: string; alias?: string;
userCertTtl?: string; userCertTtl?: string;
hostCertTtl?: string; hostCertTtl?: string;
loginMappings?: { loginMappings?: TLoginMapping[];
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
}; };
export type TDeleteSshHostDTO = { export type TDeleteSshHostDTO = {
+5 -20
View File
@@ -1,37 +1,22 @@
import { TSshHost } from "../sshHost/types"; import { TLoginMapping, TSshHost } from "../sshHost/types";
export type TSshHostGroup = { export type TSshHostGroup = {
id: string; id: string;
projectId: string; projectId: string;
name: string; name: string;
loginMappings: { loginMappings: TLoginMapping[];
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
}; };
export type TCreateSshHostGroupDTO = { export type TCreateSshHostGroupDTO = {
projectId: string; projectId: string;
name: string; name: string;
loginMappings: { loginMappings: TLoginMapping[];
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
}; };
export type TUpdateSshHostGroupDTO = { export type TUpdateSshHostGroupDTO = {
sshHostGroupId: string; sshHostGroupId: string;
name?: string; name?: string;
loginMappings?: { loginMappings?: TLoginMapping[];
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
}; };
export type TDeleteSshHostGroupDTO = { export type TDeleteSshHostGroupDTO = {
@@ -39,7 +24,7 @@ export type TDeleteSshHostGroupDTO = {
}; };
export type TListSshHostGroupHostsResponse = { export type TListSshHostGroupHostsResponse = {
hosts: TSshHost & { joinedGroupAt: string; isPartOfGroup: boolean }[]; hosts: (TSshHost & { joinedGroupAt: string; isPartOfGroup: boolean })[];
totalCount: number; totalCount: number;
}; };
@@ -13,9 +13,9 @@ import {
TBreadcrumbFormat TBreadcrumbFormat
} from "@app/components/v2"; } from "@app/components/v2";
import { import {
useProjectPermission,
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission,
useSubscription, useSubscription,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
@@ -38,7 +38,8 @@ export const AddHostGroupMemberModal = ({ popUp, handlePopUpToggle }: Props) =>
sshHostGroupId: popUpData?.sshHostGroupId, sshHostGroupId: popUpData?.sshHostGroupId,
filter: EHostGroupMembershipFilter.NON_GROUP_MEMBERS filter: EHostGroupMembershipFilter.NON_GROUP_MEMBERS
}); });
const { mutateAsync: addHostToSshHostGroup } = useAddHostToSshHostGroup(); const { mutateAsync: addHostToSshHostGroup, isPending: isAddingHostToSshHostGroup } =
useAddHostToSshHostGroup();
const handleAddHost = async (sshHostId: string) => { const handleAddHost = async (sshHostId: string) => {
try { try {
@@ -99,11 +100,11 @@ export const AddHostGroupMemberModal = ({ popUp, handlePopUpToggle }: Props) =>
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
isLoading={isPending} isLoading={isAddingHostToSshHostGroup}
isDisabled={!isAllowed} isDisabled={!isAllowed}
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
type="submit" type="button"
onClick={() => handleAddHost(host.id)} onClick={() => handleAddHost(host.id)}
> >
Add Add
@@ -47,7 +47,7 @@ export const SshHostGroupHostsTable = ({ sshHostGroupId, handlePopUpOpen }: Prop
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isPending && <TableSkeleton columns={2} innerKey="ssh-host-group-hosts" />} {isPending && <TableSkeleton columns={4} innerKey="ssh-host-group-hosts" />}
{!isPending && {!isPending &&
data?.hosts.map((host) => { data?.hosts.map((host) => {
return ( return (
@@ -61,10 +61,10 @@ export const SshHostGroupHostsTable = ({ sshHostGroupId, handlePopUpOpen }: Prop
a={ProjectPermissionSub.SshHostGroups} a={ProjectPermissionSub.SshHostGroups}
> >
{(isAllowed) => ( {(isAllowed) => (
<Tooltip content="Remove user from group"> <Tooltip content="Remove host from group">
<IconButton <IconButton
isDisabled={!isAllowed} isDisabled={!isAllowed}
ariaLabel="Remove user from group" ariaLabel="Remove host from group"
onClick={() => onClick={() =>
handlePopUpOpen("removeHostFromSshHostGroup", { handlePopUpOpen("removeHostFromSshHostGroup", {
sshHostId: host.id, sshHostId: host.id,
@@ -34,7 +34,7 @@ type Props = {
const schema = z const schema = z
.object({ .object({
name: z.string().trim(), name: z.string().trim().min(1).max(64),
loginMappings: z loginMappings: z
.object({ .object({
loginUser: z.string().trim().min(1), loginUser: z.string().trim().min(1),
@@ -56,11 +56,14 @@ export const SshHostGroupsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Host Groups</p> <p className="text-xl font-semibold text-mineshaft-100">Host Groups</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.SshHosts}> <ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SshHostGroups}
>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="primary" colorSchema="primary"
type="submit" type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddSshHostGroupModal()} onClick={() => handleAddSshHostGroupModal()}
isDisabled={!isAllowed} isDisabled={!isAllowed}
@@ -120,7 +120,7 @@ export const SshHostGroupsTable = ({ handlePopUpOpen }: Props) => {
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SshHosts} a={ProjectPermissionSub.SshHostGroups}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem