Minor cleans for consistency

This commit is contained in:
Tuan Dang
2025-04-10 12:19:37 -07:00
parent 81331ec4d1
commit 8522420e7f
10 changed files with 93 additions and 75 deletions
+1 -1
View File
@@ -269,7 +269,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
description: "Issue SSH certificate for user", description: "Issue SSH certificate for user",
params: z.object({ params: z.object({
@@ -11,7 +11,7 @@ export type TSshHostDALFactory = ReturnType<typeof sshHostDALFactory>;
export const sshHostDALFactory = (db: TDbClient) => { export const sshHostDALFactory = (db: TDbClient) => {
const sshHostOrm = ormify(db, TableName.SshHost); const sshHostOrm = ormify(db, TableName.SshHost);
const findSshHostsWithPrincipalsAcrossProjects = async (projectIds: string[], userId: string, tx?: Knex) => { const findUserAccessibleSshHosts = async (projectIds: string[], userId: string, tx?: Knex) => {
try { try {
const user = await (tx || db.replicaNode())(TableName.Users).where({ id: userId }).select("username").first(); const user = await (tx || db.replicaNode())(TableName.Users).where({ id: userId }).select("username").first();
@@ -26,6 +26,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
`${TableName.SshHostLoginUser}.id`, `${TableName.SshHostLoginUser}.id`,
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId` `${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
) )
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SshHostLoginUserMapping}.userId`)
.whereIn(`${TableName.SshHost}.projectId`, projectIds) .whereIn(`${TableName.SshHost}.projectId`, projectIds)
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId) .andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
.select( .select(
@@ -186,7 +187,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
return { return {
...sshHostOrm, ...sshHostOrm,
findSshHostsWithLoginMappings, findSshHostsWithLoginMappings,
findSshHostsWithPrincipalsAcrossProjects, findUserAccessibleSshHosts,
findSshHostByIdWithLoginMappings findSshHostByIdWithLoginMappings
}; };
}; };
@@ -13,8 +13,8 @@ export const sanitizedSshHost = SshHostsSchema.pick({
}); });
export const loginMappingSchema = z.object({ export const loginMappingSchema = z.object({
loginUser: z.string(), loginUser: z.string().trim(),
allowedPrincipals: z.object({ allowedPrincipals: z.object({
usernames: z.array(z.string()) usernames: z.array(z.string().trim()).transform((usernames) => Array.from(new Set(usernames)))
}) })
}); });
@@ -40,7 +40,7 @@ type TSshHostServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "findById" | "find">; userDAL: Pick<TUserDALFactory, "findById" | "find">;
projectDAL: Pick<TProjectDALFactory, "find">; projectDAL: Pick<TProjectDALFactory, "find">;
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">; projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">; sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findOne">;
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">; sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">; sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">; sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
@@ -53,7 +53,7 @@ type TSshHostServiceFactoryDep = {
| "deleteById" | "deleteById"
| "findOne" | "findOne"
| "findSshHostByIdWithLoginMappings" | "findSshHostByIdWithLoginMappings"
| "findSshHostsWithPrincipalsAcrossProjects" | "findUserAccessibleSshHosts"
>; >;
sshHostLoginUserDAL: TSshHostLoginUserDALFactory; sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory; sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
@@ -105,7 +105,7 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH actionProjectType: ActionProjectType.SSH
}); });
const projectHosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects([project.id], actorId); // TODO: consider fn rename const projectHosts = await sshHostDAL.findUserAccessibleSshHosts([project.id], actorId);
allowedHosts.push(...projectHosts); allowedHosts.push(...projectHosts);
} catch { } catch {
@@ -159,9 +159,15 @@ export const sshHostServiceFactory = ({
throw new BadRequestError({ message: `Missing ${label.toLowerCase()} SSH CA` }); throw new BadRequestError({ message: `Missing ${label.toLowerCase()} SSH CA` });
} }
const ca = await sshCertificateAuthorityDAL.findById(finalId); const ca = await sshCertificateAuthorityDAL.findOne({
id: finalId,
projectId
});
if (!ca) { if (!ca) {
throw new BadRequestError({ message: `${label} SSH CA with ID '${finalId}' not found` }); throw new BadRequestError({
message: `${label} SSH CA with ID '${finalId}' not found in project '${projectId}'`
});
} }
return ca.id; return ca.id;
@@ -216,6 +222,7 @@ export const sshHostServiceFactory = ({
tx tx
); );
// (dangtony98): room to optimize
for await (const { loginUser, allowedPrincipals } of loginMappings) { for await (const { loginUser, allowedPrincipals } of loginMappings) {
const sshHostLoginUser = await sshHostLoginUserDAL.create( const sshHostLoginUser = await sshHostLoginUserDAL.create(
{ {
@@ -225,6 +232,7 @@ export const sshHostServiceFactory = ({
tx tx
); );
if (allowedPrincipals.usernames.length > 0) {
const users = await userDAL.find( const users = await userDAL.find(
{ {
$in: { $in: {
@@ -234,7 +242,18 @@ export const sshHostServiceFactory = ({
{ tx } { tx }
); );
const foundUsernames = new Set(users.map((u) => u.username));
for (const uname of allowedPrincipals.usernames) {
if (!foundUsernames.has(uname)) {
throw new BadRequestError({
message: `Invalid username: ${uname}`
});
}
}
for await (const user of users) { for await (const user of users) {
// check that each user has access to the SSH project
await permissionService.getUserProjectPermission({ await permissionService.getUserProjectPermission({
userId: user.id, userId: user.id,
projectId, projectId,
@@ -252,6 +271,7 @@ export const sshHostServiceFactory = ({
tx tx
); );
} }
}
const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx); const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx);
if (!newSshHostWithLoginMappings) { if (!newSshHostWithLoginMappings) {
@@ -317,10 +337,7 @@ export const sshHostServiceFactory = ({
tx tx
); );
if (allowedPrincipals.usernames.length === 0) { if (allowedPrincipals.usernames.length > 0) {
continue; // or maybe insert no mappings and just skip validation
}
const users = await userDAL.find( const users = await userDAL.find(
{ {
$in: { $in: {
@@ -360,6 +377,7 @@ export const sshHostServiceFactory = ({
} }
} }
} }
}
const updatedHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId, tx); const updatedHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId, tx);
if (!updatedHostWithLoginMappings) { if (!updatedHostWithLoginMappings) {
+2 -2
View File
@@ -1328,7 +1328,7 @@ export const SSH_HOSTS = {
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')", loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
allowedPrincipals: "A list of allowed principals that can log in as the login user.", allowedPrincipals: "A list of allowed principals that can log in as the login user.",
loginMappings: loginMappings:
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals.", "A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project.",
userSshCaId: userSshCaId:
"The ID of the SSH CA to use for user certificates. If not specified, the default user SSH CA will be used if it exists.", "The ID of the SSH CA to use for user certificates. If not specified, the default user SSH CA will be used if it exists.",
hostSshCaId: hostSshCaId:
@@ -1342,7 +1342,7 @@ export const SSH_HOSTS = {
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')", loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
allowedPrincipals: "A list of allowed principals that can log in as the login user.", allowedPrincipals: "A list of allowed principals that can log in as the login user.",
loginMappings: loginMappings:
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals." "A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project."
}, },
DELETE: { DELETE: {
sshHostId: "The ID of the SSH host to delete." sshHostId: "The ID of the SSH host to delete."
+1 -1
View File
@@ -12,7 +12,7 @@ require (
github.com/fatih/semgroup v1.2.0 github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.8.0 github.com/gitleaks/go-gitdiff v0.8.0
github.com/h2non/filetype v1.1.3 github.com/h2non/filetype v1.1.3
github.com/infisical/go-sdk v0.5.5 github.com/infisical/go-sdk v0.5.7
github.com/infisical/infisical-kmip v0.3.5 github.com/infisical/infisical-kmip v0.3.5
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
+2 -2
View File
@@ -277,8 +277,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/infisical/go-sdk v0.5.5 h1:A0KfqZvRWScjVj19dbh2uHH4wSsElj5cTAgcT1Adezs= github.com/infisical/go-sdk v0.5.7 h1:q/gQGmbTvpCJYlhE3pyyWqifdQeM6yJyiYRIlXK4nXw=
github.com/infisical/go-sdk v0.5.5/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= github.com/infisical/go-sdk v0.5.7/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE= github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
+2 -3
View File
@@ -103,10 +103,9 @@ we will register a remote host with Infisical through a [machine identity](/docu
4.2. On the registered host in the **Hosts** tab, click **Edit SSH Host** and add a login mapping for the user(s) you added in step 4.1. 4.2. On the registered host in the **Hosts** tab, click **Edit SSH Host** and add a login mapping for the user(s) you added in step 4.1.
The login mapping dictates what user(s) will be allowed access to the remote host and under a specific login user; in the allowed principals, The login mapping dictates what user(s) will be allowed access to the remote host and under a specific login user; in the allowed principals,
you should input a comma-separated list of usernames of users part of the Infisical SSH project that will be allowed to login to the remote host as the login user. you should select user(s) part of the Infisical SSH project that will be allowed to login to the remote host as the login user.
For instance, if you add a mapping with the login user `ec2-user` with allowed principals of `[email protected]` and `[email protected]` For instance, if you add a mapping with the login user `ec2-user` to some users John and Alice in Infisical, then they will be allowed to login to the remote host as `ec2-user` which is a system user that
then both users with the username `[email protected]` and `[email protected]` will be allowed to login to the remote host as `ec2-user` which is a system user that
exists on the remote host. exists on the remote host.
![ssh host mappings](/images/platform/ssh/v2/ssh-host-login-mappings.png) ![ssh host mappings](/images/platform/ssh/v2/ssh-host-login-mappings.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

After

Width:  |  Height:  |  Size: 643 KiB

@@ -10,7 +10,7 @@ export const SshHostsPage = () => {
return ( return (
<> <>
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title> <title>{t("common.head-title", { title: "SSH" })}</title>
</Helmet> </Helmet>
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">