Add list accessible ssh hosts endpoint

This commit is contained in:
Tuan Dang
2025-04-06 17:28:46 -07:00
parent c2f2a038ad
commit 715bb447e6
12 changed files with 429 additions and 70 deletions

View File

@@ -10,6 +10,51 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerSshHostRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.array(
sanitizedSshHost.extend({
loginMappings: z.array(
z.object({
loginUser: z.string(),
allowedPrincipals: z.array(z.string())
})
)
})
)
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const hosts = await server.services.sshHost.listSshHosts({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
// TODO: audit log
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: certificateTemplate.projectId,
// event: {
// type: EventType.GET_SSH_CERTIFICATE_TEMPLATE,
// metadata: {
// certificateTemplateId: certificateTemplate.id
// }
// }
// });
return hosts;
}
});
server.route({
method: "GET",
url: "/:sshHostId",
@@ -91,7 +136,9 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
})
.array()
.default([])
.describe(SSH_HOSTS.CREATE.loginMappings)
.describe(SSH_HOSTS.CREATE.loginMappings),
userSshCaId: z.string().describe(SSH_HOSTS.CREATE.userSshCaId).optional(),
hostSshCaId: z.string().describe(SSH_HOSTS.CREATE.hostSshCaId).optional()
}),
response: {
200: sanitizedSshHost.extend({
@@ -265,7 +312,6 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
});
server.route({
// TODO: consider just using the SSH issue creds endpoint
method: "POST",
url: "/:sshHostId/issue",
config: {
@@ -287,15 +333,18 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
})
}
},
handler: () => {
// const { serialNumber, signedPublicKey, privateKey, publicKey, certificateTemplate, ttl, keyId } =
// await server.services.sshCertificateAuthority.issueSshCreds({
// actor: req.permission.type,
// actorId: req.permission.id,
// actorAuthMethod: req.permission.authMethod,
// actorOrgId: req.permission.orgId,
// ...req.body
// });
handler: async (req) => {
const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm } =
await server.services.sshHost.issueSshCredsFromHost({
sshHostId: req.params.sshHostId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
// TODO: add audit log
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// orgId: req.permission.orgId,
@@ -320,19 +369,13 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
// ...req.auditLogInfo
// }
// });
// return {
// serialNumber,
// signedKey: signedPublicKey,
// privateKey,
// publicKey,
// keyAlgorithm: req.body.keyAlgorithm
// };
return {
serialNumber: "",
signedKey: "",
privateKey: "",
publicKey: "",
keyAlgorithm: SshCertKeyAlgorithm.ED25519
serialNumber,
signedKey: signedPublicKey,
privateKey,
publicKey,
keyAlgorithm
};
}
});

View File

@@ -11,6 +11,69 @@ export type TSshHostDALFactory = ReturnType<typeof sshHostDALFactory>;
export const sshHostDALFactory = (db: TDbClient) => {
const sshHostOrm = ormify(db, TableName.SshHost);
const findSshHostsWithPrincipalsAcrossProjects = async (projectIds: string[], principals: string[], tx?: Knex) => {
try {
const matchingSshHosts = await (tx || db.replicaNode())(TableName.SshHost)
.leftJoin(
TableName.SshHostLoginMapping,
`${TableName.SshHost}.id`,
`${TableName.SshHostLoginMapping}.sshHostId`
)
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
.whereRaw(`"${TableName.SshHostLoginMapping}"."allowedPrincipals" && ?::text[]`, [principals])
.select(
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
db.ref("projectId").withSchema(TableName.SshHost),
db.ref("hostname").withSchema(TableName.SshHost),
db.ref("userCertTtl").withSchema(TableName.SshHost),
db.ref("hostCertTtl").withSchema(TableName.SshHost),
db.ref("loginUser").withSchema(TableName.SshHostLoginMapping),
db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping),
db.ref("userSshCaId").withSchema(TableName.SshHost),
db.ref("hostSshCaId").withSchema(TableName.SshHost)
)
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
const grouped = groupBy(matchingSshHosts, (r) => r.sshHostId);
return Object.values(grouped).map((hostRows) => {
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = hostRows[0];
const loginMappingGrouped = groupBy(
hostRows.filter((r) => r.loginUser),
(r) => r.loginUser
);
const loginMappings = Object.entries(loginMappingGrouped)
.map(([loginUser, entries]) => {
const filteredPrincipals = unique(entries.flatMap((entry) => entry.allowedPrincipals ?? [])).filter(
(principal) => principals.includes(principal)
);
if (filteredPrincipals.length === 0) return null;
return {
loginUser,
allowedPrincipals: filteredPrincipals
};
})
.filter(Boolean) as { loginUser: string; allowedPrincipals: string[] }[];
return {
id: sshHostId,
hostname,
projectId,
userCertTtl,
hostCertTtl,
loginMappings,
userSshCaId,
hostSshCaId
};
});
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostsWithPrincipalsAcrossProjects` });
}
};
const findSshHostsWithLoginMappings = async (projectId: string, tx?: Knex) => {
try {
const rows = await (tx || db.replicaNode())(TableName.SshHost)
@@ -27,13 +90,15 @@ export const sshHostDALFactory = (db: TDbClient) => {
db.ref("userCertTtl").withSchema(TableName.SshHost),
db.ref("hostCertTtl").withSchema(TableName.SshHost),
db.ref("loginUser").withSchema(TableName.SshHostLoginMapping),
db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping)
db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping),
db.ref("userSshCaId").withSchema(TableName.SshHost),
db.ref("hostSshCaId").withSchema(TableName.SshHost)
)
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
return Object.values(hostsGrouped).map((hostRows) => {
const { sshHostId, hostname, userCertTtl, hostCertTtl } = hostRows[0];
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
const loginMappingGrouped = groupBy(
hostRows.filter((r) => r.loginUser),
@@ -51,7 +116,9 @@ export const sshHostDALFactory = (db: TDbClient) => {
hostname,
userCertTtl,
hostCertTtl,
loginMappings
loginMappings,
userSshCaId,
hostSshCaId
};
});
} catch (error) {
@@ -75,12 +142,14 @@ export const sshHostDALFactory = (db: TDbClient) => {
db.ref("userCertTtl").withSchema(TableName.SshHost),
db.ref("hostCertTtl").withSchema(TableName.SshHost),
db.ref("loginUser").withSchema(TableName.SshHostLoginMapping),
db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping)
db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping),
db.ref("userSshCaId").withSchema(TableName.SshHost),
db.ref("hostSshCaId").withSchema(TableName.SshHost)
);
if (rows.length === 0) return null;
const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl } = rows[0];
const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
const loginMappingGrouped = groupBy(
rows.filter((r) => r.loginUser),
@@ -98,7 +167,9 @@ export const sshHostDALFactory = (db: TDbClient) => {
hostname,
userCertTtl,
hostCertTtl,
loginMappings
loginMappings,
userSshCaId,
hostSshCaId
};
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostByIdWithLoginMappings` });
@@ -107,6 +178,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
return {
...sshHostOrm,
findSshHostsWithPrincipalsAcrossProjects,
findSshHostsWithLoginMappings,
findSshHostByIdWithLoginMappings
};

View File

@@ -1,24 +1,54 @@
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { ActionProjectType, ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
import { TSshHostLoginMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-mapping-dal";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TCreateSshHostDTO, TDeleteSshHostDTO, TGetSshHostDTO, TUpdateSshHostDTO } from "./ssh-host-types";
import { convertActorToPrincipals, createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns";
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
import {
TCreateSshHostDTO,
TDeleteSshHostDTO,
TGetSshHostDTO,
TIssueSshCredsFromHostDTO,
TListSshHostsDTO,
TUpdateSshHostDTO
} from "./ssh-host-types";
type TSshCertificateAuthorityServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "find">;
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
sshHostDAL: Pick<
TSshHostDALFactory,
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "findSshHostByIdWithLoginMappings"
| "transaction"
| "create"
| "findById"
| "updateById"
| "deleteById"
| "findOne"
| "findSshHostByIdWithLoginMappings"
| "findSshHostsWithPrincipalsAcrossProjects"
>;
sshHostLoginMappingDAL: Pick<
TSshHostLoginMappingDALFactory,
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "insertMany" | "delete"
>;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
@@ -29,17 +59,72 @@ export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
*/
export const sshHostServiceFactory = ({
projectDAL
userDAL,
projectDAL,
projectSshConfigDAL,
sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL,
sshHostDAL,
sshHostLoginMappingDAL,
permissionService
permissionService,
kmsService
}: TSshCertificateAuthorityServiceFactoryDep) => {
/**
* Return list of all SSH hosts that a user has access to across all SSH projects in the organization
*/
const listSshHosts = async ({ actorId, actorAuthMethod, actor, actorOrgId }: TListSshHostsDTO) => {
const sshProjects = await projectDAL.find({
orgId: actorOrgId,
type: ProjectType.SSH
});
const projectIdsWithAccess: string[] = [];
for await (const project of sshProjects) {
let hasAccess = false;
try {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: project.id,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SSH
});
// TODO: consider glob-based permission items
hasAccess = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
} catch {
hasAccess = false;
}
if (hasAccess) {
projectIdsWithAccess.push(project.id);
}
}
// const principals = await convertActorToPrincipals({
// actor,
// actorId,
// userDAL
// });
const hosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects(projectIdsWithAccess, [
"dangtony98+2@gmail.com" // hardcode for now
]);
return hosts;
};
const createSshHost = async ({
projectId,
hostname,
userCertTtl,
hostCertTtl,
loginMappings,
userSshCaId: requestedUserSshCaId,
hostSshCaId: requestedHostSshCaId,
actorId,
actorAuthMethod,
actor,
@@ -56,6 +141,42 @@ export const sshHostServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHosts);
const resolveSshCaId = async ({
requestedId,
fallbackId,
label
}: {
requestedId?: string;
fallbackId?: string | null;
label: "User" | "Host";
}) => {
const finalId = requestedId ?? fallbackId;
if (!finalId) {
throw new BadRequestError({ message: `Missing ${label.toLowerCase()} SSH CA` });
}
const ca = await sshCertificateAuthorityDAL.findById(finalId);
if (!ca) {
throw new BadRequestError({ message: `${label} SSH CA with ID '${finalId}' not found` });
}
return ca.id;
};
const projectSshConfig = await projectSshConfigDAL.findOne({ projectId });
const userSshCaId = await resolveSshCaId({
requestedId: requestedUserSshCaId,
fallbackId: projectSshConfig?.defaultUserSshCaId,
label: "User"
});
const hostSshCaId = await resolveSshCaId({
requestedId: requestedHostSshCaId,
fallbackId: projectSshConfig?.defaultHostSshCaId,
label: "Host"
});
const newSshHost = await sshHostDAL.transaction(async (tx) => {
const existingHost = await sshHostDAL.findOne(
{
@@ -71,16 +192,14 @@ export const sshHostServiceFactory = ({
});
}
// attach hosts?
// create host ssh cas in default bound to default user ca and host ca
const host = await sshHostDAL.create(
{
projectId,
hostname,
userCertTtl,
hostCertTtl
hostCertTtl,
userSshCaId,
hostSshCaId
},
tx
);
@@ -211,10 +330,88 @@ export const sshHostServiceFactory = ({
return host;
};
/**
* Return SSH certificate and corresponding new SSH public-private key pair where
* SSH public key is signed using CA behind SSH certificate with name [templateName].
*
* Note: Used for issuing SSH credentials as part of request against a specific SSH Host.
*/
const issueSshCredsFromHost = async ({
sshHostId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TIssueSshCredsFromHostDTO) => {
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
if (!host) {
throw new NotFoundError({
message: `SSH host with ID ${sshHostId} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: host.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SSH
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
// TODO: update permissions
const keyId = `${actor}-${actorId}`;
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: host.projectId
});
const decryptedCaPrivateKey = secretManagerDecryptor({
cipherTextBlob: sshCaSecret.encryptedPrivateKey
});
// create user key pair
const keyAlgorithm = SshCertKeyAlgorithm.ED25519; // (dangtony98): will support more algorithms in the future
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
const principals = await convertActorToPrincipals({
actor,
actorId,
userDAL
});
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
clientPublicKey: publicKey,
keyId,
principals,
requestedTtl: host.userCertTtl,
certType: SshCertType.USER
});
return {
serialNumber,
signedPublicKey,
privateKey,
publicKey,
ttl,
keyId,
keyAlgorithm
};
};
return {
listSshHosts,
createSshHost,
updateSshHost,
deleteSshHost,
getSshHost
getSshHost,
issueSshCredsFromHost
};
};

View File

@@ -1,5 +1,7 @@
import { TProjectPermission } from "@app/lib/types";
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
export type TCreateSshHostDTO = {
hostname: string;
userCertTtl: string;
@@ -8,6 +10,8 @@ export type TCreateSshHostDTO = {
loginUser: string;
allowedPrincipals: string[];
}[];
userSshCaId?: string;
hostSshCaId?: string;
} & TProjectPermission;
export type TUpdateSshHostDTO = {
@@ -28,3 +32,7 @@ export type TGetSshHostDTO = {
export type TDeleteSshHostDTO = {
sshHostId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIssueSshCredsFromHostDTO = {
sshHostId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -11,6 +11,7 @@ import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certif
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { ActorType } from "@app/services/auth/auth-type";
import { KmsDataKey } from "@app/services/kms/kms-types";
import {
@@ -21,6 +22,7 @@ import {
SshCaKeySource,
SshCaStatus,
SshCertType,
TConvertActorToPrincipalsDTO,
TCreateSshCaHelperDTO,
TCreateSshCertDTO
} from "./ssh-certificate-authority-types";
@@ -439,17 +441,32 @@ export const createSshCert = async ({
clientPublicKey,
keyId,
principals,
requestedTtl,
requestedTtl, // in ms lib format
certType
}: TCreateSshCertDTO) => {
// validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(template, certType);
let ttl: number | undefined;
// validate if the requested [principals] are valid for the given [certType] under the template configuration
validateSshCertificatePrincipals(certType, template, principals);
if (!template && requestedTtl) {
const parsedTtl = Math.ceil(ms(requestedTtl) / 1000);
if (parsedTtl > 0) ttl = parsedTtl;
}
// validate if the requested TTL is valid under the template configuration
const ttl = validateSshCertificateTtl(template, requestedTtl);
if (template) {
// validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(template, certType);
// validate if the requested [principals] are valid for the given [certType] under the template configuration
validateSshCertificatePrincipals(certType, template, principals);
// validate if the requested TTL is valid under the template configuration
ttl = validateSshCertificateTtl(template, requestedTtl);
}
if (!ttl) {
throw new BadRequestError({
message: "Failed to create SSH certificate due to missing TTL"
});
}
validateSshCertificateKeyId(keyId);
await validateSshPublicKey(clientPublicKey);
@@ -561,3 +578,23 @@ export const createSshCaHelper = async ({
return sshCertificateAuthorityDAL.transaction(processCreation);
};
/**
* Convert an actor to a list of principals to be included in an SSH certificate.
*
* (dangtony98): This function is only supported for user actors at the moment and returns
* only the email of the associated user. In the future, we will consider other
* actor types and attributes such as group membership slugs and/or metadata to be
* included in the list of principals.
*/
export const convertActorToPrincipals = async ({ userDAL, actor, actorId }: TConvertActorToPrincipalsDTO) => {
if (actor !== ActorType.USER) {
throw new BadRequestError({
message: "Failed to convert actor to principals due to unsupported actor type"
});
}
const user = await userDAL.findById(actorId);
return [user.username];
};

View File

@@ -7,22 +7,14 @@ import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-cer
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal";
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
import { createSshCaHelper, createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
import {
createSshCaHelper,
createSshCert,
createSshKeyPair,
getSshPublicKey,
validateExternalSshCaKeyPair
} from "./ssh-certificate-authority-fns";
import {
SshCaKeySource,
SshCaStatus,
TCreateSshCaDTO,
TDeleteSshCaDTO,

View File

@@ -5,7 +5,9 @@ import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-cer
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
export enum SshCaStatus {
ACTIVE = "active",
@@ -84,7 +86,7 @@ export type TGetSshCaCertificateTemplatesDTO = {
} & Omit<TProjectPermission, "projectId">;
export type TCreateSshCertDTO = {
template: TSshCertificateTemplates;
template?: TSshCertificateTemplates;
caPrivateKey: string;
clientPublicKey: string;
keyId: string;
@@ -92,3 +94,9 @@ export type TCreateSshCertDTO = {
requestedTtl?: string;
certType: SshCertType;
};
export type TConvertActorToPrincipalsDTO = {
actor: ActorType;
actorId: string;
userDAL: Pick<TUserDALFactory, "findById">;
};

View File

@@ -1328,7 +1328,11 @@ 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.",
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:
"The ID of the SSH CA to use for host certificates. If not specified, the default host SSH CA will be used if it exists."
},
UPDATE: {
sshHostId: "The ID of the SSH host to update.",

View File

@@ -798,9 +798,15 @@ export const registerRoutes = async (
});
const sshHostService = sshHostServiceFactory({
userDAL,
projectDAL,
projectSshConfigDAL,
sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL,
sshHostDAL,
sshHostLoginMappingDAL,
permissionService
permissionService,
kmsService
});
const certificateAuthorityService = certificateAuthorityServiceFactory({

View File

@@ -1068,6 +1068,7 @@ export const projectServiceFactory = ({
actor,
projectId
}: TListProjectSshHostsDTO) => {
console.log("listProjectSshHosts: ", actor, actorId, actorAuthMethod, actorOrgId, projectId);
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,

View File

@@ -16,7 +16,7 @@ export const SshHostsPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Overview"
title="Hosts"
description="Infisical SSH lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure."
/>
<SshHostsSection />

View File

@@ -52,17 +52,8 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
data.map((host) => {
return (
<Tr
className="h-10"
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`ssh-host-${host.id}`}
// onClick={() =>
// navigate({
// to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const,
// params: {
// projectId: currentWorkspace.id,
// caId: ca.id
// }
// })
// }
>
<Td>{host.hostname}</Td>
<Td className="flex justify-end">