diff --git a/backend/src/ee/routes/v1/ssh-host-router.ts b/backend/src/ee/routes/v1/ssh-host-router.ts index 5c17765aa..4193b80f2 100644 --- a/backend/src/ee/routes/v1/ssh-host-router.ts +++ b/backend/src/ee/routes/v1/ssh-host-router.ts @@ -269,7 +269,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { config: { rateLimit: writeLimit }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT]), schema: { description: "Issue SSH certificate for user", params: z.object({ diff --git a/backend/src/ee/services/ssh-host/ssh-host-dal.ts b/backend/src/ee/services/ssh-host/ssh-host-dal.ts index 7232a4f07..4baeca503 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-dal.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-dal.ts @@ -11,7 +11,7 @@ export type TSshHostDALFactory = ReturnType; export const sshHostDALFactory = (db: TDbClient) => { 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 { 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.SshHostLoginUserMapping}.sshHostLoginUserId` ) + .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SshHostLoginUserMapping}.userId`) .whereIn(`${TableName.SshHost}.projectId`, projectIds) .andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId) .select( @@ -186,7 +187,7 @@ export const sshHostDALFactory = (db: TDbClient) => { return { ...sshHostOrm, findSshHostsWithLoginMappings, - findSshHostsWithPrincipalsAcrossProjects, + findUserAccessibleSshHosts, findSshHostByIdWithLoginMappings }; }; diff --git a/backend/src/ee/services/ssh-host/ssh-host-schema.ts b/backend/src/ee/services/ssh-host/ssh-host-schema.ts index 75f7a590c..4eeb90881 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-schema.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-schema.ts @@ -13,8 +13,8 @@ export const sanitizedSshHost = SshHostsSchema.pick({ }); export const loginMappingSchema = z.object({ - loginUser: z.string(), + loginUser: z.string().trim(), allowedPrincipals: z.object({ - usernames: z.array(z.string()) + usernames: z.array(z.string().trim()).transform((usernames) => Array.from(new Set(usernames))) }) }); diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index 92ec07f9c..91d77cf5f 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -40,7 +40,7 @@ type TSshHostServiceFactoryDep = { userDAL: Pick; projectDAL: Pick; projectSshConfigDAL: Pick; - sshCertificateAuthorityDAL: Pick; + sshCertificateAuthorityDAL: Pick; sshCertificateAuthoritySecretDAL: Pick; sshCertificateDAL: Pick; sshCertificateBodyDAL: Pick; @@ -53,7 +53,7 @@ type TSshHostServiceFactoryDep = { | "deleteById" | "findOne" | "findSshHostByIdWithLoginMappings" - | "findSshHostsWithPrincipalsAcrossProjects" + | "findUserAccessibleSshHosts" >; sshHostLoginUserDAL: TSshHostLoginUserDALFactory; sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory; @@ -105,7 +105,7 @@ export const sshHostServiceFactory = ({ 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); } catch { @@ -159,9 +159,15 @@ export const sshHostServiceFactory = ({ 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) { - 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; @@ -216,6 +222,7 @@ export const sshHostServiceFactory = ({ tx ); + // (dangtony98): room to optimize for await (const { loginUser, allowedPrincipals } of loginMappings) { const sshHostLoginUser = await sshHostLoginUserDAL.create( { @@ -225,32 +232,45 @@ export const sshHostServiceFactory = ({ tx ); - const users = await userDAL.find( - { - $in: { - username: allowedPrincipals.usernames + if (allowedPrincipals.usernames.length > 0) { + const users = await userDAL.find( + { + $in: { + username: allowedPrincipals.usernames + } + }, + { 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}` + }); } - }, - { tx } - ); + } - for await (const user of users) { - await permissionService.getUserProjectPermission({ - userId: user.id, - projectId, - authMethod: actorAuthMethod, - userOrgId: actorOrgId, - actionProjectType: ActionProjectType.SSH - }); + for await (const user of users) { + // check that each user has access to the SSH project + await permissionService.getUserProjectPermission({ + userId: user.id, + projectId, + authMethod: actorAuthMethod, + userOrgId: actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + } + + await sshHostLoginUserMappingDAL.insertMany( + users.map((user) => ({ + sshHostLoginUserId: sshHostLoginUser.id, + userId: user.id + })), + tx + ); } - - await sshHostLoginUserMappingDAL.insertMany( - users.map((user) => ({ - sshHostLoginUserId: sshHostLoginUser.id, - userId: user.id - })), - tx - ); } const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx); @@ -317,46 +337,44 @@ export const sshHostServiceFactory = ({ tx ); - if (allowedPrincipals.usernames.length === 0) { - continue; // or maybe insert no mappings and just skip validation - } + if (allowedPrincipals.usernames.length > 0) { + const users = await userDAL.find( + { + $in: { + username: allowedPrincipals.usernames + } + }, + { tx } + ); - const users = await userDAL.find( - { - $in: { - username: allowedPrincipals.usernames + 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}` + }); } - }, - { 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) { + await permissionService.getUserProjectPermission({ + userId: user.id, + projectId: host.projectId, + authMethod: actorAuthMethod, + userOrgId: actorOrgId, + actionProjectType: ActionProjectType.SSH }); } - } - for await (const user of users) { - await permissionService.getUserProjectPermission({ - userId: user.id, - projectId: host.projectId, - authMethod: actorAuthMethod, - userOrgId: actorOrgId, - actionProjectType: ActionProjectType.SSH - }); + await sshHostLoginUserMappingDAL.insertMany( + users.map((user) => ({ + sshHostLoginUserId: sshHostLoginUser.id, + userId: user.id + })), + tx + ); } - - await sshHostLoginUserMappingDAL.insertMany( - users.map((user) => ({ - sshHostLoginUserId: sshHostLoginUser.id, - userId: user.id - })), - tx - ); } } } diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 1746e58e0..95dbd1642 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1328,7 +1328,7 @@ export const SSH_HOSTS = { 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.", 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: "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: @@ -1342,7 +1342,7 @@ export const SSH_HOSTS = { 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.", 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: { sshHostId: "The ID of the SSH host to delete." diff --git a/cli/go.mod b/cli/go.mod index b7003a6df..6132f56c8 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -12,7 +12,7 @@ require ( github.com/fatih/semgroup v1.2.0 github.com/gitleaks/go-gitdiff v0.8.0 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/mattn/go-isatty v0.0.20 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a diff --git a/cli/go.sum b/cli/go.sum index a0d6028e5..410a61154 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -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/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= 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.5/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= +github.com/infisical/go-sdk v0.5.7 h1:q/gQGmbTvpCJYlhE3pyyWqifdQeM6yJyiYRIlXK4nXw= +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/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= diff --git a/docs/documentation/platform/ssh.mdx b/docs/documentation/platform/ssh.mdx index 6ff281612..d07c1c53b 100644 --- a/docs/documentation/platform/ssh.mdx +++ b/docs/documentation/platform/ssh.mdx @@ -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. 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 `bob@acme.com` and `alice@acme.com` - then both users with the username `bob@acme.com` and `alice@acme.com` will be allowed to login to the remote host as `ec2-user` which is a system user that + 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 exists on the remote host. ![ssh host mappings](/images/platform/ssh/v2/ssh-host-login-mappings.png) diff --git a/docs/images/platform/ssh/v2/ssh-host-login-mappings.png b/docs/images/platform/ssh/v2/ssh-host-login-mappings.png index 0d095d56b..cdc192274 100644 Binary files a/docs/images/platform/ssh/v2/ssh-host-login-mappings.png and b/docs/images/platform/ssh/v2/ssh-host-login-mappings.png differ diff --git a/frontend/src/pages/ssh/SshHostsPage/SshHostsPage.tsx b/frontend/src/pages/ssh/SshHostsPage/SshHostsPage.tsx index ff619fae9..aca76bb73 100644 --- a/frontend/src/pages/ssh/SshHostsPage/SshHostsPage.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/SshHostsPage.tsx @@ -10,7 +10,7 @@ export const SshHostsPage = () => { return ( <> - {t("common.head-title", { title: "Certificates" })} + {t("common.head-title", { title: "SSH" })}