mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add list accessible ssh hosts endpoint
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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">;
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">;
|
||||
};
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -798,9 +798,15 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const sshHostService = sshHostServiceFactory({
|
||||
userDAL,
|
||||
projectDAL,
|
||||
projectSshConfigDAL,
|
||||
sshCertificateAuthorityDAL,
|
||||
sshCertificateAuthoritySecretDAL,
|
||||
sshHostDAL,
|
||||
sshHostLoginMappingDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user