mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 22:27:48 +00:00
Finish preliminary support for external key source for ssh cas
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SshCertificateAuthority, "keySource"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.string("keySource");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backfilling the keySource to internal
|
||||||
|
await knex(TableName.SshCertificateAuthority).update({ keySource: "internal" });
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.string("keySource").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SshCertificateAuthority, "keySource")) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.dropColumn("keySource");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,10 +23,10 @@ export const OrganizationsSchema = z.object({
|
|||||||
defaultMembershipRole: z.string().default("member"),
|
defaultMembershipRole: z.string().default("member"),
|
||||||
enforceMfa: z.boolean().default(false),
|
enforceMfa: z.boolean().default(false),
|
||||||
selectedMfaMethod: z.string().nullable().optional(),
|
selectedMfaMethod: z.string().nullable().optional(),
|
||||||
|
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
||||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
privilegeUpgradeInitiatedAt: z.date().nullable().optional()
|
||||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export const SshCertificateAuthoritiesSchema = z.object({
|
|||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
keyAlgorithm: z.string()
|
keyAlgorithm: z.string(),
|
||||||
|
keySource: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
|
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { normalizeSshPrivateKey } from "@app/ee/services/ssh/ssh-certificate-authority-fns";
|
||||||
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||||
import { SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCaKeySource, SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||||
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -20,13 +21,33 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
description: "Create SSH CA",
|
description: "Create SSH CA",
|
||||||
body: z.object({
|
body: z
|
||||||
|
.object({
|
||||||
projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId),
|
projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId),
|
||||||
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
||||||
keyAlgorithm: z
|
keyAlgorithm: z
|
||||||
.nativeEnum(CertKeyAlgorithm)
|
.nativeEnum(CertKeyAlgorithm)
|
||||||
.default(CertKeyAlgorithm.RSA_2048)
|
.default(CertKeyAlgorithm.RSA_2048)
|
||||||
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm),
|
||||||
|
publicKey: z.string().trim().optional().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.publicKey),
|
||||||
|
privateKey: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => (val ? normalizeSshPrivateKey(val) : undefined))
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.privateKey),
|
||||||
|
keySource: z
|
||||||
|
.nativeEnum(SshCaKeySource)
|
||||||
|
.default(SshCaKeySource.INTERNAL)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keySource)
|
||||||
|
})
|
||||||
|
.refine((data) => data.keySource === SshCaKeySource.INTERNAL || (!!data.publicKey && !!data.privateKey), {
|
||||||
|
message: "publicKey and privateKey are required when keySource is external",
|
||||||
|
path: ["publicKey"]
|
||||||
|
})
|
||||||
|
.refine((data) => data.keySource === SshCaKeySource.EXTERNAL || !!data.keyAlgorithm, {
|
||||||
|
message: "keyAlgorithm is required when keySource is internal",
|
||||||
|
path: ["keyAlgorithm"]
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -322,6 +322,91 @@ const validateSshPublicKey = async (publicKey: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getKeyAlgorithmFromFingerprintOutput = (output: string): CertKeyAlgorithm | undefined => {
|
||||||
|
const parts = output.trim().split(" ");
|
||||||
|
const bitsInt = parseInt(parts[0], 10);
|
||||||
|
const keyTypeRaw = parts.at(-1)?.replace(/[()]/g, ""); // remove surrounding parentheses
|
||||||
|
|
||||||
|
if (keyTypeRaw === "RSA") {
|
||||||
|
return bitsInt === 2048 ? CertKeyAlgorithm.RSA_2048 : CertKeyAlgorithm.RSA_4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyTypeRaw === "ECDSA") {
|
||||||
|
return bitsInt === 256 ? CertKeyAlgorithm.ECDSA_P256 : CertKeyAlgorithm.ECDSA_P384;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeSshPrivateKey = (raw: string): string => {
|
||||||
|
return `${raw
|
||||||
|
.replace(/\r\n/g, "\n") // Windows CRLF → LF
|
||||||
|
.replace(/\r/g, "\n") // Old Mac CR → LF
|
||||||
|
.replace(/\\n/g, "\n") // Double-escaped \n
|
||||||
|
.trim()}\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the format of the SSH private key
|
||||||
|
*
|
||||||
|
* Returns the SSH public key corresponding to the private key
|
||||||
|
* and the key algorithm categorization.
|
||||||
|
*/
|
||||||
|
export const validateSshPrivateKey = async (privateKey: string) => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-privkey-"));
|
||||||
|
const privateKeyFile = path.join(tempDir, "id_key");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(privateKeyFile, privateKey, {
|
||||||
|
encoding: "utf8",
|
||||||
|
mode: 0o600
|
||||||
|
});
|
||||||
|
|
||||||
|
// This will fail if the private key is malformed or unreadable
|
||||||
|
const { stdout: publicKey } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], {
|
||||||
|
timeout: EXEC_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
|
||||||
|
const { stdout: fingerprint } = await execFileAsync("ssh-keygen", ["-lf", privateKeyFile]);
|
||||||
|
const keyAlgorithm = getKeyAlgorithmFromFingerprintOutput(fingerprint);
|
||||||
|
|
||||||
|
if (!keyAlgorithm) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH private key format: The key algorithm is not supported."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey,
|
||||||
|
keyAlgorithm
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH private key format: could not be parsed."
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the provided public and private keys are valid and constitute
|
||||||
|
* a matching SSH key pair.
|
||||||
|
*/
|
||||||
|
export const validateExternalSshCaKeyPair = async (publicKey: string, privateKey: string) => {
|
||||||
|
await validateSshPublicKey(publicKey);
|
||||||
|
|
||||||
|
const { publicKey: derivedPublicKey, keyAlgorithm } = await validateSshPrivateKey(privateKey);
|
||||||
|
|
||||||
|
if (publicKey.trim() !== derivedPublicKey.trim()) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate matching SSH key pair."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyAlgorithm;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an SSH certificate for a user or host.
|
* Create an SSH certificate for a user or host.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ export const sanitizedSshCa = SshCertificateAuthoritiesSchema.pick({
|
|||||||
projectId: true,
|
projectId: true,
|
||||||
friendlyName: true,
|
friendlyName: true,
|
||||||
status: true,
|
status: true,
|
||||||
keyAlgorithm: true
|
keyAlgorithm: true,
|
||||||
|
keySource: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,12 +9,19 @@ import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/
|
|||||||
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
|
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
|
|
||||||
import {
|
import {
|
||||||
|
createSshCert,
|
||||||
|
createSshKeyPair,
|
||||||
|
getSshPublicKey,
|
||||||
|
validateExternalSshCaKeyPair
|
||||||
|
} from "./ssh-certificate-authority-fns";
|
||||||
|
import {
|
||||||
|
SshCaKeySource,
|
||||||
SshCaStatus,
|
SshCaStatus,
|
||||||
TCreateSshCaDTO,
|
TCreateSshCaDTO,
|
||||||
TDeleteSshCaDTO,
|
TDeleteSshCaDTO,
|
||||||
@@ -59,7 +66,10 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
const createSshCa = async ({
|
const createSshCa = async ({
|
||||||
projectId,
|
projectId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
keyAlgorithm,
|
keyAlgorithm: requestedKeyAlgorithm,
|
||||||
|
publicKey: externalPk,
|
||||||
|
privateKey: externalSk,
|
||||||
|
keySource,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
@@ -80,18 +90,37 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const newCa = await sshCertificateAuthorityDAL.transaction(async (tx) => {
|
const newCa = await sshCertificateAuthorityDAL.transaction(async (tx) => {
|
||||||
|
let publicKey: string;
|
||||||
|
let privateKey: string;
|
||||||
|
let keyAlgorithm: CertKeyAlgorithm = requestedKeyAlgorithm;
|
||||||
|
|
||||||
|
if (keySource === SshCaKeySource.INTERNAL) {
|
||||||
|
// generate SSH CA key pair internally
|
||||||
|
({ publicKey, privateKey } = await createSshKeyPair(requestedKeyAlgorithm));
|
||||||
|
} else {
|
||||||
|
// use external SSH CA key pair
|
||||||
|
if (!externalPk || !externalSk) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Public and private keys are required if generateSigningKey is false"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
publicKey = externalPk;
|
||||||
|
privateKey = externalSk;
|
||||||
|
|
||||||
|
keyAlgorithm = await validateExternalSshCaKeyPair(publicKey, privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
const ca = await sshCertificateAuthorityDAL.create(
|
const ca = await sshCertificateAuthorityDAL.create(
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
status: SshCaStatus.ACTIVE,
|
status: SshCaStatus.ACTIVE,
|
||||||
keyAlgorithm
|
keyAlgorithm,
|
||||||
|
keySource
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
|
||||||
|
|
||||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId
|
projectId
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export enum SshCaStatus {
|
|||||||
DISABLED = "disabled"
|
DISABLED = "disabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SshCaKeySource {
|
||||||
|
INTERNAL = "internal",
|
||||||
|
EXTERNAL = "external"
|
||||||
|
}
|
||||||
|
|
||||||
export enum SshCertType {
|
export enum SshCertType {
|
||||||
USER = "user",
|
USER = "user",
|
||||||
HOST = "host"
|
HOST = "host"
|
||||||
@@ -15,6 +20,9 @@ export enum SshCertType {
|
|||||||
export type TCreateSshCaDTO = {
|
export type TCreateSshCaDTO = {
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
publicKey?: string;
|
||||||
|
privateKey?: string;
|
||||||
|
keySource: SshCaKeySource;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetSshCaDTO = {
|
export type TGetSshCaDTO = {
|
||||||
|
|||||||
@@ -633,7 +633,8 @@ export const FOLDERS = {
|
|||||||
path: "The path to list folders from.",
|
path: "The path to list folders from.",
|
||||||
directory: "The directory to list folders from. (Deprecated in favor of path)",
|
directory: "The directory to list folders from. (Deprecated in favor of path)",
|
||||||
recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories.",
|
recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories.",
|
||||||
lastSecretModified: "The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)"
|
lastSecretModified:
|
||||||
|
"The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)"
|
||||||
},
|
},
|
||||||
GET_BY_ID: {
|
GET_BY_ID: {
|
||||||
folderId: "The ID of the folder to get details."
|
folderId: "The ID of the folder to get details."
|
||||||
@@ -1234,7 +1235,11 @@ export const SSH_CERTIFICATE_AUTHORITIES = {
|
|||||||
CREATE: {
|
CREATE: {
|
||||||
projectId: "The ID of the project to create the SSH CA in.",
|
projectId: "The ID of the project to create the SSH CA in.",
|
||||||
friendlyName: "A friendly name for the SSH CA.",
|
friendlyName: "A friendly name for the SSH CA.",
|
||||||
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH CA."
|
keyAlgorithm:
|
||||||
|
"The type of public key algorithm and size, in bits, of the key pair for the SSH CA; required if keySource is internal.",
|
||||||
|
publicKey: "The public key for the SSH CA key pair; required if keySource is external.",
|
||||||
|
privateKey: "The private key for the SSH CA key pair; required if keySource is external.",
|
||||||
|
keySource: "The source of the SSH CA key pair. This can be one of internal or external."
|
||||||
},
|
},
|
||||||
GET: {
|
GET: {
|
||||||
sshCaId: "The ID of the SSH CA to get."
|
sshCaId: "The ID of the SSH CA to get."
|
||||||
|
|||||||
@@ -12,3 +12,8 @@ export const sshCertTypeToNameMap: { [K in SshCertType]: string } = {
|
|||||||
[SshCertType.USER]: "User",
|
[SshCertType.USER]: "User",
|
||||||
[SshCertType.HOST]: "Host"
|
[SshCertType.HOST]: "Host"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum SshCaKeySource {
|
||||||
|
INTERNAL = "internal",
|
||||||
|
EXTERNAL = "external"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CertKeyAlgorithm } from "../certificates/enums";
|
import { CertKeyAlgorithm } from "../certificates/enums";
|
||||||
import { SshCaStatus, SshCertType } from "./constants";
|
import { SshCaKeySource, SshCaStatus, SshCertType } from "./constants";
|
||||||
|
|
||||||
export type TSshCertificate = {
|
export type TSshCertificate = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,16 +19,27 @@ export type TSshCertificateAuthority = {
|
|||||||
status: SshCaStatus;
|
status: SshCaStatus;
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
keySource: SshCaKeySource;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateSshCaDTO = {
|
export type TCreateSshCaDTO =
|
||||||
|
| {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
|
keySource: SshCaKeySource.INTERNAL;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
};
|
}
|
||||||
|
| {
|
||||||
|
projectId: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
keySource: SshCaKeySource.EXTERNAL;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TUpdateSshCaDTO = {
|
export type TUpdateSshCaDTO = {
|
||||||
caId: string;
|
caId: string;
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem,
|
||||||
|
TextArea
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { useCreateSshCa, useGetSshCaById, useUpdateSshCa } from "@app/hooks/api";
|
import { useCreateSshCa, useGetSshCaById, useUpdateSshCa } from "@app/hooks/api";
|
||||||
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
||||||
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
||||||
|
import { SshCaKeySource } from "@app/hooks/api/sshCa/constants";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -26,9 +28,17 @@ type Props = {
|
|||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshCa"]>, state?: boolean) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshCa"]>, state?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sshCaKeySources = [
|
||||||
|
{ label: "Internal", value: SshCaKeySource.INTERNAL },
|
||||||
|
{ label: "External", value: SshCaKeySource.EXTERNAL }
|
||||||
|
];
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
|
keySource: z.nativeEnum(SshCaKeySource),
|
||||||
|
publicKey: z.string().optional(),
|
||||||
|
privateKey: z.string().optional(),
|
||||||
keyAlgorithm: z.enum([
|
keyAlgorithm: z.enum([
|
||||||
CertKeyAlgorithm.RSA_2048,
|
CertKeyAlgorithm.RSA_2048,
|
||||||
CertKeyAlgorithm.RSA_4096,
|
CertKeyAlgorithm.RSA_4096,
|
||||||
@@ -53,30 +63,47 @@ export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting },
|
||||||
|
watch
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
friendlyName: "",
|
friendlyName: "",
|
||||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048
|
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||||
|
keySource: SshCaKeySource.INTERNAL,
|
||||||
|
publicKey: "",
|
||||||
|
privateKey: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const caKeySource = watch("keySource");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ca) {
|
if (ca) {
|
||||||
reset({
|
reset({
|
||||||
friendlyName: ca.friendlyName,
|
friendlyName: ca.friendlyName,
|
||||||
keyAlgorithm: ca.keyAlgorithm
|
keyAlgorithm: ca.keyAlgorithm,
|
||||||
|
keySource: ca.keySource,
|
||||||
|
publicKey: ca.publicKey
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
friendlyName: "",
|
friendlyName: "",
|
||||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048
|
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||||
|
keySource: SshCaKeySource.INTERNAL,
|
||||||
|
publicKey: "",
|
||||||
|
privateKey: ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [ca]);
|
}, [ca]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ friendlyName, keyAlgorithm }: FormData) => {
|
const onFormSubmit = async ({
|
||||||
|
friendlyName,
|
||||||
|
keySource,
|
||||||
|
keyAlgorithm,
|
||||||
|
publicKey,
|
||||||
|
privateKey
|
||||||
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
|
|
||||||
@@ -89,7 +116,10 @@ export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const { id: newCaId } = await createMutateAsync({
|
const { id: newCaId } = await createMutateAsync({
|
||||||
projectId,
|
projectId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
keyAlgorithm
|
keySource,
|
||||||
|
keyAlgorithm,
|
||||||
|
publicKey,
|
||||||
|
privateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
@@ -147,6 +177,36 @@ export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{caKeySource && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="keySource"
|
||||||
|
defaultValue={SshCaKeySource.INTERNAL}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Key Source"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
isDisabled={Boolean(ca)}
|
||||||
|
>
|
||||||
|
{sshCaKeySources.map(({ label, value }) => (
|
||||||
|
<SelectItem value={String(value || "")} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{caKeySource === SshCaKeySource.INTERNAL && (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="keyAlgorithm"
|
name="keyAlgorithm"
|
||||||
@@ -156,6 +216,7 @@ export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
label="Key Algorithm"
|
label="Key Algorithm"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
|
isRequired={caKeySource === SshCaKeySource.INTERNAL}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
@@ -173,6 +234,41 @@ export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{caKeySource === SshCaKeySource.EXTERNAL && !ca && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="publicKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Public Key"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired={caKeySource === SshCaKeySource.EXTERNAL}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="ssh-rsa AAA..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="privateKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Private Key"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isRequired={caKeySource === SshCaKeySource.EXTERNAL}
|
||||||
|
>
|
||||||
|
<TextArea {...field} placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
|||||||
Reference in New Issue
Block a user