mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 09:26:47 +00:00
Finish preliminary loop on SSH certificates
This commit is contained in:
@@ -17,7 +17,8 @@ RUN apk --update add \
|
|||||||
openssl-dev \
|
openssl-dev \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++
|
g++ \
|
||||||
|
openssh
|
||||||
|
|
||||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
|
|||||||
Vendored
+4
@@ -29,6 +29,8 @@ import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-ap
|
|||||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||||
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
||||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
|
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||||
|
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||||
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
@@ -168,6 +170,8 @@ declare module "fastify" {
|
|||||||
auditLogStream: TAuditLogStreamServiceFactory;
|
auditLogStream: TAuditLogStreamServiceFactory;
|
||||||
certificate: TCertificateServiceFactory;
|
certificate: TCertificateServiceFactory;
|
||||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||||
|
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
||||||
|
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
||||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
certificateEst: TCertificateEstServiceFactory;
|
certificateEst: TCertificateEstServiceFactory;
|
||||||
|
|||||||
Vendored
+24
@@ -311,6 +311,15 @@ import {
|
|||||||
TSlackIntegrations,
|
TSlackIntegrations,
|
||||||
TSlackIntegrationsInsert,
|
TSlackIntegrationsInsert,
|
||||||
TSlackIntegrationsUpdate,
|
TSlackIntegrationsUpdate,
|
||||||
|
TSshCertificateAuthorities,
|
||||||
|
TSshCertificateAuthoritiesInsert,
|
||||||
|
TSshCertificateAuthoritiesUpdate,
|
||||||
|
TSshCertificateAuthoritySecrets,
|
||||||
|
TSshCertificateAuthoritySecretsInsert,
|
||||||
|
TSshCertificateAuthoritySecretsUpdate,
|
||||||
|
TSshCertificateTemplates,
|
||||||
|
TSshCertificateTemplatesInsert,
|
||||||
|
TSshCertificateTemplatesUpdate,
|
||||||
TSuperAdmin,
|
TSuperAdmin,
|
||||||
TSuperAdminInsert,
|
TSuperAdminInsert,
|
||||||
TSuperAdminUpdate,
|
TSuperAdminUpdate,
|
||||||
@@ -372,6 +381,21 @@ declare module "knex/types/tables" {
|
|||||||
interface Tables {
|
interface Tables {
|
||||||
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||||
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||||
|
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateAuthorities,
|
||||||
|
TSshCertificateAuthoritiesInsert,
|
||||||
|
TSshCertificateAuthoritiesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshCertificateAuthoritySecret]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateAuthoritySecrets,
|
||||||
|
TSshCertificateAuthoritySecretsInsert,
|
||||||
|
TSshCertificateAuthoritySecretsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshCertificateTemplate]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateTemplates,
|
||||||
|
TSshCertificateTemplatesInsert,
|
||||||
|
TSshCertificateTemplatesUpdate
|
||||||
|
>;
|
||||||
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
TCertificateAuthorities,
|
TCertificateAuthorities,
|
||||||
TCertificateAuthoritiesInsert,
|
TCertificateAuthoritiesInsert,
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificateAuthority))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.string("status").notNullable(); // active / disabled
|
||||||
|
t.string("friendlyName").notNullable();
|
||||||
|
t.string("keyAlgorithm").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateAuthority);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificateAuthoritySecret))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateAuthoritySecret, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshCaId").notNullable().unique();
|
||||||
|
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedPrivateKey").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateAuthoritySecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificateTemplate))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateTemplate, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshCaId").notNullable();
|
||||||
|
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.string("name").notNullable(); // note: how do we handle this being unique? across orgs?
|
||||||
|
t.string("ttl").notNullable();
|
||||||
|
t.string("maxTTL").notNullable();
|
||||||
|
t.specificType("allowedUsers", "text[]").notNullable();
|
||||||
|
t.specificType("allowedHosts", "text[]").notNullable();
|
||||||
|
t.boolean("allowUserCertificates").notNullable();
|
||||||
|
t.boolean("allowHostCertificates").notNullable();
|
||||||
|
t.boolean("allowCustomKeyIds").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateTemplate);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateAuthoritySecret);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateAuthoritySecret);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateAuthority);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateAuthority);
|
||||||
|
}
|
||||||
@@ -105,6 +105,9 @@ export * from "./secrets";
|
|||||||
export * from "./secrets-v2";
|
export * from "./secrets-v2";
|
||||||
export * from "./service-tokens";
|
export * from "./service-tokens";
|
||||||
export * from "./slack-integrations";
|
export * from "./slack-integrations";
|
||||||
|
export * from "./ssh-certificate-authorities";
|
||||||
|
export * from "./ssh-certificate-authority-secrets";
|
||||||
|
export * from "./ssh-certificate-templates";
|
||||||
export * from "./super-admin";
|
export * from "./super-admin";
|
||||||
export * from "./totp-configs";
|
export * from "./totp-configs";
|
||||||
export * from "./trusted-ips";
|
export * from "./trusted-ips";
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export enum TableName {
|
export enum TableName {
|
||||||
Users = "users",
|
Users = "users",
|
||||||
|
SshCertificateAuthority = "ssh_certificate_authorities",
|
||||||
|
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
|
||||||
|
SshCertificateTemplate = "ssh_certificate_templates",
|
||||||
CertificateAuthority = "certificate_authorities",
|
CertificateAuthority = "certificate_authorities",
|
||||||
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
||||||
CertificateAuthorityCert = "certificate_authority_certs",
|
CertificateAuthorityCert = "certificate_authority_certs",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SshCertificateAuthoritiesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
status: z.string(),
|
||||||
|
friendlyName: z.string(),
|
||||||
|
keyAlgorithm: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
|
||||||
|
export type TSshCertificateAuthoritiesInsert = Omit<z.input<typeof SshCertificateAuthoritiesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshCertificateAuthoritiesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshCertificateAuthoritiesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SshCertificateAuthoritySecretsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshCaId: z.string().uuid(),
|
||||||
|
encryptedPrivateKey: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateAuthoritySecrets = z.infer<typeof SshCertificateAuthoritySecretsSchema>;
|
||||||
|
export type TSshCertificateAuthoritySecretsInsert = Omit<
|
||||||
|
z.input<typeof SshCertificateAuthoritySecretsSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TSshCertificateAuthoritySecretsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshCertificateAuthoritySecretsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SshCertificateTemplatesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshCaId: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
ttl: z.string(),
|
||||||
|
maxTTL: z.string(),
|
||||||
|
allowedUsers: z.string().array(),
|
||||||
|
allowedHosts: z.string().array(),
|
||||||
|
allowUserCertificates: z.boolean(),
|
||||||
|
allowHostCertificates: z.boolean(),
|
||||||
|
allowCustomKeyIds: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateTemplates = z.infer<typeof SshCertificateTemplatesSchema>;
|
||||||
|
export type TSshCertificateTemplatesInsert = Omit<z.input<typeof SshCertificateTemplatesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshCertificateTemplatesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshCertificateTemplatesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -25,6 +25,9 @@ import { registerSecretRotationRouter } from "./secret-rotation-router";
|
|||||||
import { registerSecretScanningRouter } from "./secret-scanning-router";
|
import { registerSecretScanningRouter } from "./secret-scanning-router";
|
||||||
import { registerSecretVersionRouter } from "./secret-version-router";
|
import { registerSecretVersionRouter } from "./secret-version-router";
|
||||||
import { registerSnapshotRouter } from "./snapshot-router";
|
import { registerSnapshotRouter } from "./snapshot-router";
|
||||||
|
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
|
||||||
|
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
||||||
|
import { registerSshRouter } from "./ssh-router";
|
||||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||||
|
|
||||||
@@ -68,6 +71,15 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/pki" }
|
{ prefix: "/pki" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (sshRouter) => {
|
||||||
|
await sshRouter.register(registerSshRouter, { prefix: "/" });
|
||||||
|
await sshRouter.register(registerSshCaRouter, { prefix: "/ca" });
|
||||||
|
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||||
|
},
|
||||||
|
{ prefix: "/ssh" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (ssoRouter) => {
|
async (ssoRouter) => {
|
||||||
await ssoRouter.register(registerSamlRouter);
|
await ssoRouter.register(registerSamlRouter);
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||||
|
import { SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||||
|
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Create SSH CA",
|
||||||
|
body: z.object({
|
||||||
|
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
||||||
|
keyAlgorithm: z
|
||||||
|
.nativeEnum(CertKeyAlgorithm)
|
||||||
|
.default(CertKeyAlgorithm.RSA_2048)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.createSshCa({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: ca.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshCaId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.getSshCaById({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: ca.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:sshCaId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Update SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.sshCaId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
status: z
|
||||||
|
.enum([SshCaStatus.ACTIVE, SshCaStatus.DISABLED])
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.status)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.updateSshCaById({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: ca.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName,
|
||||||
|
status: ca.status as SshCaStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:sshCaId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Delete SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.DELETE.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.deleteSshCaById({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: ca.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshCaId/certificate-templates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get list of certificate templates for the SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_CERTIFICATE_TEMPLATES.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificateTemplates: sanitizedSshCertificateTemplate.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplates, ca } = await server.services.sshCertificateAuthority.getSshCaCertificateTemplates({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: ca.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_CA_CERTIFICATE_TEMPLATES,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateTemplates
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
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 {
|
||||||
|
isValidHostPattern,
|
||||||
|
isValidUserPattern
|
||||||
|
} from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators";
|
||||||
|
import { SSH_CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerSshCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:certificateTemplateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.GET.certificateTemplateId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const certificateTemplate = await server.services.sshCertificateTemplate.getSshCertTemplate({
|
||||||
|
id: req.params.certificateTemplateId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: certificateTemplate.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
sshCaId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.sshCaId),
|
||||||
|
name: z.string().min(1).describe(SSH_CERTIFICATE_TEMPLATES.CREATE.name),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.default("1h")
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.ttl),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "Max TTL must be a positive number")
|
||||||
|
.default("30d")
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.maxTTL),
|
||||||
|
allowedUsers: z
|
||||||
|
.array(z.string().refine(isValidUserPattern, "Invalid user pattern"))
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowedUsers),
|
||||||
|
allowedHosts: z
|
||||||
|
.array(z.string().refine(isValidHostPattern, "Invalid host pattern"))
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowedHosts),
|
||||||
|
allowUserCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowUserCertificates),
|
||||||
|
allowHostCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowHostCertificates),
|
||||||
|
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplate, ca } = await server.services.sshCertificateTemplate.createSshCertTemplate({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: ca.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
sshCaId: ca.id,
|
||||||
|
name: certificateTemplate.name,
|
||||||
|
ttl: certificateTemplate.ttl,
|
||||||
|
maxTTL: certificateTemplate.maxTTL,
|
||||||
|
allowedUsers: certificateTemplate.allowedUsers,
|
||||||
|
allowedHosts: certificateTemplate.allowedHosts,
|
||||||
|
allowUserCertificates: certificateTemplate.allowUserCertificates,
|
||||||
|
allowHostCertificates: certificateTemplate.allowHostCertificates,
|
||||||
|
allowCustomKeyIds: certificateTemplate.allowCustomKeyIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:certificateTemplateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().min(1).optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.name),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.ttl),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "Max TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.maxTTL),
|
||||||
|
allowedUsers: z
|
||||||
|
.array(z.string().refine(isValidUserPattern, "Invalid user pattern"))
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowedUsers),
|
||||||
|
allowedHosts: z
|
||||||
|
.array(z.string().refine(isValidHostPattern, "Invalid host pattern"))
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowedHosts),
|
||||||
|
allowUserCertificates: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowUserCertificates),
|
||||||
|
allowHostCertificates: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowHostCertificates),
|
||||||
|
allowCustomKeyIds: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowCustomKeyIds)
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplate, orgId } = await server.services.sshCertificateTemplate.updateSshCertTemplate({
|
||||||
|
...req.body,
|
||||||
|
id: req.params.certificateTemplateId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
sshCaId: certificateTemplate.sshCaId,
|
||||||
|
name: certificateTemplate.name,
|
||||||
|
ttl: certificateTemplate.ttl,
|
||||||
|
maxTTL: certificateTemplate.maxTTL,
|
||||||
|
allowedUsers: certificateTemplate.allowedUsers,
|
||||||
|
allowedHosts: certificateTemplate.allowedHosts,
|
||||||
|
allowUserCertificates: certificateTemplate.allowUserCertificates,
|
||||||
|
allowHostCertificates: certificateTemplate.allowHostCertificates,
|
||||||
|
allowCustomKeyIds: certificateTemplate.allowCustomKeyIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:certificateTemplateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const certificateTemplate = await server.services.sshCertificateTemplate.deleteSshCertTemplate({
|
||||||
|
id: req.params.certificateTemplateId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: certificateTemplate.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { CERTIFICATE_AUTHORITIES, CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; // TODO: update to SSH CA
|
||||||
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
export const registerSshRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/sign",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Sign SSH public key",
|
||||||
|
body: z.object({
|
||||||
|
name: z.string(), // name of SSH certificate template
|
||||||
|
publicKey: z.string(),
|
||||||
|
certType: z.nativeEnum(SshCertType).default(SshCertType.USER),
|
||||||
|
principals: z.array(z.string().transform((val) => val.trim())).nonempty("Principals array must not be empty"),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl),
|
||||||
|
keyId: z.string().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string(),
|
||||||
|
signedKey: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { serialNumber, signedPublicKey, certificateTemplate, ttl, keyId } =
|
||||||
|
await server.services.sshCertificateAuthority.signSshKey({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SIGN_SSH_KEY,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
certType: req.body.certType,
|
||||||
|
principals: req.body.principals,
|
||||||
|
ttl: String(ttl),
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedKey: signedPublicKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/issue",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Issue SSH credentials (certificate + key)",
|
||||||
|
body: z.object({
|
||||||
|
name: z.string(), // name of SSH certificate template
|
||||||
|
keyAlgorithm: z
|
||||||
|
.nativeEnum(CertKeyAlgorithm)
|
||||||
|
.default(CertKeyAlgorithm.RSA_2048)
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm),
|
||||||
|
certType: z.nativeEnum(SshCertType).default(SshCertType.USER),
|
||||||
|
principals: z.array(z.string().transform((val) => val.trim())).nonempty("Principals array must not be empty"),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl),
|
||||||
|
keyId: z.string().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string(),
|
||||||
|
signedKey: z.string(),
|
||||||
|
privateKey: z.string(),
|
||||||
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ISSUE_SSH_CREDS,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm,
|
||||||
|
certType: req.body.certType,
|
||||||
|
principals: req.body.principals,
|
||||||
|
ttl: String(ttl),
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedKey: signedPublicKey,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -2,9 +2,11 @@ import {
|
|||||||
TCreateProjectTemplateDTO,
|
TCreateProjectTemplateDTO,
|
||||||
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 { 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";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||||
@@ -137,6 +139,17 @@ export enum EventType {
|
|||||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||||
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
||||||
|
SIGN_SSH_KEY = "sign-ssh-key",
|
||||||
|
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
||||||
|
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
||||||
|
GET_SSH_CA = "get-ssh-certificate-authority",
|
||||||
|
UPDATE_SSH_CA = "update-ssh-certificate-authority",
|
||||||
|
DELETE_SSH_CA = "delete-ssh-certificate-authority",
|
||||||
|
GET_SSH_CA_CERTIFICATE_TEMPLATES = "get-ssh-certificate-authority-certificate-templates",
|
||||||
|
CREATE_SSH_CERTIFICATE_TEMPLATE = "create-ssh-certificate-template",
|
||||||
|
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
||||||
|
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
||||||
|
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
||||||
CREATE_CA = "create-certificate-authority",
|
CREATE_CA = "create-certificate-authority",
|
||||||
GET_CA = "get-certificate-authority",
|
GET_CA = "get-certificate-authority",
|
||||||
UPDATE_CA = "update-certificate-authority",
|
UPDATE_CA = "update-certificate-authority",
|
||||||
@@ -1132,6 +1145,116 @@ interface SecretApprovalRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SignSshKey {
|
||||||
|
type: EventType.SIGN_SSH_KEY;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl: string;
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IssueSshCreds {
|
||||||
|
type: EventType.ISSUE_SSH_CREDS;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl: string;
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSshCa {
|
||||||
|
type: EventType.CREATE_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshCa {
|
||||||
|
type: EventType.GET_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSshCa {
|
||||||
|
type: EventType.UPDATE_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
status: SshCaStatus;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSshCa {
|
||||||
|
type: EventType.DELETE_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshCaCertificateTemplates {
|
||||||
|
type: EventType.GET_SSH_CA_CERTIFICATE_TEMPLATES;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSshCertificateTemplate {
|
||||||
|
type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshCertificateTemplate {
|
||||||
|
type: EventType.GET_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSshCertificateTemplate {
|
||||||
|
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSshCertificateTemplate {
|
||||||
|
type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateCa {
|
interface CreateCa {
|
||||||
type: EventType.CREATE_CA;
|
type: EventType.CREATE_CA;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -1757,6 +1880,17 @@ export type Event =
|
|||||||
| SecretApprovalClosed
|
| SecretApprovalClosed
|
||||||
| SecretApprovalRequest
|
| SecretApprovalRequest
|
||||||
| SecretApprovalReopened
|
| SecretApprovalReopened
|
||||||
|
| SignSshKey
|
||||||
|
| IssueSshCreds
|
||||||
|
| CreateSshCa
|
||||||
|
| GetSshCa
|
||||||
|
| UpdateSshCa
|
||||||
|
| DeleteSshCa
|
||||||
|
| GetSshCaCertificateTemplates
|
||||||
|
| CreateSshCertificateTemplate
|
||||||
|
| UpdateSshCertificateTemplate
|
||||||
|
| GetSshCertificateTemplate
|
||||||
|
| DeleteSshCertificateTemplate
|
||||||
| CreateCa
|
| CreateCa
|
||||||
| GetCa
|
| GetCa
|
||||||
| UpdateCa
|
| UpdateCa
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export enum OrgPermissionSubjects {
|
|||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
AdminConsole = "organization-admin-console",
|
AdminConsole = "organization-admin-console",
|
||||||
AuditLogs = "audit-logs",
|
AuditLogs = "audit-logs",
|
||||||
ProjectTemplates = "project-templates"
|
ProjectTemplates = "project-templates",
|
||||||
|
SshCertificateAuthorities = "ssh-certificate-authorities",
|
||||||
|
SshCertificateTemplates = "ssh-certificate-templates"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -46,7 +48,9 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateTemplates];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
@@ -123,6 +127,16 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificateAuthorities);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateAuthorities);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateAuthorities);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificateTemplates);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateTemplates);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateTemplates);
|
||||||
|
|
||||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
@@ -153,6 +167,9 @@ const buildMemberPermission = () => {
|
|||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities);
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateTemplateDALFactory = ReturnType<typeof sshCertificateTemplateDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateTemplateDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCertificateTemplateOrm = ormify(db, TableName.SshCertificateTemplate);
|
||||||
|
|
||||||
|
const getById = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const certTemplate = await (tx || db.replicaNode())(TableName.SshCertificateTemplate)
|
||||||
|
.join(
|
||||||
|
TableName.SshCertificateAuthority,
|
||||||
|
`${TableName.SshCertificateAuthority}.id`,
|
||||||
|
`${TableName.SshCertificateTemplate}.sshCaId`
|
||||||
|
)
|
||||||
|
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.SshCertificateAuthority}.orgId`)
|
||||||
|
.where(`${TableName.SshCertificateTemplate}.id`, "=", id)
|
||||||
|
.select(selectAllTableCols(TableName.SshCertificateTemplate))
|
||||||
|
.select(
|
||||||
|
db.ref("orgId").withSchema(TableName.SshCertificateAuthority),
|
||||||
|
db.ref("friendlyName").as("caName").withSchema(TableName.SshCertificateAuthority),
|
||||||
|
db.ref("status").as("caStatus").withSchema(TableName.SshCertificateAuthority)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return certTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Get SSH certificate template by ID" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...sshCertificateTemplateOrm, getById };
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { SshCertificateTemplatesSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshCertificateTemplate = SshCertificateTemplatesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
sshCaId: true,
|
||||||
|
name: true,
|
||||||
|
ttl: true,
|
||||||
|
maxTTL: true,
|
||||||
|
allowedUsers: true,
|
||||||
|
allowedHosts: true,
|
||||||
|
allowCustomKeyIds: true,
|
||||||
|
allowUserCertificates: true,
|
||||||
|
allowHostCertificates: true
|
||||||
|
});
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
||||||
|
import {
|
||||||
|
TCreateSshCertTemplateDTO,
|
||||||
|
TDeleteSshCertTemplateDTO,
|
||||||
|
TGetSshCertTemplateDTO,
|
||||||
|
TUpdateSshCertTemplateDTO
|
||||||
|
} from "./ssh-certificate-template-types";
|
||||||
|
|
||||||
|
type TSshCertificateTemplateServiceFactoryDep = {
|
||||||
|
sshCertificateTemplateDAL: TSshCertificateTemplateDALFactory;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSshCertificateTemplateServiceFactory = ReturnType<typeof sshCertificateTemplateServiceFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateTemplateServiceFactory = ({
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
permissionService
|
||||||
|
}: TSshCertificateTemplateServiceFactoryDep) => {
|
||||||
|
const createSshCertTemplate = async ({
|
||||||
|
sshCaId,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateSshCertTemplateDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(sshCaId);
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH CA with ID ${sshCaId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Create,
|
||||||
|
OrgPermissionSubjects.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ms(ttl) > ms(maxTTL)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TTL cannot be greater than max TTL"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTemplate = await sshCertificateTemplateDAL.create({
|
||||||
|
sshCaId,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds
|
||||||
|
});
|
||||||
|
|
||||||
|
return { certificateTemplate, ca };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSshCertTemplate = async ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateSshCertTemplateDTO) => {
|
||||||
|
const certTemplate = await sshCertificateTemplateDAL.getById(id);
|
||||||
|
if (!certTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH certificate template with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
certTemplate.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Edit,
|
||||||
|
OrgPermissionSubjects.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TTL cannot be greater than max TTL"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTemplate = await sshCertificateTemplateDAL.updateById(id, {
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateTemplate,
|
||||||
|
orgId: certTemplate.orgId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSshCertTemplate = async ({
|
||||||
|
id,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TDeleteSshCertTemplateDTO) => {
|
||||||
|
const certificateTemplate = await sshCertificateTemplateDAL.getById(id);
|
||||||
|
if (!certificateTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH certificate template with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
certificateTemplate.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Delete,
|
||||||
|
OrgPermissionSubjects.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshCertificateTemplateDAL.deleteById(certificateTemplate.id);
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshCertTemplateDTO) => {
|
||||||
|
const certTemplate = await sshCertificateTemplateDAL.getById(id);
|
||||||
|
if (!certTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH certificate template with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
certTemplate.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Read,
|
||||||
|
OrgPermissionSubjects.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
return certTemplate;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSshCertTemplate,
|
||||||
|
updateSshCertTemplate,
|
||||||
|
deleteSshCertTemplate,
|
||||||
|
getSshCertTemplate
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateSshCertTemplateDTO = {
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateSshCertTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
ttl?: string;
|
||||||
|
maxTTL?: string;
|
||||||
|
allowUserCertificates?: boolean;
|
||||||
|
allowHostCertificates?: boolean;
|
||||||
|
allowedUsers?: string[];
|
||||||
|
allowedHosts?: string[];
|
||||||
|
allowCustomKeyIds?: boolean;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetSshCertTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteSshCertTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
// Validates usernames or wildcard (*)
|
||||||
|
export const isValidUserPattern = (value: string): boolean => {
|
||||||
|
// Matches valid Linux usernames or a wildcard (*)
|
||||||
|
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
|
||||||
|
return userRegex.test(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validates hostnames, wildcard domains, or IP addresses
|
||||||
|
export const isValidHostPattern = (value: string): boolean => {
|
||||||
|
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
|
||||||
|
const hostRegex =
|
||||||
|
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
|
||||||
|
return hostRegex.test(value);
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateAuthorityDALFactory = ReturnType<typeof sshCertificateAuthorityDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateAuthorityDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCaOrm = ormify(db, TableName.SshCertificateAuthority);
|
||||||
|
return sshCaOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { execSync } from "child_process";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import fs from "fs";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isValidHostPattern,
|
||||||
|
isValidUserPattern
|
||||||
|
} from "../ssh-certificate-template/ssh-certificate-template-validators";
|
||||||
|
import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types";
|
||||||
|
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
export const createSshCertSerialNumber = () => {
|
||||||
|
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
|
||||||
|
randomBytes[0] &= 0x7f; // Ensure the most significant bit is 0 (to stay within unsigned range)
|
||||||
|
return BigInt(`0x${randomBytes.toString("hex")}`).toString(10); // Convert to decimal
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
|
||||||
|
* We use this function because the key format generated by `ssh-keygen` is unique.
|
||||||
|
*/
|
||||||
|
export const createSshKeyPair = (keyAlgorithm: CertKeyAlgorithm, comment: string) => {
|
||||||
|
const uniqueId = crypto.randomBytes(8).toString("hex"); // to avoid collions if high-volume key generation
|
||||||
|
const privateKeyFile = `ssh_key_${uniqueId}`; // temp key path
|
||||||
|
const publicKeyFile = `${privateKeyFile}.pub`;
|
||||||
|
|
||||||
|
if (fs.existsSync(publicKeyFile)) fs.unlinkSync(publicKeyFile);
|
||||||
|
if (fs.existsSync(privateKeyFile)) fs.unlinkSync(privateKeyFile);
|
||||||
|
|
||||||
|
let keyType = "";
|
||||||
|
let keyBits = "";
|
||||||
|
|
||||||
|
switch (keyAlgorithm) {
|
||||||
|
case CertKeyAlgorithm.RSA_2048:
|
||||||
|
keyType = "rsa";
|
||||||
|
keyBits = "2048";
|
||||||
|
break;
|
||||||
|
case CertKeyAlgorithm.RSA_4096:
|
||||||
|
keyType = "rsa";
|
||||||
|
keyBits = "4096";
|
||||||
|
break;
|
||||||
|
case CertKeyAlgorithm.ECDSA_P256:
|
||||||
|
keyType = "ecdsa";
|
||||||
|
keyBits = "256";
|
||||||
|
break;
|
||||||
|
case CertKeyAlgorithm.ECDSA_P384:
|
||||||
|
keyType = "ecdsa";
|
||||||
|
keyBits = "384";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("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}"`);
|
||||||
|
|
||||||
|
const publicKey = fs.readFileSync(publicKeyFile, "utf8");
|
||||||
|
const privateKey = fs.readFileSync(privateKeyFile, "utf8");
|
||||||
|
|
||||||
|
fs.unlinkSync(privateKeyFile);
|
||||||
|
fs.unlinkSync(publicKeyFile);
|
||||||
|
|
||||||
|
return { publicKey, privateKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate type based on the SSH certificate template configuration.
|
||||||
|
* @param template - The SSH certificate template configuration
|
||||||
|
* @param certType - The SSH certificate type
|
||||||
|
*/
|
||||||
|
export const validateSshCertificateType = (template: TSshCertificateTemplates, certType: SshCertType) => {
|
||||||
|
if (!template.allowUserCertificates && certType === SshCertType.USER) {
|
||||||
|
throw new BadRequestError({ message: "Failed to validate user certificate type due to template restriction" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!template.allowHostCertificates && certType === SshCertType.HOST) {
|
||||||
|
throw new BadRequestError({ message: "Failed to validate host certificate type due to template restriction" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate principals based on the SSH certificate template configuration.
|
||||||
|
* @param certType - The SSH certificate type
|
||||||
|
* @param template - The SSH certificate template configuration
|
||||||
|
* @param principals - The requested SSH certificate principals
|
||||||
|
* @returns The validated SSH certificate principals
|
||||||
|
*/
|
||||||
|
export const validateSshCertificatePrincipals = (
|
||||||
|
certType: SshCertType,
|
||||||
|
template: TSshCertificateTemplates,
|
||||||
|
principals: string[]
|
||||||
|
) => {
|
||||||
|
switch (certType) {
|
||||||
|
case SshCertType.USER: {
|
||||||
|
const allowsAllUsers = template.allowedUsers?.includes("*") ?? false;
|
||||||
|
return principals.every((principal) => {
|
||||||
|
if (principal === "*") return false;
|
||||||
|
if (allowsAllUsers) return isValidUserPattern(principal);
|
||||||
|
return template.allowedUsers?.includes(principal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case SshCertType.HOST: {
|
||||||
|
const allowsAllHosts = template.allowedHosts?.includes("*") ?? false;
|
||||||
|
return principals.every((principal) => {
|
||||||
|
if (principal.includes("*")) return false;
|
||||||
|
if (allowsAllHosts) return isValidHostPattern(principal);
|
||||||
|
|
||||||
|
// Validate against allowed domains
|
||||||
|
return (
|
||||||
|
isValidHostPattern(principal) &&
|
||||||
|
template.allowedHosts?.some((allowedHost) => {
|
||||||
|
if (allowedHost.startsWith("*.")) {
|
||||||
|
// Match subdomains of a wildcard domain
|
||||||
|
const baseDomain = allowedHost.slice(2); // Remove the leading "*."
|
||||||
|
return principal.endsWith(`.${baseDomain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match for non-wildcard domains
|
||||||
|
return principal === allowedHost;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH certificate principals due to unrecognized requested certificate type"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate TTL based on the SSH certificate template configuration.
|
||||||
|
* @param template - The SSH certificate template configuration
|
||||||
|
* @param ttl - The TTL to validate
|
||||||
|
* @returns The TTL (in seconds) to use for issuing the SSH certificate
|
||||||
|
*/
|
||||||
|
export const validateSshCertificateTtl = (template: TSshCertificateTemplates, ttl: string | undefined) => {
|
||||||
|
if (!ttl) {
|
||||||
|
// use default template ttl
|
||||||
|
return ms(template.ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms(ttl) > ms(template.maxTTL)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed TTL validation due to TTL being greater than configured max TTL on template"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ms(ttl) / 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an SSH certificate for a user or host.
|
||||||
|
*/
|
||||||
|
export const createSshCert = ({ caPrivateKey, userPublicKey, keyId, principals, ttl, certType }: TCreateSshCertDTO) => {
|
||||||
|
const uniqueId = crypto.randomBytes(8).toString("hex");
|
||||||
|
const publicKeyFile = `user_key_${uniqueId}.pub`;
|
||||||
|
const privateKeyFile = `ssh_ca_key_${uniqueId}`;
|
||||||
|
|
||||||
|
if (fs.existsSync(publicKeyFile)) fs.unlinkSync(publicKeyFile);
|
||||||
|
if (fs.existsSync(privateKeyFile)) fs.unlinkSync(privateKeyFile);
|
||||||
|
|
||||||
|
// write public and private keys to temp files
|
||||||
|
fs.writeFileSync(publicKeyFile, userPublicKey);
|
||||||
|
fs.writeFileSync(privateKeyFile, caPrivateKey);
|
||||||
|
fs.chmodSync(privateKeyFile, 0o600);
|
||||||
|
|
||||||
|
const serialNumber = createSshCertSerialNumber();
|
||||||
|
console.log("signSshKey serialNumber: ", serialNumber);
|
||||||
|
|
||||||
|
const certOptions = [
|
||||||
|
`-s ${privateKeyFile}`, // path to SSH CA private key
|
||||||
|
`-I "${keyId}"`, // identity for the issued certificate (key id)
|
||||||
|
`-n "${principals.join(",")}"`, // principal(s) that is user(s) or host(s)
|
||||||
|
`-V +${ttl}s`, // TTL in seconds (validity period) for the issue certificate
|
||||||
|
`-z ${serialNumber}`, // custom serial number for certificate
|
||||||
|
certType === "host" ? "-h" : "", // host certificate flag
|
||||||
|
publicKeyFile // path to signed [publicKey]
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
const command = `ssh-keygen ${certOptions}`;
|
||||||
|
|
||||||
|
// Execute the signing process
|
||||||
|
execSync(command);
|
||||||
|
|
||||||
|
const signedPublicKey = fs.readFileSync(publicKeyFile, "utf8");
|
||||||
|
|
||||||
|
fs.unlinkSync(publicKeyFile);
|
||||||
|
fs.unlinkSync(privateKeyFile);
|
||||||
|
|
||||||
|
return { serialNumber, signedPublicKey };
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { SshCertificateAuthoritiesSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshCa = SshCertificateAuthoritiesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
orgId: true,
|
||||||
|
friendlyName: true,
|
||||||
|
status: true,
|
||||||
|
keyAlgorithm: true
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateAuthoritySecretDALFactory = ReturnType<typeof sshCertificateAuthoritySecretDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateAuthoritySecretDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCaSecretOrm = ormify(db, TableName.SshCertificateAuthoritySecret);
|
||||||
|
return sshCaSecretOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
|
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createSshCert,
|
||||||
|
createSshKeyPair,
|
||||||
|
validateSshCertificatePrincipals,
|
||||||
|
validateSshCertificateTtl,
|
||||||
|
validateSshCertificateType
|
||||||
|
} from "./ssh-certificate-authority-fns";
|
||||||
|
import {
|
||||||
|
SshCaStatus,
|
||||||
|
TCreateSshCaDTO,
|
||||||
|
TDeleteSshCaDTO,
|
||||||
|
TGetSshCaCertificateTemplatesDTO,
|
||||||
|
TGetSshCaDTO,
|
||||||
|
TIssueSshCredsDTO,
|
||||||
|
TSignSshKeyDTO,
|
||||||
|
TUpdateSshCaDTO
|
||||||
|
} from "./ssh-certificate-authority-types";
|
||||||
|
|
||||||
|
type TSshCertificateAuthorityServiceFactoryDep = {
|
||||||
|
sshCertificateAuthorityDAL: Pick<
|
||||||
|
TSshCertificateAuthorityDALFactory,
|
||||||
|
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
|
||||||
|
>;
|
||||||
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||||
|
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find" | "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "getOrgKmsKeyId">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSshCertificateAuthorityServiceFactory = ReturnType<typeof sshCertificateAuthorityServiceFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateAuthorityServiceFactory = ({
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
}: TSshCertificateAuthorityServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Generates a new SSH CA
|
||||||
|
*/
|
||||||
|
const createSshCa = async ({
|
||||||
|
friendlyName,
|
||||||
|
keyAlgorithm,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateSshCaDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Create,
|
||||||
|
OrgPermissionSubjects.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const newCa = await sshCertificateAuthorityDAL.transaction(async (tx) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.create(
|
||||||
|
{
|
||||||
|
orgId: actorOrgId,
|
||||||
|
friendlyName: friendlyName || "",
|
||||||
|
status: SshCaStatus.ACTIVE,
|
||||||
|
keyAlgorithm
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const { privateKey } = createSshKeyPair(keyAlgorithm, ca.friendlyName);
|
||||||
|
|
||||||
|
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId);
|
||||||
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
|
kmsId: orgKmsKeyId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(privateKey, "utf8")
|
||||||
|
});
|
||||||
|
|
||||||
|
await sshCertificateAuthoritySecretDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId: ca.id,
|
||||||
|
encryptedPrivateKey
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return ca;
|
||||||
|
});
|
||||||
|
|
||||||
|
return newCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SSH CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getSshCaById = async ({ caId, actor, actorId, actorAuthMethod, actorOrgId }: TGetSshCaDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Read,
|
||||||
|
OrgPermissionSubjects.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
return ca;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update SSH CA with id [caId]
|
||||||
|
* Note: Used to enable/disable CA
|
||||||
|
*/
|
||||||
|
const updateSshCaById = async ({ caId, status, actor, actorId, actorAuthMethod, actorOrgId }: TUpdateSshCaDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Edit,
|
||||||
|
OrgPermissionSubjects.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCa = await sshCertificateAuthorityDAL.updateById(caId, { status });
|
||||||
|
|
||||||
|
return updatedCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete SSH CA with id [caId]
|
||||||
|
*/
|
||||||
|
const deleteSshCaById = async ({ caId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteSshCaDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Delete,
|
||||||
|
OrgPermissionSubjects.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedCa = await sshCertificateAuthorityDAL.deleteById(caId);
|
||||||
|
|
||||||
|
return deletedCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SSH certificate and corresponding new SSH public-private key pair where
|
||||||
|
* SSH public key is signed using CA behind SSH certificate with name [name].
|
||||||
|
*/
|
||||||
|
const issueSshCreds = async ({
|
||||||
|
name,
|
||||||
|
keyAlgorithm,
|
||||||
|
certType,
|
||||||
|
principals,
|
||||||
|
ttl: requestedTtl,
|
||||||
|
keyId: requestedKeyId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TIssueSshCredsDTO) => {
|
||||||
|
// TODO: proper permission check
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Create,
|
||||||
|
OrgPermissionSubjects.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: adjust to find within org
|
||||||
|
const sshCertificateTemplate = await sshCertificateTemplateDAL.findOne({ name });
|
||||||
|
|
||||||
|
// validate if the requested [certType] is allowed under the template configuration
|
||||||
|
validateSshCertificateType(sshCertificateTemplate, certType);
|
||||||
|
|
||||||
|
// validate if the requested [principals] are valid for the given [certType] under the template configuration
|
||||||
|
validateSshCertificatePrincipals(certType, sshCertificateTemplate, principals);
|
||||||
|
|
||||||
|
// validate if the requested TTL is valid under the template configuration
|
||||||
|
const ttl = validateSshCertificateTtl(sshCertificateTemplate, requestedTtl);
|
||||||
|
|
||||||
|
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||||
|
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||||
|
? requestedKeyId ?? `${actor}-${actorId}`
|
||||||
|
: `${actor}-${actorId}`;
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||||
|
|
||||||
|
// decrypt secret
|
||||||
|
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId);
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: orgKmsKeyId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = await kmsDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// create user key pair
|
||||||
|
const { publicKey, privateKey } = createSshKeyPair(keyAlgorithm, "Client Key");
|
||||||
|
|
||||||
|
const { serialNumber, signedPublicKey } = createSshCert({
|
||||||
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
|
userPublicKey: publicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
ttl,
|
||||||
|
certType
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedPublicKey,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
certificateTemplate: sshCertificateTemplate,
|
||||||
|
ttl,
|
||||||
|
keyId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SSH certificate by signing SSH public key [publicKey]
|
||||||
|
* using CA behind SSH certificate template with name [name]
|
||||||
|
*/
|
||||||
|
const signSshKey = async ({
|
||||||
|
name,
|
||||||
|
publicKey,
|
||||||
|
certType,
|
||||||
|
principals,
|
||||||
|
ttl: requestedTtl,
|
||||||
|
keyId: requestedKeyId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TSignSshKeyDTO) => {
|
||||||
|
// TODO: proper permission check
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Create,
|
||||||
|
OrgPermissionSubjects.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: adjust to find within org
|
||||||
|
const sshCertificateTemplate = await sshCertificateTemplateDAL.findOne({ name });
|
||||||
|
|
||||||
|
// validate if the requested [certType] is allowed under the template configuration
|
||||||
|
validateSshCertificateType(sshCertificateTemplate, certType);
|
||||||
|
|
||||||
|
// validate if the requested [principals] are valid for the given [certType] under the template configuration
|
||||||
|
validateSshCertificatePrincipals(certType, sshCertificateTemplate, principals);
|
||||||
|
|
||||||
|
// validate if the requested TTL is valid under the template configuration
|
||||||
|
const ttl = validateSshCertificateTtl(sshCertificateTemplate, requestedTtl);
|
||||||
|
|
||||||
|
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||||
|
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||||
|
? requestedKeyId ?? `${actor}-${actorId}`
|
||||||
|
: `${actor}-${actorId}`;
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||||
|
|
||||||
|
// decrypt secret
|
||||||
|
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId);
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: orgKmsKeyId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = await kmsDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const { serialNumber, signedPublicKey } = createSshCert({
|
||||||
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
|
userPublicKey: publicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
ttl,
|
||||||
|
certType
|
||||||
|
});
|
||||||
|
|
||||||
|
return { serialNumber, signedPublicKey, certificateTemplate: sshCertificateTemplate, ttl, keyId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshCaCertificateTemplates = async ({
|
||||||
|
caId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TGetSshCaCertificateTemplatesDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Read,
|
||||||
|
OrgPermissionSubjects.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
const certificateTemplates = await sshCertificateTemplateDAL.find({ sshCaId: caId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateTemplates,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
issueSshCreds,
|
||||||
|
signSshKey,
|
||||||
|
createSshCa,
|
||||||
|
getSshCaById,
|
||||||
|
updateSshCaById,
|
||||||
|
deleteSshCaById,
|
||||||
|
getSshCaCertificateTemplates
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
export enum SshCaStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SshCertType {
|
||||||
|
USER = "user",
|
||||||
|
HOST = "host"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateSshCaDTO = {
|
||||||
|
friendlyName?: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGetSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TUpdateSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
status?: SshCaStatus;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TDeleteSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TIssueSshCredsDTO = {
|
||||||
|
name: string; // name of SSH certificate template
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl?: string;
|
||||||
|
keyId?: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TSignSshKeyDTO = {
|
||||||
|
name: string; // name of SSH certificate template
|
||||||
|
publicKey: string;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl?: string;
|
||||||
|
keyId?: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGetSshCaCertificateTemplatesDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TCreateSshCertDTO = {
|
||||||
|
caPrivateKey: string;
|
||||||
|
userPublicKey: string;
|
||||||
|
keyId: string;
|
||||||
|
principals: string[];
|
||||||
|
ttl: number;
|
||||||
|
certType: SshCertType;
|
||||||
|
};
|
||||||
@@ -384,6 +384,9 @@ export const ORGANIZATIONS = {
|
|||||||
},
|
},
|
||||||
LIST_GROUPS: {
|
LIST_GROUPS: {
|
||||||
organizationId: "The ID of the organization to list groups for."
|
organizationId: "The ID of the organization to list groups for."
|
||||||
|
},
|
||||||
|
LIST_SSH_CAS: {
|
||||||
|
organizationId: "The ID of the organization to list SSH CAs for."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -443,6 +446,9 @@ export const PROJECTS = {
|
|||||||
LIST_INTEGRATION_AUTHORIZATION: {
|
LIST_INTEGRATION_AUTHORIZATION: {
|
||||||
workspaceId: "The ID of the project to list integration auths for."
|
workspaceId: "The ID of the project to list integration auths for."
|
||||||
},
|
},
|
||||||
|
LIST_SSH_CAS: {
|
||||||
|
slug: "The slug of the project to list SSH CAs for."
|
||||||
|
},
|
||||||
LIST_CAS: {
|
LIST_CAS: {
|
||||||
slug: "The slug of the project to list CAs for.",
|
slug: "The slug of the project to list CAs for.",
|
||||||
status: "The status of the CA to filter by.",
|
status: "The status of the CA to filter by.",
|
||||||
@@ -1132,6 +1138,57 @@ export const AUDIT_LOG_STREAMS = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SSH_CERTIFICATE_AUTHORITIES = {
|
||||||
|
CREATE: {
|
||||||
|
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."
|
||||||
|
},
|
||||||
|
GET: {
|
||||||
|
sshCaId: "The ID of the SSH CA to get."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
sshCaId: "The ID of the SSH CA to update.",
|
||||||
|
status: "The status of the SSH CA to update to. This can be one of active or disabled."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
sshCaId: "The ID of the SSH CA to delete."
|
||||||
|
},
|
||||||
|
GET_CERTIFICATE_TEMPLATES: {
|
||||||
|
sshCaId: "The ID of the SSH CA to get the certificate templates for."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SSH_CERTIFICATE_TEMPLATES = {
|
||||||
|
GET: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to get."
|
||||||
|
},
|
||||||
|
CREATE: {
|
||||||
|
sshCaId: "The ID of the SSH CA to associate the certificate template with.",
|
||||||
|
name: "The name of the certificate template.",
|
||||||
|
ttl: "The default time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
maxTTL: "The maximum time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
allowedUsers: "The list of allowed users for certificates issued under this template.",
|
||||||
|
allowedHosts: "The list of allowed hosts for certificates issued under this template.",
|
||||||
|
allowUserCertificates: "Whether or not to allow user certificates to be issued under this template.",
|
||||||
|
allowHostCertificates: "Whether or not to allow host certificates to be issued under this template.",
|
||||||
|
allowCustomKeyIds: "Whether or not to allow custom key IDs for certificates issued under this template."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to update.",
|
||||||
|
name: "The name of the certificate template.",
|
||||||
|
ttl: "The default time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
maxTTL: "The maximum time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
allowedUsers: "The list of allowed users for certificates issued under this template.",
|
||||||
|
allowedHosts: "The list of allowed hosts for certificates issued under this template.",
|
||||||
|
allowUserCertificates: "Whether or not to allow user certificates to be issued under this template.",
|
||||||
|
allowHostCertificates: "Whether or not to allow host certificates to be issued under this template.",
|
||||||
|
allowCustomKeyIds: "Whether or not to allow custom key IDs for certificates issued under this template."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to delete."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const CERTIFICATE_AUTHORITIES = {
|
export const CERTIFICATE_AUTHORITIES = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectSlug: "Slug of the project to create the CA in.",
|
projectSlug: "Slug of the project to create the CA in.",
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-da
|
|||||||
import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
|
import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
|
||||||
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
|
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
|
||||||
import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
||||||
|
import { sshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { sshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
|
import { sshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||||
|
import { sshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
|
import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||||
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
||||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
@@ -342,6 +347,10 @@ export const registerRoutes = async (
|
|||||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||||
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
||||||
|
|
||||||
|
const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db);
|
||||||
|
const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db);
|
||||||
|
const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db);
|
||||||
|
|
||||||
const kmsDAL = kmskeyDALFactory(db);
|
const kmsDAL = kmskeyDALFactory(db);
|
||||||
const internalKmsDAL = internalKmsDALFactory(db);
|
const internalKmsDAL = internalKmsDALFactory(db);
|
||||||
const externalKmsDAL = externalKmsDALFactory(db);
|
const externalKmsDAL = externalKmsDALFactory(db);
|
||||||
@@ -554,7 +563,8 @@ export const registerRoutes = async (
|
|||||||
groupDAL,
|
groupDAL,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
oidcConfigDAL,
|
oidcConfigDAL,
|
||||||
projectBotService
|
projectBotService,
|
||||||
|
sshCertificateAuthorityDAL
|
||||||
});
|
});
|
||||||
const signupService = authSignupServiceFactory({
|
const signupService = authSignupServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
@@ -702,6 +712,20 @@ export const registerRoutes = async (
|
|||||||
queueService
|
queueService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
|
const sshCertificateTemplateService = sshCertificateTemplateServiceFactory({
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
@@ -784,6 +808,7 @@ export const registerRoutes = async (
|
|||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
@@ -1354,6 +1379,8 @@ export const registerRoutes = async (
|
|||||||
auditLog: auditLogService,
|
auditLog: auditLogService,
|
||||||
auditLogStream: auditLogStreamService,
|
auditLogStream: auditLogStreamService,
|
||||||
certificate: certificateService,
|
certificate: certificateService,
|
||||||
|
sshCertificateAuthority: sshCertificateAuthorityService,
|
||||||
|
sshCertificateTemplate: sshCertificateTemplateService,
|
||||||
certificateAuthority: certificateAuthorityService,
|
certificateAuthority: certificateAuthorityService,
|
||||||
certificateTemplate: certificateTemplateService,
|
certificateTemplate: certificateTemplateService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||||
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
import { getLastMidnightDateISO } from "@app/lib/fn";
|
import { getLastMidnightDateISO } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -404,4 +405,34 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { groups };
|
return { groups };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:organizationId/ssh-cas",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim().describe(ORGANIZATIONS.LIST_SSH_CAS.organizationId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
cas: z.array(sanitizedSshCa)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const cas = await server.services.org.listOrgSshCas({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { cas };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
|
|
||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
export const createSerialNumber = () => {
|
export const createSerialNumber = () => {
|
||||||
const randomBytes = crypto.randomBytes(20);
|
const randomBytes = crypto.randomBytes(20); // 20 bytes = 160 bits
|
||||||
randomBytes[0] &= 0x7f; // ensure the first bit is 0
|
randomBytes[0] &= 0x7f; // ensure the first bit is 0
|
||||||
return randomBytes.toString("hex");
|
return randomBytes.toString("hex");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||||
import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
@@ -62,6 +63,7 @@ import {
|
|||||||
TGetOrgGroupsDTO,
|
TGetOrgGroupsDTO,
|
||||||
TGetOrgMembershipDTO,
|
TGetOrgMembershipDTO,
|
||||||
TInviteUserToOrgDTO,
|
TInviteUserToOrgDTO,
|
||||||
|
TListOrgSshCasDTO,
|
||||||
TListProjectMembershipsByOrgMembershipIdDTO,
|
TListProjectMembershipsByOrgMembershipIdDTO,
|
||||||
TUpdateOrgDTO,
|
TUpdateOrgDTO,
|
||||||
TUpdateOrgMembershipDTO,
|
TUpdateOrgMembershipDTO,
|
||||||
@@ -98,6 +100,7 @@ type TOrgServiceFactoryDep = {
|
|||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
||||||
@@ -125,6 +128,7 @@ export const orgServiceFactory = ({
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityMetadataDAL,
|
identityMetadataDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
projectBotService
|
projectBotService
|
||||||
}: TOrgServiceFactoryDep) => {
|
}: TOrgServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
@@ -1127,6 +1131,27 @@ export const orgServiceFactory = ({
|
|||||||
return incidentContact;
|
return incidentContact;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of SSH CAs for project
|
||||||
|
*/
|
||||||
|
const listOrgSshCas = async ({ actorId, actorOrgId, actorAuthMethod, actor, orgId }: TListOrgSshCasDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Read,
|
||||||
|
OrgPermissionSubjects.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const cas = await sshCertificateAuthorityDAL.find(
|
||||||
|
{
|
||||||
|
orgId
|
||||||
|
},
|
||||||
|
{ sort: [["updatedAt", "desc"]] }
|
||||||
|
);
|
||||||
|
|
||||||
|
return cas;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
findOrganizationById,
|
findOrganizationById,
|
||||||
findAllOrgMembers,
|
findAllOrgMembers,
|
||||||
@@ -1148,6 +1173,7 @@ export const orgServiceFactory = ({
|
|||||||
deleteIncidentContact,
|
deleteIncidentContact,
|
||||||
getOrgGroups,
|
getOrgGroups,
|
||||||
listProjectMembershipsByOrgMembershipId,
|
listProjectMembershipsByOrgMembershipId,
|
||||||
findOrgBySlug
|
findOrgBySlug,
|
||||||
|
listOrgSshCas
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export type TListProjectMembershipsByOrgMembershipIdDTO = {
|
|||||||
orgMembershipId: string;
|
orgMembershipId: string;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TListOrgSshCasDTO = TOrgPermission;
|
||||||
|
|
||||||
export enum OrgAuthMethod {
|
export enum OrgAuthMethod {
|
||||||
OIDC = "oidc",
|
OIDC = "oidc",
|
||||||
SAML = "saml"
|
SAML = "saml"
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export enum OrgPermissionSubjects {
|
|||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
AdminConsole = "organization-admin-console",
|
AdminConsole = "organization-admin-console",
|
||||||
AuditLogs = "audit-logs",
|
AuditLogs = "audit-logs",
|
||||||
ProjectTemplates = "project-templates"
|
ProjectTemplates = "project-templates",
|
||||||
|
SshCertificateAuthorities = "ssh-certificate-authorities",
|
||||||
|
SshCertificateTemplates = "ssh-certificate-templates"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OrgPermissionAdminConsoleAction {
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
@@ -47,6 +49,8 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates];
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateTemplates];
|
||||||
|
|
||||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { SshCaStatus } from "@app/hooks/api/ssh-ca";
|
||||||
|
|
||||||
import { CaStatus, CaType } from "./enums";
|
import { CaStatus, CaType } from "./enums";
|
||||||
|
|
||||||
export const caTypeToNameMap: { [K in CaType]: string } = {
|
export const caTypeToNameMap: { [K in CaType]: string } = {
|
||||||
@@ -11,7 +13,7 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
|||||||
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
|
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCaStatusBadgeVariant = (status: CaStatus) => {
|
export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case CaStatus.ACTIVE:
|
case CaStatus.ACTIVE:
|
||||||
return "success";
|
return "success";
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export * from "./secretSharing";
|
|||||||
export * from "./secretSnapshots";
|
export * from "./secretSnapshots";
|
||||||
export * from "./serverDetails";
|
export * from "./serverDetails";
|
||||||
export * from "./serviceTokens";
|
export * from "./serviceTokens";
|
||||||
|
export * from "./ssh-ca";
|
||||||
|
export * from "./sshCertificateTemplates";
|
||||||
export * from "./ssoConfig";
|
export * from "./ssoConfig";
|
||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
export * from "./tags";
|
export * from "./tags";
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export {
|
|||||||
useGetOrgPmtMethods,
|
useGetOrgPmtMethods,
|
||||||
useGetOrgTaxIds,
|
useGetOrgTaxIds,
|
||||||
useGetOrgTrialUrl,
|
useGetOrgTrialUrl,
|
||||||
|
useListOrgSshCas,
|
||||||
useUpdateOrg,
|
useUpdateOrg,
|
||||||
useUpdateOrgBillingDetails
|
useUpdateOrgBillingDetails} from "./queries";
|
||||||
} from "./queries";
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
import { TGroupOrgMembership } from "../groups/types";
|
import { TGroupOrgMembership } from "../groups/types";
|
||||||
|
import { TSshCertificateAuthority } from "../ssh-ca/types";
|
||||||
import { IntegrationAuth } from "../types";
|
import { IntegrationAuth } from "../types";
|
||||||
import {
|
import {
|
||||||
BillingDetails,
|
BillingDetails,
|
||||||
@@ -41,7 +42,8 @@ export const organizationKeys = {
|
|||||||
}: TListOrgIdentitiesDTO) =>
|
}: TListOrgIdentitiesDTO) =>
|
||||||
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
|
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
|
||||||
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
|
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
|
||||||
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const
|
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const,
|
||||||
|
getOrgSshCas: ({ orgId }: { orgId: string }) => [{ orgId }, "org-ssh-cas"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchOrganizations = async () => {
|
export const fetchOrganizations = async () => {
|
||||||
@@ -495,3 +497,18 @@ export const useGetOrgIntegrationAuths = <TData = IntegrationAuth[],>(
|
|||||||
select
|
select
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useListOrgSshCas = ({ orgId }: { orgId: string }) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: organizationKeys.getOrgSshCas({ orgId }),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { cas }
|
||||||
|
} = await apiRequest.get<{ cas: TSshCertificateAuthority[] }>(
|
||||||
|
`/api/v1/organization/${orgId}/ssh-cas`
|
||||||
|
);
|
||||||
|
return cas;
|
||||||
|
},
|
||||||
|
enabled: Boolean(orgId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum SshCaStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { SshCaStatus } from "./enums";
|
||||||
|
export { useCreateSshCa, useDeleteSshCa,useUpdateSshCa } from "./mutations";
|
||||||
|
export { useGetSshCaById, useGetSshCaCertTemplates } from "./queries";
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { organizationKeys } from "../organization/queries";
|
||||||
|
import {
|
||||||
|
TCreateSshCaDTO,
|
||||||
|
TDeleteSshCaDTO,
|
||||||
|
TSshCertificateAuthority,
|
||||||
|
TUpdateSshCaDTO} from "./types";
|
||||||
|
|
||||||
|
export const sshCaKeys = {
|
||||||
|
getSshCaById: (caId: string) => [{ caId }, "ssh-ca"]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateSshCa = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TSshCertificateAuthority, {}, TCreateSshCaDTO>({
|
||||||
|
mutationFn: async (body) => {
|
||||||
|
const {
|
||||||
|
data: { ca }
|
||||||
|
} = await apiRequest.post<{ ca: TSshCertificateAuthority }>("/api/v1/ssh/ca/", body);
|
||||||
|
return ca;
|
||||||
|
},
|
||||||
|
onSuccess: ({ orgId }) => {
|
||||||
|
queryClient.invalidateQueries(organizationKeys.getOrgSshCas({ orgId }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateSshCa = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TSshCertificateAuthority, {}, TUpdateSshCaDTO>({
|
||||||
|
mutationFn: async ({ caId, ...body }) => {
|
||||||
|
const {
|
||||||
|
data: { ca }
|
||||||
|
} = await apiRequest.patch<{ ca: TSshCertificateAuthority }>(`/api/v1/ssh/ca/${caId}`, body);
|
||||||
|
return ca;
|
||||||
|
},
|
||||||
|
onSuccess: ({ orgId }, { caId }) => {
|
||||||
|
queryClient.invalidateQueries(organizationKeys.getOrgSshCas({ orgId }));
|
||||||
|
queryClient.invalidateQueries(sshCaKeys.getSshCaById(caId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteSshCa = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TSshCertificateAuthority, {}, TDeleteSshCaDTO>({
|
||||||
|
mutationFn: async ({ caId }) => {
|
||||||
|
const {
|
||||||
|
data: { ca }
|
||||||
|
} = await apiRequest.delete<{ ca: TSshCertificateAuthority }>(`/api/v1/ssh/ca/${caId}`);
|
||||||
|
return ca;
|
||||||
|
},
|
||||||
|
onSuccess: ({ orgId }) => {
|
||||||
|
queryClient.invalidateQueries(organizationKeys.getOrgSshCas({ orgId }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
|
||||||
|
import { TSshCertificateAuthority } from "./types";
|
||||||
|
|
||||||
|
export const sshCaKeys = {
|
||||||
|
getSshCaById: (caId: string) => [{ caId }, "ssh-ca"],
|
||||||
|
getSshCaCertTemplates: (caId: string) => [{ caId }, "ssh-ca-cert-templates"]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetSshCaById = (caId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: sshCaKeys.getSshCaById(caId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { ca }
|
||||||
|
} = await apiRequest.get<{ ca: TSshCertificateAuthority }>(`/api/v1/ssh/ca/${caId}`);
|
||||||
|
return ca;
|
||||||
|
},
|
||||||
|
enabled: Boolean(caId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetSshCaCertTemplates = (caId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: sshCaKeys.getSshCaCertTemplates(caId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
certificateTemplates: TSshCertificateTemplate[];
|
||||||
|
}>(`/api/v1/ssh/ca/${caId}/certificate-templates`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: Boolean(caId)
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { CertKeyAlgorithm } from "../certificates/enums";
|
||||||
|
import { SshCaStatus } from "./enums";
|
||||||
|
|
||||||
|
export type TSshCertificateAuthority = {
|
||||||
|
id: string;
|
||||||
|
orgId: string;
|
||||||
|
status: SshCaStatus;
|
||||||
|
friendlyName: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateSshCaDTO = {
|
||||||
|
friendlyName?: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
status?: SshCaStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export {
|
||||||
|
useCreateSshCertTemplate,
|
||||||
|
useDeleteSshCertTemplate,
|
||||||
|
useUpdateSshCertTemplate} from "./mutations";
|
||||||
|
export { useGetSshCertTemplate } from "./queries";
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { sshCaKeys } from "../ssh-ca/queries";
|
||||||
|
import {
|
||||||
|
TCreateSshCertificateTemplateDTO,
|
||||||
|
TDeleteSshCertificateTemplateDTO,
|
||||||
|
TSshCertificateTemplate,
|
||||||
|
TUpdateSshCertificateTemplateDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const useCreateSshCertTemplate = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TSshCertificateTemplate, {}, TCreateSshCertificateTemplateDTO>({
|
||||||
|
mutationFn: async (data) => {
|
||||||
|
const { data: certificateTemplate } = await apiRequest.post<TSshCertificateTemplate>(
|
||||||
|
"/api/v1/ssh/certificate-templates",
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return certificateTemplate;
|
||||||
|
},
|
||||||
|
onSuccess: ({ sshCaId }) => {
|
||||||
|
queryClient.invalidateQueries(sshCaKeys.getSshCaCertTemplates(sshCaId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateSshCertTemplate = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TSshCertificateTemplate, {}, TUpdateSshCertificateTemplateDTO>({
|
||||||
|
mutationFn: async (data) => {
|
||||||
|
const { data: certificateTemplate } = await apiRequest.patch<TSshCertificateTemplate>(
|
||||||
|
`/api/v1/ssh/certificate-templates/${data.id}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
},
|
||||||
|
onSuccess: ({ sshCaId }) => {
|
||||||
|
queryClient.invalidateQueries(sshCaKeys.getSshCaCertTemplates(sshCaId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteSshCertTemplate = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TSshCertificateTemplate, {}, TDeleteSshCertificateTemplateDTO>({
|
||||||
|
mutationFn: async (data) => {
|
||||||
|
const { data: certificateTemplate } = await apiRequest.delete<TSshCertificateTemplate>(
|
||||||
|
`/api/v1/ssh/certificate-templates/${data.id}`
|
||||||
|
);
|
||||||
|
return certificateTemplate;
|
||||||
|
},
|
||||||
|
onSuccess: ({ sshCaId }) => {
|
||||||
|
queryClient.invalidateQueries(sshCaKeys.getSshCaCertTemplates(sshCaId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TSshCertificateTemplate } from "./types";
|
||||||
|
|
||||||
|
export const certTemplateKeys = {
|
||||||
|
getSshCertTemplateById: (id: string) => [{ id }, "ssh-cert-template"]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetSshCertTemplate = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: certTemplateKeys.getSshCertTemplateById(id),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data: certificateTemplate } = await apiRequest.get<TSshCertificateTemplate>(
|
||||||
|
`/api/v1/ssh/certificate-templates/${id}`
|
||||||
|
);
|
||||||
|
return certificateTemplate;
|
||||||
|
},
|
||||||
|
enabled: Boolean(id)
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
export type TSshCertificateTemplate = {
|
||||||
|
id: string;
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateSshCertificateTemplateDTO = {
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateSshCertificateTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
ttl?: string;
|
||||||
|
maxTTL?: string;
|
||||||
|
allowedUsers?: string[];
|
||||||
|
allowedHosts?: string[];
|
||||||
|
allowUserCertificates?: boolean;
|
||||||
|
allowHostCertificates?: boolean;
|
||||||
|
allowCustomKeyIds?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSshCertificateTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
@@ -513,6 +513,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href={`/org/${currentOrg?.id}/ssh`} passHref>
|
||||||
|
<a>
|
||||||
|
<MenuItem
|
||||||
|
isSelected={router.asPath === `/org/${currentOrg?.id}/ssh`}
|
||||||
|
icon="system-outline-90-lock-closed"
|
||||||
|
>
|
||||||
|
SSH
|
||||||
|
</MenuItem>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
<Link href={`/org/${currentOrg?.id}/secret-scanning`} passHref>
|
<Link href={`/org/${currentOrg?.id}/secret-scanning`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
|
import { SshCaPage } from "@app/views/Org/SshCaPage";
|
||||||
|
|
||||||
|
export default function SshCa() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>SSH Certificate Authority</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
</Head>
|
||||||
|
<SshCaPage />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SshCa.requireAuth = true;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
|
import { SshPage } from "@app/views/Org/SshPage";
|
||||||
|
|
||||||
|
// TODO: update meta tags
|
||||||
|
|
||||||
|
const Ssh = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{t("common.head-title", { title: t("approval.title") })}</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
<meta property="og:title" content={String(t("approval.og-title"))} />
|
||||||
|
<meta name="og:description" content={String(t("approval.og-description"))} />
|
||||||
|
</Head>
|
||||||
|
<div className="h-full">
|
||||||
|
<SshPage />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Ssh;
|
||||||
|
|
||||||
|
Ssh.requireAuth = true;
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan, ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useOrganization
|
||||||
|
} from "@app/context";
|
||||||
|
import { withPermission } from "@app/hoc";
|
||||||
|
import { useDeleteSshCa, useGetSshCaById } from "@app/hooks/api";
|
||||||
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components";
|
||||||
|
|
||||||
|
export const SshCaPage = withPermission(
|
||||||
|
() => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const router = useRouter();
|
||||||
|
const caId = router.query.caId as string;
|
||||||
|
const { data } = useGetSshCaById(caId);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteSshCa } = useDeleteSshCa();
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"sshCa",
|
||||||
|
"deleteSshCa"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const onRemoveCaSubmit = async (caIdToDelete: string) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg?.id) return;
|
||||||
|
|
||||||
|
await deleteSshCa({ caId: caIdToDelete });
|
||||||
|
|
||||||
|
await createNotification({
|
||||||
|
text: "Successfully deleted SSH CA",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("deleteSshCa");
|
||||||
|
router.push(`/org/${currentOrg.id}/ssh`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete SSH CA",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
|
{data && (
|
||||||
|
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||||
|
onClick={() => router.push(`/org/${currentOrg?.id}/ssh`)}
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
SSH Certificate Authorities
|
||||||
|
</Button>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<p className="text-3xl font-semibold text-white">{data.friendlyName}</p>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.SshCertificateAuthorities}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
isAllowed
|
||||||
|
? "hover:!bg-red-500 hover:!text-white"
|
||||||
|
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("deleteSshCa", {
|
||||||
|
caId: data.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Delete SSH CA
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="mr-4 w-96">
|
||||||
|
<SshCaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<SshCertificateTemplatesSection caId={caId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteSshCa.isOpen}
|
||||||
|
title="Are you sure want to remove the SSH CA from the project?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteSshCa", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onRemoveCaSubmit((popUp?.deleteSshCa?.data as { caId: string })?.caId)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.SshCertificateAuthorities }
|
||||||
|
);
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { IconButton, Tooltip } from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
|
import { useTimedReset } from "@app/hooks";
|
||||||
|
import { useGetSshCaById } from "@app/hooks/api";
|
||||||
|
import { caStatusToNameMap } from "@app/hooks/api/ca/constants";
|
||||||
|
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
caId: string;
|
||||||
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["sshCa"]>, data?: {}) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SshCaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||||
|
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||||
|
initialState: "Copy ID to clipboard"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: ca } = useGetSshCaById(caId);
|
||||||
|
|
||||||
|
return ca ? (
|
||||||
|
<div className="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">
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.SshCertificateAuthorities}
|
||||||
|
>
|
||||||
|
{(isAllowed) => {
|
||||||
|
return (
|
||||||
|
<Tooltip content="Edit SSH CA">
|
||||||
|
<IconButton
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("sshCa", {
|
||||||
|
caId: ca.id
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm font-semibold text-mineshaft-300">SSH CA ID</p>
|
||||||
|
<div className="group flex align-top">
|
||||||
|
<p className="text-sm text-mineshaft-300">{ca.id}</p>
|
||||||
|
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||||
|
<Tooltip content={copyTextId}>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(ca.id);
|
||||||
|
setCopyTextId("Copied");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm font-semibold text-mineshaft-300">Friendly Name</p>
|
||||||
|
<p className="text-sm text-mineshaft-300">{ca.friendlyName}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
|
||||||
|
<p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-mineshaft-300">Key Algorithm</p>
|
||||||
|
<p className="text-sm text-mineshaft-300">{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch} from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import {
|
||||||
|
useCreateSshCertTemplate,
|
||||||
|
useGetSshCaById,
|
||||||
|
useGetSshCertTemplate,
|
||||||
|
useListOrgSshCas,
|
||||||
|
useUpdateSshCertTemplate} from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
sshCaId: z.string(),
|
||||||
|
name: z.string().min(1),
|
||||||
|
ttl: z.string().trim().min(1),
|
||||||
|
maxTTL: z.string().trim().min(1),
|
||||||
|
allowedUsers: z.string(),
|
||||||
|
allowedHosts: z.string(),
|
||||||
|
allowUserCertificates: z.boolean().optional().default(false),
|
||||||
|
allowHostCertificates: z.boolean().optional().default(false),
|
||||||
|
allowCustomKeyIds: z.boolean().optional().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sshCaId: string;
|
||||||
|
popUp: UsePopUpState<["sshCertificateTemplate"]>;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<["sshCertificateTemplate"]>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SshCertificateTemplateModal = ({ popUp, handlePopUpToggle, sshCaId }: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
|
const { data: ca } = useGetSshCaById(sshCaId);
|
||||||
|
|
||||||
|
const { data: certTemplate } = useGetSshCertTemplate(
|
||||||
|
(popUp?.sshCertificateTemplate?.data as { id: string })?.id || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: cas } = useListOrgSshCas({
|
||||||
|
orgId: currentOrg?.id ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: createSshCertTemplate } = useCreateSshCertTemplate();
|
||||||
|
const { mutateAsync: updateSshCertTemplate } = useUpdateSshCertTemplate();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (certTemplate) {
|
||||||
|
reset({
|
||||||
|
sshCaId: certTemplate.sshCaId,
|
||||||
|
name: certTemplate.name,
|
||||||
|
ttl: certTemplate.ttl,
|
||||||
|
maxTTL: certTemplate.maxTTL,
|
||||||
|
allowedUsers: certTemplate.allowedUsers.join(", "),
|
||||||
|
allowedHosts: certTemplate.allowedHosts.join(", "),
|
||||||
|
allowUserCertificates: certTemplate.allowUserCertificates,
|
||||||
|
allowHostCertificates: certTemplate.allowHostCertificates,
|
||||||
|
allowCustomKeyIds: certTemplate.allowCustomKeyIds
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reset({
|
||||||
|
sshCaId,
|
||||||
|
name: "",
|
||||||
|
ttl: "1h",
|
||||||
|
maxTTL: "30d",
|
||||||
|
allowedUsers: "",
|
||||||
|
allowedHosts: "",
|
||||||
|
allowUserCertificates: false,
|
||||||
|
allowHostCertificates: false,
|
||||||
|
allowCustomKeyIds: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [certTemplate, ca]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds
|
||||||
|
}: FormData) => {
|
||||||
|
try {
|
||||||
|
if (certTemplate) {
|
||||||
|
await updateSshCertTemplate({
|
||||||
|
id: certTemplate.id,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowedUsers: allowedUsers ? allowedUsers.split(",").map((user) => user.trim()) : [],
|
||||||
|
allowedHosts: allowedHosts ? allowedHosts.split(",").map((host) => host.trim()) : [],
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowCustomKeyIds
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated SSH certificate template",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await createSshCertTemplate({
|
||||||
|
sshCaId,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowedUsers: allowedUsers ? allowedUsers.split(",").map((user) => user.trim()) : [],
|
||||||
|
allowedHosts: allowedHosts ? allowedHosts.split(",").map((host) => host.trim()) : [],
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowCustomKeyIds
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created SSH certificate template",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset();
|
||||||
|
handlePopUpToggle("sshCertificateTemplate", false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to save changes",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.sshCertificateTemplate?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("sshCertificateTemplate", isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={certTemplate ? "SSH Certificate Template" : "Create SSH Certificate Template"}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
{certTemplate && (
|
||||||
|
<FormControl label="SSH Certificate Template ID">
|
||||||
|
<Input value={certTemplate.id} isDisabled className="bg-white/[0.07]" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="SSH Template Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="My SSH Certificate Template" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="sshCaId"
|
||||||
|
defaultValue={sshCaId}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Issuing SSH CA"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="mt-4"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
isDisabled
|
||||||
|
>
|
||||||
|
{(cas || []).map(({ id, friendlyName }) => (
|
||||||
|
<SelectItem value={id} key={`ssh-ca-${id}`}>
|
||||||
|
{friendlyName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowedUsers"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Users"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="ec2-user, developer, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowedHosts"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Hosts"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="*.compute.amazonaws.com, api.example.com, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="ttl"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="TTL"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="maxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Max TTL"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowUserCertificates"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Switch
|
||||||
|
id="allow-user-certificates"
|
||||||
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
|
isChecked={field.value}
|
||||||
|
>
|
||||||
|
<p className="ml-1 w-full">Allow User Certificates</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowHostCertificates"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Switch
|
||||||
|
id="allow-host-certificates"
|
||||||
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
|
isChecked={field.value}
|
||||||
|
>
|
||||||
|
<p className="ml-1 w-full">Allow Host Certificates</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowCustomKeyIds"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Switch
|
||||||
|
id="allow-custom-key-ids"
|
||||||
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
|
isChecked={field.value}
|
||||||
|
>
|
||||||
|
<p className="ml-1 w-full">Allow Custom Key IDs</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("sshCertificateTemplate", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { DeleteActionModal, IconButton } from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteSshCertTemplate } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { SshCertificateTemplateModal } from "./SshCertificateTemplateModal";
|
||||||
|
import { SshCertificateTemplatesTable } from "./SshCertificateTemplatesTable";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
caId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SshCertificateTemplatesSection = ({ caId }: Props) => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"sshCertificateTemplate",
|
||||||
|
"deleteSshCertificateTemplate",
|
||||||
|
"upgradePlan"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteSshCertTemplate } = useDeleteSshCertTemplate();
|
||||||
|
|
||||||
|
const onRemoveSshCertificateTemplateSubmit = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await deleteSshCertTemplate({
|
||||||
|
id
|
||||||
|
});
|
||||||
|
|
||||||
|
await createNotification({
|
||||||
|
text: "Successfully deleted SSH certificate template",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("deleteSshCertificateTemplate");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete SSH certificate template",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Create}
|
||||||
|
a={OrgPermissionSubjects.SshCertificateTemplates}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => handlePopUpOpen("sshCertificateTemplate")}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<div className="py-4">
|
||||||
|
<SshCertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} sshCaId={caId} />
|
||||||
|
</div>
|
||||||
|
<SshCertificateTemplateModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
sshCaId={caId}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteSshCertificateTemplate.isOpen}
|
||||||
|
title={`Are you sure want to delete the SSH certificate template ${
|
||||||
|
(popUp?.deleteSshCertificateTemplate?.data as { name: string })?.name || ""
|
||||||
|
}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteSshCertificateTemplate", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onRemoveSshCertificateTemplateSubmit(
|
||||||
|
(popUp?.deleteSshCertificateTemplate?.data as { id: string })?.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { faEllipsis, faFileAlt, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
|
import { useGetSshCaCertTemplates } from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sshCaId: string;
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<
|
||||||
|
["sshCertificateTemplate", "deleteSshCertificateTemplate", "upgradePlan"]
|
||||||
|
>,
|
||||||
|
data?: {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props) => {
|
||||||
|
const { data, isLoading } = useGetSshCaCertTemplates(sshCaId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={2} innerKey="project-cas" />}
|
||||||
|
{!isLoading &&
|
||||||
|
data?.certificateTemplates.map((certificateTemplate) => {
|
||||||
|
return (
|
||||||
|
<Tr className="h-10" key={`certificate-${certificateTemplate.id}`}>
|
||||||
|
<Td>{certificateTemplate.name}</Td>
|
||||||
|
<Td className="flex justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("sshCertificateTemplate", {
|
||||||
|
id: certificateTemplate.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
icon={<FontAwesomeIcon icon={faFileAlt} size="sm" className="mr-1" />}
|
||||||
|
>
|
||||||
|
Edit Template
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.SshCertificateTemplates}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} size="sm" className="mr-1" />}
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("deleteSshCertificateTemplate", {
|
||||||
|
id: certificateTemplate.id,
|
||||||
|
name: certificateTemplate.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete Template
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isLoading && !data?.certificateTemplates?.length && (
|
||||||
|
<EmptyState
|
||||||
|
title="No certificate templates have been created for this SSH CA"
|
||||||
|
icon={faFileAlt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { SshCaDetailsSection } from "./SshCaDetailsSection";
|
||||||
|
export { SshCertificateTemplatesSection } from "./SshCertificateTemplatesSection";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SshCaPage } from "./SshCaPage";
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
|
import { withPermission } from "@app/hoc";
|
||||||
|
|
||||||
|
import { SshCaSection } from "./components";
|
||||||
|
|
||||||
|
export const SshPage = withPermission(
|
||||||
|
() => {
|
||||||
|
return (
|
||||||
|
<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 py-6 px-6">
|
||||||
|
<p className="mr-4 mb-4 text-3xl font-semibold text-white">SSH</p>
|
||||||
|
<SshCaSection />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.SshCertificateAuthorities }
|
||||||
|
);
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useCreateSshCa, useGetSshCaById, useUpdateSshCa } from "@app/hooks/api";
|
||||||
|
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
||||||
|
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["sshCa"]>;
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshCa"]>, state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = z
|
||||||
|
.object({
|
||||||
|
friendlyName: z.string(),
|
||||||
|
keyAlgorithm: z.enum([
|
||||||
|
CertKeyAlgorithm.RSA_2048,
|
||||||
|
CertKeyAlgorithm.RSA_4096,
|
||||||
|
CertKeyAlgorithm.ECDSA_P256,
|
||||||
|
CertKeyAlgorithm.ECDSA_P384
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
|
const { data: ca } = useGetSshCaById((popUp?.sshCa?.data as { caId: string })?.caId || "");
|
||||||
|
|
||||||
|
const { mutateAsync: createMutateAsync } = useCreateSshCa();
|
||||||
|
const { mutateAsync: updateMutateAsync } = useUpdateSshCa();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
friendlyName: "",
|
||||||
|
keyAlgorithm: CertKeyAlgorithm.RSA_2048
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ friendlyName, keyAlgorithm }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (ca) {
|
||||||
|
// update
|
||||||
|
await updateMutateAsync({
|
||||||
|
caId: ca.id
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// create
|
||||||
|
await createMutateAsync({
|
||||||
|
friendlyName,
|
||||||
|
keyAlgorithm
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset();
|
||||||
|
handlePopUpToggle("sshCa", false);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully ${ca ? "updated" : "created"} SSH CA`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create SSH CA",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.sshCa?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
reset();
|
||||||
|
handlePopUpToggle("sshCa", isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={`${ca ? "View" : "Create"} SSH CA`}>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
{ca && (
|
||||||
|
<FormControl label="CA ID">
|
||||||
|
<Input value={ca.id} isDisabled className="bg-white/[0.07]" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="friendlyName"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Friendly Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="My SSH CA" isDisabled={Boolean(ca)} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="keyAlgorithm"
|
||||||
|
defaultValue={CertKeyAlgorithm.RSA_2048}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Key Algorithm"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
isDisabled={Boolean(ca)}
|
||||||
|
>
|
||||||
|
{certKeyAlgorithms.map(({ label, value }) => (
|
||||||
|
<SelectItem value={String(value || "")} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{popUp?.sshCa?.data ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("sshCa", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
|
import { SshCaStatus, useDeleteSshCa, useUpdateSshCa } from "@app/hooks/api";
|
||||||
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { SshCaModal } from "./SshCaModal";
|
||||||
|
import { SshCaTable } from "./SshCaTable";
|
||||||
|
|
||||||
|
export const SshCaSection = () => {
|
||||||
|
const { mutateAsync: deleteSshCa } = useDeleteSshCa();
|
||||||
|
const { mutateAsync: updateSshCa } = useUpdateSshCa();
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"sshCa",
|
||||||
|
"deleteSshCa",
|
||||||
|
"sshCaStatus", // enable / disable
|
||||||
|
"upgradePlan"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const onRemoveSshCaSubmit = async (caId: string) => {
|
||||||
|
try {
|
||||||
|
await deleteSshCa({ caId });
|
||||||
|
|
||||||
|
await createNotification({
|
||||||
|
text: "Successfully deleted SSH CA",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("deleteSshCa");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete SSH CA",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateSshCaStatus = async ({ caId, status }: { caId: string; status: SshCaStatus }) => {
|
||||||
|
try {
|
||||||
|
await updateSshCa({ caId, status });
|
||||||
|
|
||||||
|
await createNotification({
|
||||||
|
text: `Successfully ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("sshCaStatus");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Certificate Authorities</p>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Create}
|
||||||
|
a={OrgPermissionSubjects.SshCertificateAuthorities}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("sshCa")}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Create SSH CA
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<SshCaTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
<SshCaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteSshCa.isOpen}
|
||||||
|
title="Are you sure want to remove the SSH CA?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteSshCa", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onRemoveSshCaSubmit((popUp?.deleteSshCa?.data as { caId: string })?.caId)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.sshCaStatus.isOpen}
|
||||||
|
title={`Are you sure want to ${
|
||||||
|
(popUp?.sshCaStatus?.data as { status: string })?.status === SshCaStatus.ACTIVE
|
||||||
|
? "enable"
|
||||||
|
: "disable"
|
||||||
|
} the CA?`}
|
||||||
|
subTitle={
|
||||||
|
(popUp?.sshCaStatus?.data as { status: string })?.status === SshCaStatus.ACTIVE
|
||||||
|
? "This action will allow the SSH CA to start issuing certificates again."
|
||||||
|
: "This action will prevent the SSH CA from issuing new certificates."
|
||||||
|
}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("sshCaStatus", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onUpdateSshCaStatus(popUp?.sshCaStatus?.data as { caId: string; status: SshCaStatus })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* <UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
|
import { SshCaStatus , useListOrgSshCas } from "@app/hooks/api";
|
||||||
|
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["deleteSshCa", "sshCaStatus"]>,
|
||||||
|
data?: {}
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SshCaTable = ({ handlePopUpOpen }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data, isLoading } = useListOrgSshCas({
|
||||||
|
orgId: currentOrg?.id ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Friendly Name</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={3} innerKey="org-ssh-cas" />}
|
||||||
|
{!isLoading &&
|
||||||
|
data &&
|
||||||
|
data.length > 0 &&
|
||||||
|
data.map((ca) => {
|
||||||
|
return (
|
||||||
|
<Tr
|
||||||
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
key={`ca-${ca.id}`}
|
||||||
|
onClick={() => router.push(`/org/${currentOrg?.id}/ssh/ca/${ca.id}`)}
|
||||||
|
>
|
||||||
|
<Td>{ca.friendlyName}</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant={getCaStatusBadgeVariant(ca.status)}>
|
||||||
|
{caStatusToNameMap[ca.status]}
|
||||||
|
</Badge>
|
||||||
|
</Td>
|
||||||
|
<Td className="flex justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
{(ca.status === SshCaStatus.ACTIVE ||
|
||||||
|
ca.status === SshCaStatus.DISABLED) && (
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.SshCertificateAuthorities}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed &&
|
||||||
|
"pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("sshCaStatus", {
|
||||||
|
caId: ca.id,
|
||||||
|
status:
|
||||||
|
ca.status === SshCaStatus.ACTIVE
|
||||||
|
? SshCaStatus.DISABLED
|
||||||
|
: SshCaStatus.ACTIVE
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faBan} />}
|
||||||
|
>
|
||||||
|
{`${
|
||||||
|
ca.status === SshCaStatus.ACTIVE ? "Disable" : "Enable"
|
||||||
|
} SSH CA`}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.SshCertificateAuthorities}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("deleteSshCa", {
|
||||||
|
caId: ca.id
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete SSH CA
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isLoading && data?.length === 0 && (
|
||||||
|
<EmptyState
|
||||||
|
title="No SSH certificate authorities have been created"
|
||||||
|
icon={faCertificate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SshCaSection } from "./SshCaSection";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SshPage } from "./SshPage";
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
// DatePicker
|
// DatePicker
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { CaType, useCreateCa, useGetCaById,useUpdateCa } from "@app/hooks/api/ca";
|
import { CaType, useCreateCa, useGetCaById, useUpdateCa } from "@app/hooks/api/ca";
|
||||||
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 { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -72,7 +72,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||||
|
|
||||||
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || "");
|
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || "");
|
||||||
|
|
||||||
const { mutateAsync: createMutateAsync } = useCreateCa();
|
const { mutateAsync: createMutateAsync } = useCreateCa();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateCa();
|
const { mutateAsync: updateMutateAsync } = useUpdateCa();
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentWorkspace?.slug) return;
|
if (!currentWorkspace?.slug) return;
|
||||||
|
|
||||||
if (ca) {
|
if (ca) {
|
||||||
// update
|
// update
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {
|
import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
faBan,
|
|
||||||
faCertificate,
|
|
||||||
faEllipsis,
|
|
||||||
faTrash
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|||||||
+1
-5
@@ -1,7 +1,3 @@
|
|||||||
/**
|
|
||||||
* TODO (dangtony98): Reevaluate if this component should be in main
|
|
||||||
* CertificateTab or under CA page in the future.
|
|
||||||
*/
|
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
@@ -18,7 +14,7 @@ import { CertificateTemplatesTable } from "./CertificateTemplatesTable";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
caId: string;
|
caId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const CertificateTemplatesSection = ({ caId }: Props) => {
|
export const CertificateTemplatesSection = ({ caId }: Props) => {
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
|||||||
Reference in New Issue
Block a user