mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 03:28:15 +00:00
Make PR review adjustments, ssh ca public key endpoint, ssh cert template status
This commit is contained in:
@@ -34,6 +34,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
t.uuid("sshCaId").notNullable();
|
t.uuid("sshCaId").notNullable();
|
||||||
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.string("status").notNullable(); // active / disabled
|
||||||
t.string("name").notNullable();
|
t.string("name").notNullable();
|
||||||
t.string("ttl").notNullable();
|
t.string("ttl").notNullable();
|
||||||
t.string("maxTTL").notNullable();
|
t.string("maxTTL").notNullable();
|
||||||
@@ -51,7 +52,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
t.uuid("sshCaId").notNullable();
|
t.uuid("sshCaId").notNullable();
|
||||||
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
|
||||||
t.uuid("sshCertificateTemplateId");
|
t.uuid("sshCertificateTemplateId");
|
||||||
t.foreign("sshCertificateTemplateId")
|
t.foreign("sshCertificateTemplateId")
|
||||||
.references("id")
|
.references("id")
|
||||||
@@ -65,7 +66,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.datetime("notBefore").notNullable();
|
t.datetime("notBefore").notNullable();
|
||||||
t.datetime("notAfter").notNullable();
|
t.datetime("notAfter").notNullable();
|
||||||
});
|
});
|
||||||
await createOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
|
await createOnUpdateTrigger(knex, TableName.SshCertificate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const SshCertificateTemplatesSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
sshCaId: z.string().uuid(),
|
sshCaId: z.string().uuid(),
|
||||||
|
status: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
ttl: z.string(),
|
ttl: z.string(),
|
||||||
maxTTL: z.string(),
|
maxTTL: z.string(),
|
||||||
|
|||||||
@@ -109,6 +109,30 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshCaId/public-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get public key of SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_PUBLIC_KEY.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.string()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const publicKey = await server.services.sshCertificateAuthority.getSshCaPublicKey({
|
||||||
|
caId: req.params.sshCaId
|
||||||
|
});
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:sshCaId",
|
url: "/:sshCaId",
|
||||||
@@ -123,10 +147,7 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
friendlyName: z.string().optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.friendlyName),
|
friendlyName: z.string().optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.friendlyName),
|
||||||
status: z
|
status: z.nativeEnum(SshCaStatus).optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.status)
|
||||||
.enum([SshCaStatus.ACTIVE, SshCaStatus.DISABLED])
|
|
||||||
.optional()
|
|
||||||
.describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.status)
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { 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 { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import {
|
import {
|
||||||
isValidHostPattern,
|
isValidHostPattern,
|
||||||
isValidUserPattern
|
isValidUserPattern
|
||||||
@@ -136,6 +137,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
status: z.nativeEnum(SshCertTemplateStatus).optional(),
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
@@ -191,6 +193,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE,
|
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
status: certificateTemplate.status as SshCertTemplateStatus,
|
||||||
certificateTemplateId: certificateTemplate.id,
|
certificateTemplateId: certificateTemplate.id,
|
||||||
sshCaId: certificateTemplate.sshCaId,
|
sshCaId: certificateTemplate.sshCaId,
|
||||||
name: certificateTemplate.name,
|
name: certificateTemplate.name,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
TUpdateProjectTemplateDTO
|
TUpdateProjectTemplateDTO
|
||||||
} from "@app/ee/services/project-template/project-template-types";
|
} from "@app/ee/services/project-template/project-template-types";
|
||||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@@ -1238,6 +1239,7 @@ interface UpdateSshCertificateTemplate {
|
|||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
sshCaId: string;
|
sshCaId: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
status: SshCertTemplateStatus;
|
||||||
ttl: string;
|
ttl: string;
|
||||||
maxTTL: string;
|
maxTTL: string;
|
||||||
allowedUsers: string[];
|
allowedUsers: string[];
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { SshCertificateTemplatesSchema } from "@app/db/schemas";
|
|||||||
export const sanitizedSshCertificateTemplate = SshCertificateTemplatesSchema.pick({
|
export const sanitizedSshCertificateTemplate = SshCertificateTemplatesSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
sshCaId: true,
|
sshCaId: true,
|
||||||
|
status: true,
|
||||||
name: true,
|
name: true,
|
||||||
ttl: true,
|
ttl: true,
|
||||||
maxTTL: true,
|
maxTTL: true,
|
||||||
|
|||||||
+61
-39
@@ -8,6 +8,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
||||||
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
||||||
import {
|
import {
|
||||||
|
SshCertTemplateStatus,
|
||||||
TCreateSshCertTemplateDTO,
|
TCreateSshCertTemplateDTO,
|
||||||
TDeleteSshCertTemplateDTO,
|
TDeleteSshCertTemplateDTO,
|
||||||
TGetSshCertTemplateDTO,
|
TGetSshCertTemplateDTO,
|
||||||
@@ -15,7 +16,10 @@ import {
|
|||||||
} from "./ssh-certificate-template-types";
|
} from "./ssh-certificate-template-types";
|
||||||
|
|
||||||
type TSshCertificateTemplateServiceFactoryDep = {
|
type TSshCertificateTemplateServiceFactoryDep = {
|
||||||
sshCertificateTemplateDAL: TSshCertificateTemplateDALFactory;
|
sshCertificateTemplateDAL: Pick<
|
||||||
|
TSshCertificateTemplateDALFactory,
|
||||||
|
"transaction" | "getByName" | "create" | "updateById" | "deleteById" | "getById"
|
||||||
|
>;
|
||||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
};
|
};
|
||||||
@@ -62,36 +66,45 @@ export const sshCertificateTemplateServiceFactory = ({
|
|||||||
OrgPermissionSubjects.SshCertificateTemplates
|
OrgPermissionSubjects.SshCertificateTemplates
|
||||||
);
|
);
|
||||||
|
|
||||||
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, ca.orgId);
|
|
||||||
if (existingTemplate) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `SSH certificate template with name ${name} already exists`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ms(ttl) > ms(maxTTL)) {
|
if (ms(ttl) > ms(maxTTL)) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "TTL cannot be greater than max TTL"
|
message: "TTL cannot be greater than max TTL"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const certificateTemplate = await sshCertificateTemplateDAL.create({
|
const newCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => {
|
||||||
sshCaId,
|
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, ca.orgId, tx);
|
||||||
name,
|
if (existingTemplate) {
|
||||||
ttl,
|
throw new BadRequestError({
|
||||||
maxTTL,
|
message: `SSH certificate template with name ${name} already exists`
|
||||||
allowUserCertificates,
|
});
|
||||||
allowHostCertificates,
|
}
|
||||||
allowedUsers,
|
|
||||||
allowedHosts,
|
const certificateTemplate = await sshCertificateTemplateDAL.create(
|
||||||
allowCustomKeyIds
|
{
|
||||||
|
sshCaId,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds,
|
||||||
|
status: SshCertTemplateStatus.ACTIVE
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { certificateTemplate, ca };
|
return { certificateTemplate: newCertificateTemplate, ca };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSshCertTemplate = async ({
|
const updateSshCertTemplate = async ({
|
||||||
id,
|
id,
|
||||||
|
status,
|
||||||
name,
|
name,
|
||||||
ttl,
|
ttl,
|
||||||
maxTTL,
|
maxTTL,
|
||||||
@@ -125,34 +138,43 @@ export const sshCertificateTemplateServiceFactory = ({
|
|||||||
OrgPermissionSubjects.SshCertificateTemplates
|
OrgPermissionSubjects.SshCertificateTemplates
|
||||||
);
|
);
|
||||||
|
|
||||||
if (name) {
|
const updatedCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => {
|
||||||
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, actorOrgId);
|
if (name) {
|
||||||
if (existingTemplate && existingTemplate.id !== id) {
|
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, actorOrgId, tx);
|
||||||
|
if (existingTemplate && existingTemplate.id !== id) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `SSH certificate template with name ${name} already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `SSH certificate template with name ${name} already exists`
|
message: "TTL cannot be greater than max TTL"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) {
|
const certificateTemplate = await sshCertificateTemplateDAL.updateById(
|
||||||
throw new BadRequestError({
|
id,
|
||||||
message: "TTL cannot be greater than max TTL"
|
{
|
||||||
});
|
status,
|
||||||
}
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
const certificateTemplate = await sshCertificateTemplateDAL.updateById(id, {
|
return certificateTemplate;
|
||||||
name,
|
|
||||||
ttl,
|
|
||||||
maxTTL,
|
|
||||||
allowUserCertificates,
|
|
||||||
allowHostCertificates,
|
|
||||||
allowedUsers,
|
|
||||||
allowedHosts,
|
|
||||||
allowCustomKeyIds
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificateTemplate,
|
certificateTemplate: updatedCertificateTemplate,
|
||||||
orgId: certTemplate.orgId
|
orgId: certTemplate.orgId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export enum SshCertTemplateStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
export type TCreateSshCertTemplateDTO = {
|
export type TCreateSshCertTemplateDTO = {
|
||||||
sshCaId: string;
|
sshCaId: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -14,6 +19,7 @@ export type TCreateSshCertTemplateDTO = {
|
|||||||
|
|
||||||
export type TUpdateSshCertTemplateDTO = {
|
export type TUpdateSshCertTemplateDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
|
status?: SshCertTemplateStatus;
|
||||||
name?: string;
|
name?: string;
|
||||||
ttl?: string;
|
ttl?: string;
|
||||||
maxTTL?: string;
|
maxTTL?: string;
|
||||||
|
|||||||
@@ -8,5 +8,7 @@ export const sanitizedSshCertificate = SshCertificatesSchema.pick({
|
|||||||
certType: true,
|
certType: true,
|
||||||
publicKey: true,
|
publicKey: true,
|
||||||
principals: true,
|
principals: true,
|
||||||
keyId: true
|
keyId: true,
|
||||||
|
notBefore: true,
|
||||||
|
notAfter: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ export const createSshKeyPair = (keyAlgorithm: CertKeyAlgorithm, comment: string
|
|||||||
keyBits = "384";
|
keyBits = "384";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Failed to produce SSH CA key pair generation command due to unrecognized key algorithm");
|
throw new BadRequestError({
|
||||||
|
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
execSync(`ssh-keygen -t ${keyType} -b ${keyBits} -f ${privateKeyFile} -N '' -C "${comment}"`);
|
execSync(`ssh-keygen -t ${keyType} -b ${keyBits} -f ${privateKeyFile} -N '' -C "${comment}"`);
|
||||||
@@ -200,7 +202,7 @@ export const validateSshCertificatePrincipals = (
|
|||||||
* @param ttl - The TTL to validate
|
* @param ttl - The TTL to validate
|
||||||
* @returns The TTL (in seconds) to use for issuing the SSH certificate
|
* @returns The TTL (in seconds) to use for issuing the SSH certificate
|
||||||
*/
|
*/
|
||||||
export const validateSshCertificateTtl = (template: TSshCertificateTemplates, ttl: string | undefined) => {
|
export const validateSshCertificateTtl = (template: TSshCertificateTemplates, ttl?: string) => {
|
||||||
if (!ttl) {
|
if (!ttl) {
|
||||||
// use default template ttl
|
// use default template ttl
|
||||||
return ms(template.ttl) / 1000;
|
return ms(template.ttl) / 1000;
|
||||||
@@ -249,6 +251,8 @@ export const createSshCert = ({ caPrivateKey, userPublicKey, keyId, principals,
|
|||||||
|
|
||||||
const command = `ssh-keygen ${certOptions}`;
|
const command = `ssh-keygen ${certOptions}`;
|
||||||
|
|
||||||
|
console.log("executing command", command);
|
||||||
|
|
||||||
// Execute the signing process
|
// Execute the signing process
|
||||||
execSync(command);
|
execSync(command);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certific
|
|||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import {
|
import {
|
||||||
createSshCert,
|
createSshCert,
|
||||||
createSshKeyPair,
|
createSshKeyPair,
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
TDeleteSshCaDTO,
|
TDeleteSshCaDTO,
|
||||||
TGetSshCaCertificateTemplatesDTO,
|
TGetSshCaCertificateTemplatesDTO,
|
||||||
TGetSshCaDTO,
|
TGetSshCaDTO,
|
||||||
|
TGetSshCaPublicKeyDTO,
|
||||||
TIssueSshCredsDTO,
|
TIssueSshCredsDTO,
|
||||||
TSignSshKeyDTO,
|
TSignSshKeyDTO,
|
||||||
TUpdateSshCaDTO
|
TUpdateSshCaDTO
|
||||||
@@ -147,6 +149,30 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
return { ...ca, publicKey };
|
return { ...ca, publicKey };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return public key of SSH CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getSshCaPublicKey = async ({ caId }: TGetSshCaPublicKeyDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id });
|
||||||
|
|
||||||
|
// decrypt secret
|
||||||
|
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(ca.orgId);
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: orgKmsKeyId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = await kmsDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update SSH CA with id [caId]
|
* Update SSH CA with id [caId]
|
||||||
* Note: Used to enable/disable CA
|
* Note: Used to enable/disable CA
|
||||||
@@ -259,6 +285,12 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sshCertificateTemplate.status === SshCertTemplateStatus.DISABLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SSH certificate template is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// validate if the requested [certType] is allowed under the template configuration
|
// validate if the requested [certType] is allowed under the template configuration
|
||||||
validateSshCertificateType(sshCertificateTemplate, certType);
|
validateSshCertificateType(sshCertificateTemplate, certType);
|
||||||
|
|
||||||
@@ -359,6 +391,12 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sshCertificateTemplate.status === SshCertTemplateStatus.DISABLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SSH certificate template is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// validate if the requested [certType] is allowed under the template configuration
|
// validate if the requested [certType] is allowed under the template configuration
|
||||||
validateSshCertificateType(sshCertificateTemplate, certType);
|
validateSshCertificateType(sshCertificateTemplate, certType);
|
||||||
|
|
||||||
@@ -445,6 +483,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
signSshKey,
|
signSshKey,
|
||||||
createSshCa,
|
createSshCa,
|
||||||
getSshCaById,
|
getSshCaById,
|
||||||
|
getSshCaPublicKey,
|
||||||
updateSshCaById,
|
updateSshCaById,
|
||||||
deleteSshCaById,
|
deleteSshCaById,
|
||||||
getSshCaCertificateTemplates
|
getSshCaCertificateTemplates
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export type TGetSshCaDTO = {
|
|||||||
caId: string;
|
caId: string;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGetSshCaPublicKeyDTO = {
|
||||||
|
caId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TUpdateSshCaDTO = {
|
export type TUpdateSshCaDTO = {
|
||||||
caId: string;
|
caId: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
|
|||||||
@@ -1154,6 +1154,9 @@ export const SSH_CERTIFICATE_AUTHORITIES = {
|
|||||||
GET: {
|
GET: {
|
||||||
sshCaId: "The ID of the SSH CA to get."
|
sshCaId: "The ID of the SSH CA to get."
|
||||||
},
|
},
|
||||||
|
GET_PUBLIC_KEY: {
|
||||||
|
sshCaId: "The ID of the SSH CA to get the public key for."
|
||||||
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
sshCaId: "The ID of the SSH CA to update.",
|
sshCaId: "The ID of the SSH CA to update.",
|
||||||
friendlyName: "A friendly name for the SSH CA to update to.",
|
friendlyName: "A friendly name for the SSH CA to update to.",
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificates: z.array(sanitizedSshCertificate),
|
certificates: z.array(sanitizedSshCertificate),
|
||||||
totalCount: z.number() // TODO
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { SshCaStatus } from "@app/hooks/api/ssh-ca";
|
import { SshCaStatus } from "@app/hooks/api/ssh-ca";
|
||||||
|
import { SshCertTemplateStatus } from "@app/hooks/api/sshCertificateTemplates";
|
||||||
|
|
||||||
import { CaStatus, CaType } from "./enums";
|
import { CaStatus, CaType } from "./enums";
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
|||||||
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
|
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus) => {
|
export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCertTemplateStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case CaStatus.ACTIVE:
|
case CaStatus.ACTIVE:
|
||||||
return "success";
|
return "success";
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export type TSshCertificate = {
|
|||||||
publicKey: string;
|
publicKey: string;
|
||||||
principals: string[];
|
principals: string[];
|
||||||
keyId: string;
|
keyId: string;
|
||||||
|
notBefore: string;
|
||||||
|
notAfter: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSshCertificateAuthority = {
|
export type TSshCertificateAuthority = {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
useCreateSshCertTemplate,
|
useCreateSshCertTemplate,
|
||||||
useDeleteSshCertTemplate,
|
useDeleteSshCertTemplate,
|
||||||
useUpdateSshCertTemplate} from "./mutations";
|
useUpdateSshCertTemplate
|
||||||
|
} from "./mutations";
|
||||||
export { useGetSshCertTemplate } from "./queries";
|
export { useGetSshCertTemplate } from "./queries";
|
||||||
|
export * from "./types";
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
|
export enum SshCertTemplateStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
export type TSshCertificateTemplate = {
|
export type TSshCertificateTemplate = {
|
||||||
id: string;
|
id: string;
|
||||||
sshCaId: string;
|
sshCaId: string;
|
||||||
|
status: SshCertTemplateStatus;
|
||||||
name: string;
|
name: string;
|
||||||
ttl: string;
|
ttl: string;
|
||||||
maxTTL: string;
|
maxTTL: string;
|
||||||
@@ -25,6 +31,7 @@ export type TCreateSshCertificateTemplateDTO = {
|
|||||||
|
|
||||||
export type TUpdateSshCertificateTemplateDTO = {
|
export type TUpdateSshCertificateTemplateDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
|
status?: SshCertTemplateStatus;
|
||||||
name?: string;
|
name?: string;
|
||||||
ttl?: string;
|
ttl?: string;
|
||||||
maxTTL?: string;
|
maxTTL?: string;
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ import {
|
|||||||
SelectItem
|
SelectItem
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { useIssueSshCreds, useListOrgSshCertificateTemplates, useSignSshKey } from "@app/hooks/api";
|
import {
|
||||||
|
SshCertTemplateStatus,
|
||||||
|
useGetSshCertTemplate,
|
||||||
|
useIssueSshCreds,
|
||||||
|
useListOrgSshCertificateTemplates,
|
||||||
|
useSignSshKey} 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 { SshCertType } from "@app/hooks/api/ssh-ca/constants";
|
import { SshCertType } from "@app/hooks/api/ssh-ca/constants";
|
||||||
@@ -22,14 +27,8 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
|
|
||||||
import { SshCertificateContent } from "./SshCertificateContent";
|
import { SshCertificateContent } from "./SshCertificateContent";
|
||||||
|
|
||||||
/**
|
|
||||||
* // NOTE (dangtony98): current UI only supports SSH certificate
|
|
||||||
* issuance via /issue endpoint but should extend to also support
|
|
||||||
* /sign endpoint as this is already supported in the backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
templateName: z.string(),
|
templateId: z.string(),
|
||||||
publicKey: z.string().optional(),
|
publicKey: z.string().optional(),
|
||||||
keyAlgorithm: z.enum([
|
keyAlgorithm: z.enum([
|
||||||
CertKeyAlgorithm.RSA_2048,
|
CertKeyAlgorithm.RSA_2048,
|
||||||
@@ -72,7 +71,11 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const { mutateAsync: signSshKey } = useSignSshKey();
|
const { mutateAsync: signSshKey } = useSignSshKey();
|
||||||
const { mutateAsync: issueSshCreds } = useIssueSshCreds();
|
const { mutateAsync: issueSshCreds } = useIssueSshCreds();
|
||||||
|
|
||||||
const popUpData = popUp?.sshCertificate?.data as { sshCaId: string; templateName: string };
|
const popUpData = popUp?.sshCertificate?.data as {
|
||||||
|
sshCaId: string;
|
||||||
|
templateName: string;
|
||||||
|
templateId: string;
|
||||||
|
};
|
||||||
|
|
||||||
const { data: templatesData } = useListOrgSshCertificateTemplates({
|
const { data: templatesData } = useListOrgSshCertificateTemplates({
|
||||||
orgId: currentOrg?.id || ""
|
orgId: currentOrg?.id || ""
|
||||||
@@ -83,7 +86,8 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
setValue
|
setValue,
|
||||||
|
watch
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -92,16 +96,18 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const templateId = watch("templateId");
|
||||||
|
const { data: templateData } = useGetSshCertTemplate(templateId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (popUpData) {
|
if (popUpData) {
|
||||||
setValue("templateName", popUpData.templateName);
|
setValue("templateId", popUpData.templateId);
|
||||||
} else if (templatesData && templatesData.certificateTemplates.length > 0) {
|
} else if (templatesData && templatesData.certificateTemplates.length > 0) {
|
||||||
setValue("templateName", templatesData.certificateTemplates[0].name);
|
setValue("templateId", templatesData.certificateTemplates[0].id);
|
||||||
}
|
}
|
||||||
}, [popUpData]);
|
}, [popUpData]);
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({
|
||||||
templateName,
|
|
||||||
keyAlgorithm,
|
keyAlgorithm,
|
||||||
certType,
|
certType,
|
||||||
publicKey: existingPublicKey,
|
publicKey: existingPublicKey,
|
||||||
@@ -110,10 +116,12 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
keyId
|
keyId
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
|
if (!templateData) return;
|
||||||
|
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case SshCertificateOperation.SIGN_SSH_KEY: {
|
case SshCertificateOperation.SIGN_SSH_KEY: {
|
||||||
const { serialNumber, signedKey } = await signSshKey({
|
const { serialNumber, signedKey } = await signSshKey({
|
||||||
templateName,
|
templateName: templateData.name,
|
||||||
publicKey: existingPublicKey,
|
publicKey: existingPublicKey,
|
||||||
certType,
|
certType,
|
||||||
principals: principals.split(",").map((user) => user.trim()),
|
principals: principals.split(",").map((user) => user.trim()),
|
||||||
@@ -129,7 +137,7 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
case SshCertificateOperation.ISSUE_SSH_CREDS: {
|
case SshCertificateOperation.ISSUE_SSH_CREDS: {
|
||||||
const { serialNumber, publicKey, privateKey, signedKey } = await issueSshCreds({
|
const { serialNumber, publicKey, privateKey, signedKey } = await issueSshCreds({
|
||||||
templateName,
|
templateName: templateData.name,
|
||||||
keyAlgorithm,
|
keyAlgorithm,
|
||||||
certType,
|
certType,
|
||||||
principals: principals.split(",").map((user) => user.trim()),
|
principals: principals.split(",").map((user) => user.trim()),
|
||||||
@@ -179,7 +187,7 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="templateName"
|
name="templateId"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -195,11 +203,13 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
isDisabled={Boolean(popUpData?.sshCaId)}
|
isDisabled={Boolean(popUpData?.sshCaId)}
|
||||||
>
|
>
|
||||||
{(templatesData?.certificateTemplates || []).map(({ id, name }) => (
|
{(templatesData?.certificateTemplates || [])
|
||||||
<SelectItem value={name} key={`ssh-cert-template-${id}`}>
|
.filter((template) => template.status === SshCertTemplateStatus.ACTIVE)
|
||||||
{name}
|
.map(({ id, name }) => (
|
||||||
</SelectItem>
|
<SelectItem value={id} key={`ssh-cert-template-${id}`}>
|
||||||
))}
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -235,8 +245,12 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
onValueChange={(e) => onChange(e)}
|
onValueChange={(e) => onChange(e)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<SelectItem value={SshCertType.USER}>User</SelectItem>
|
{templateData && templateData.allowUserCertificates && (
|
||||||
<SelectItem value={SshCertType.HOST}>Host</SelectItem>
|
<SelectItem value={SshCertType.USER}>User</SelectItem>
|
||||||
|
)}
|
||||||
|
{templateData && templateData.allowHostCertificates && (
|
||||||
|
<SelectItem value={SshCertType.HOST}>Host</SelectItem>
|
||||||
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -308,15 +322,17 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
{templateData && templateData.allowCustomKeyIds && (
|
||||||
control={control}
|
<Controller
|
||||||
name="keyId"
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
name="keyId"
|
||||||
<FormControl label="Key ID" isError={Boolean(error)} errorText={error?.message}>
|
render={({ field, fieldState: { error } }) => (
|
||||||
<Input {...field} placeholder="12345678" />
|
<FormControl label="Key ID" isError={Boolean(error)} errorText={error?.message}>
|
||||||
</FormControl>
|
<Input {...field} placeholder="12345678" />
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="mt-4 flex items-center">
|
<div className="mt-4 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { OrgPermissionCan } from "@app/components/permissions";
|
|||||||
import { DeleteActionModal, IconButton } from "@app/components/v2";
|
import { DeleteActionModal, IconButton } from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteSshCertTemplate } from "@app/hooks/api";
|
import {
|
||||||
|
SshCertTemplateStatus,
|
||||||
|
useDeleteSshCertTemplate,
|
||||||
|
useUpdateSshCertTemplate} from "@app/hooks/api";
|
||||||
|
|
||||||
import { SshCertificateModal } from "./SshCertificateModal";
|
import { SshCertificateModal } from "./SshCertificateModal";
|
||||||
import { SshCertificateTemplateModal } from "./SshCertificateTemplateModal";
|
import { SshCertificateTemplateModal } from "./SshCertificateTemplateModal";
|
||||||
@@ -19,12 +22,14 @@ type Props = {
|
|||||||
export const SshCertificateTemplatesSection = ({ caId }: Props) => {
|
export const SshCertificateTemplatesSection = ({ caId }: Props) => {
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"sshCertificateTemplate",
|
"sshCertificateTemplate",
|
||||||
|
"sshCertificateTemplateStatus",
|
||||||
"sshCertificate",
|
"sshCertificate",
|
||||||
"deleteSshCertificateTemplate",
|
"deleteSshCertificateTemplate",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const { mutateAsync: deleteSshCertTemplate } = useDeleteSshCertTemplate();
|
const { mutateAsync: deleteSshCertTemplate } = useDeleteSshCertTemplate();
|
||||||
|
const { mutateAsync: updateSshCertTemplate } = useUpdateSshCertTemplate();
|
||||||
|
|
||||||
const onRemoveSshCertificateTemplateSubmit = async (id: string) => {
|
const onRemoveSshCertificateTemplateSubmit = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -47,6 +52,35 @@ export const SshCertificateTemplatesSection = ({ caId }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onUpdateSshCaStatus = async ({
|
||||||
|
certTemplateId,
|
||||||
|
status
|
||||||
|
}: {
|
||||||
|
certTemplateId: string;
|
||||||
|
status: SshCertTemplateStatus;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
await updateSshCertTemplate({ id: certTemplateId, status });
|
||||||
|
|
||||||
|
await createNotification({
|
||||||
|
text: `Successfully ${
|
||||||
|
status === SshCertTemplateStatus.ACTIVE ? "enabled" : "disabled"
|
||||||
|
} SSH certificate template`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("sshCertificateTemplateStatus");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to ${
|
||||||
|
status === SshCertTemplateStatus.ACTIVE ? "enabled" : "disabled"
|
||||||
|
} SSH certificate template`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="h-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
@@ -90,6 +124,37 @@ export const SshCertificateTemplatesSection = ({ caId }: Props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.sshCertificateTemplateStatus.isOpen}
|
||||||
|
title={`Are you sure want to ${
|
||||||
|
(popUp?.sshCertificateTemplateStatus?.data as { status: string })?.status ===
|
||||||
|
SshCertTemplateStatus.ACTIVE
|
||||||
|
? "enable"
|
||||||
|
: "disable"
|
||||||
|
} this certificate template?`}
|
||||||
|
subTitle={
|
||||||
|
(popUp?.sshCertificateTemplateStatus?.data as { status: string })?.status ===
|
||||||
|
SshCertTemplateStatus.ACTIVE
|
||||||
|
? "This action will allow certificate issuance under this template again."
|
||||||
|
: "This action will prevent certificate issuance under this template."
|
||||||
|
}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("sshCertificateTemplateStatus", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onUpdateSshCaStatus(
|
||||||
|
popUp?.sshCertificateTemplateStatus?.data as {
|
||||||
|
certTemplateId: string;
|
||||||
|
status: SshCertTemplateStatus;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
buttonText={
|
||||||
|
(popUp?.sshCertificateTemplateStatus?.data as { status: string })?.status ===
|
||||||
|
SshCertTemplateStatus.ACTIVE
|
||||||
|
? "Enable"
|
||||||
|
: "Disable"
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { faCertificate, faEllipsis, faFileAlt, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faBan,
|
||||||
|
faCertificate,
|
||||||
|
faEllipsis,
|
||||||
|
faFileAlt,
|
||||||
|
faTrash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -20,19 +27,28 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { useGetSshCaCertTemplates } from "@app/hooks/api";
|
import { SshCertTemplateStatus,useGetSshCaCertTemplates } from "@app/hooks/api";
|
||||||
|
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
sshCaId: string;
|
sshCaId: string;
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<
|
popUpName: keyof UsePopUpState<
|
||||||
["sshCertificateTemplate", "sshCertificate", "deleteSshCertificateTemplate", "upgradePlan"]
|
[
|
||||||
|
"sshCertificateTemplate",
|
||||||
|
"sshCertificateTemplateStatus",
|
||||||
|
"sshCertificate",
|
||||||
|
"deleteSshCertificateTemplate",
|
||||||
|
"upgradePlan"
|
||||||
|
]
|
||||||
>,
|
>,
|
||||||
data?: {
|
data?: {
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
sshCaId?: string;
|
sshCaId?: string;
|
||||||
|
certTemplateId?: string;
|
||||||
|
status?: SshCertTemplateStatus;
|
||||||
templateName?: string;
|
templateName?: string;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
@@ -40,7 +56,6 @@ type Props = {
|
|||||||
|
|
||||||
export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props) => {
|
export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props) => {
|
||||||
const { data, isLoading } = useGetSshCaCertTemplates(sshCaId);
|
const { data, isLoading } = useGetSshCaCertTemplates(sshCaId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
@@ -48,6 +63,7 @@ export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props
|
|||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
<Th />
|
<Th />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -58,6 +74,11 @@ export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props
|
|||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`certificate-${certificateTemplate.id}`}>
|
<Tr className="h-10" key={`certificate-${certificateTemplate.id}`}>
|
||||||
<Td>{certificateTemplate.name}</Td>
|
<Td>{certificateTemplate.name}</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant={getCaStatusBadgeVariant(certificateTemplate.status)}>
|
||||||
|
{caStatusToNameMap[certificateTemplate.status]}
|
||||||
|
</Badge>
|
||||||
|
</Td>
|
||||||
<Td className="flex justify-end">
|
<Td className="flex justify-end">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
@@ -68,6 +89,36 @@ export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.SshCertificateTemplates}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("sshCertificateTemplateStatus", {
|
||||||
|
certTemplateId: certificateTemplate.id,
|
||||||
|
status:
|
||||||
|
certificateTemplate.status === SshCertTemplateStatus.ACTIVE
|
||||||
|
? SshCertTemplateStatus.DISABLED
|
||||||
|
: SshCertTemplateStatus.ACTIVE
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faBan} />}
|
||||||
|
>
|
||||||
|
{`${
|
||||||
|
certificateTemplate.status === SshCertTemplateStatus.ACTIVE
|
||||||
|
? "Disable"
|
||||||
|
: "Enable"
|
||||||
|
} Template`}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.SshCertificateTemplates}
|
a={OrgPermissionSubjects.SshCertificateTemplates}
|
||||||
@@ -83,7 +134,7 @@ export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props
|
|||||||
<FontAwesomeIcon icon={faCertificate} size="sm" className="mr-1" />
|
<FontAwesomeIcon icon={faCertificate} size="sm" className="mr-1" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Issue SSH Certificate
|
Issue Certificate
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
|
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
Pagination,
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
@@ -15,7 +17,8 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { useListOrgSshCertificates } from "@app/hooks/api";
|
import { useListOrgSshCertificates } from "@app/hooks/api";
|
||||||
import { sshCertTypeToNameMap } from "@app/hooks/api/ssh-ca/constants";
|
|
||||||
|
import { getSshCertStatusBadgeDetails } from "./SshCertificatesTable.utils";
|
||||||
|
|
||||||
const PER_PAGE_INIT = 25;
|
const PER_PAGE_INIT = 25;
|
||||||
|
|
||||||
@@ -30,27 +33,38 @@ export const SshCertificatesTable = () => {
|
|||||||
limit: perPage
|
limit: perPage
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("SSH Certificates Table data: ", data);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Serial Number</Th>
|
|
||||||
<Th>Certificate Type</Th>
|
|
||||||
<Th>Principals</Th>
|
<Th>Principals</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
|
<Th>Not Before</Th>
|
||||||
|
<Th>Not After</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={4} innerKey="org-ssh-certificates" />}
|
{isLoading && <TableSkeleton columns={4} innerKey="org-ssh-certificates" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data?.certificates?.map((certificate) => {
|
data?.certificates?.map((certificate) => {
|
||||||
|
const { variant, label } = getSshCertStatusBadgeDetails(certificate.notAfter);
|
||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||||
<Td>{certificate.serialNumber}</Td>
|
|
||||||
<Td>{sshCertTypeToNameMap[certificate.certType]}</Td>
|
|
||||||
<Td>{certificate.principals.join(", ")}</Td>
|
<Td>{certificate.principals.join(", ")}</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant={variant}>{label}</Badge>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{certificate.notBefore
|
||||||
|
? format(new Date(certificate.notBefore), "yyyy-MM-dd")
|
||||||
|
: "-"}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{certificate.notAfter
|
||||||
|
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
|
||||||
|
: "-"}
|
||||||
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export const getSshCertStatusBadgeDetails = (notAfter: string) => {
|
||||||
|
const currentDate = new Date().getTime();
|
||||||
|
const notAfterDate = new Date(notAfter).getTime();
|
||||||
|
|
||||||
|
let variant: "success" | "primary" | "danger" = "success";
|
||||||
|
let label = "Active";
|
||||||
|
|
||||||
|
if (notAfterDate > currentDate) {
|
||||||
|
variant = "success";
|
||||||
|
label = "Active";
|
||||||
|
} else {
|
||||||
|
variant = "danger";
|
||||||
|
label = "Expired";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { variant, label };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user