Begin work on ssh host groups

This commit is contained in:
Tuan Dang
2025-04-29 13:39:24 -07:00
parent 6a26a11cbb
commit a9a16c9bd1
50 changed files with 2473 additions and 122 deletions
@@ -0,0 +1,85 @@
import { Knex } from "knex";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { TCreateSshLoginMappingsDTO } from "./ssh-host-types";
/**
* Create SSH login mappings for a given SSH host
* or SSH host group.
*/
export const createSshLoginMappings = async ({
sshHostId,
sshHostGroupId,
loginMappings,
sshHostLoginUserDAL,
sshHostLoginUserMappingDAL,
userDAL,
permissionService,
projectId,
actorAuthMethod,
actorOrgId,
tx: outerTx
}: TCreateSshLoginMappingsDTO) => {
const processCreation = async (tx: Knex) => {
// (dangtony98): room to optimize
for await (const { loginUser, allowedPrincipals } of loginMappings) {
const sshHostLoginUser = await sshHostLoginUserDAL.create(
// (dangtony98): should either pass in sshHostId or sshHostGroupId but not both
{
sshHostId,
sshHostGroupId,
loginUser
},
tx
);
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}`
});
}
}
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
);
}
}
};
if (outerTx) {
return processCreation(outerTx);
}
return sshHostLoginUserDAL.transaction(processCreation);
};
@@ -26,6 +26,7 @@ import {
getSshPublicKey
} from "../ssh/ssh-certificate-authority-fns";
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
import { createSshLoginMappings } from "./ssh-host-fns";
import {
TCreateSshHostDTO,
TDeleteSshHostDTO,
@@ -202,56 +203,18 @@ export const sshHostServiceFactory = ({
tx
);
// (dangtony98): room to optimize
for await (const { loginUser, allowedPrincipals } of loginMappings) {
const sshHostLoginUser = await sshHostLoginUserDAL.create(
{
sshHostId: host.id,
loginUser
},
tx
);
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}`
});
}
}
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 createSshLoginMappings({
sshHostId: host.id,
loginMappings,
sshHostLoginUserDAL,
sshHostLoginUserMappingDAL,
userDAL,
permissionService,
projectId,
actorAuthMethod,
actorOrgId,
tx
});
const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx);
if (!newSshHostWithLoginMappings) {
@@ -310,54 +273,18 @@ export const sshHostServiceFactory = ({
if (loginMappings) {
await sshHostLoginUserDAL.delete({ sshHostId: host.id }, tx);
if (loginMappings.length) {
for await (const { loginUser, allowedPrincipals } of loginMappings) {
const sshHostLoginUser = await sshHostLoginUserDAL.create(
{
sshHostId: host.id,
loginUser
},
tx
);
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}`
});
}
}
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 createSshLoginMappings({
sshHostId: host.id,
loginMappings,
sshHostLoginUserDAL,
sshHostLoginUserMappingDAL,
userDAL,
permissionService,
projectId: host.projectId,
actorAuthMethod,
actorOrgId,
tx
});
}
}
@@ -1,18 +1,27 @@
import { Knex } from "knex";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
import { TProjectPermission } from "@app/lib/types";
import { ActorAuthMethod } from "@app/services/auth/auth-type";
import { TUserDALFactory } from "@app/services/user/user-dal";
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
type LoginMapping = {
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
};
export type TCreateSshHostDTO = {
hostname: string;
alias?: string;
userCertTtl: string;
hostCertTtl: string;
loginMappings: {
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
loginMappings: LoginMapping[];
userSshCaId?: string;
hostSshCaId?: string;
} & TProjectPermission;
@@ -23,12 +32,7 @@ export type TUpdateSshHostDTO = {
alias?: string;
userCertTtl?: string;
hostCertTtl?: string;
loginMappings?: {
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
loginMappings?: LoginMapping[];
} & Omit<TProjectPermission, "projectId">;
export type TGetSshHostDTO = {
@@ -48,3 +52,19 @@ export type TIssueSshHostHostCertDTO = {
sshHostId: string;
publicKey: string;
} & Omit<TProjectPermission, "projectId">;
type BaseCreateSshLoginMappingsDTO = {
loginMappings: LoginMapping[];
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction">;
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
userDAL: Pick<TUserDALFactory, "find">;
permissionService: Pick<TPermissionServiceFactory, "getUserProjectPermission">;
projectId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
tx?: Knex;
};
export type TCreateSshLoginMappingsDTO =
| (BaseCreateSshLoginMappingsDTO & { sshHostId: string; sshHostGroupId?: undefined })
| (BaseCreateSshLoginMappingsDTO & { sshHostGroupId: string; sshHostId?: undefined });