diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index b6485536a..c0a2c0eaa 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -317,6 +317,9 @@ import { TSshCertificateAuthoritySecrets, TSshCertificateAuthoritySecretsInsert, TSshCertificateAuthoritySecretsUpdate, + TSshCertificates, + TSshCertificatesInsert, + TSshCertificatesUpdate, TSshCertificateTemplates, TSshCertificateTemplatesInsert, TSshCertificateTemplatesUpdate, @@ -396,6 +399,11 @@ declare module "knex/types/tables" { TSshCertificateTemplatesInsert, TSshCertificateTemplatesUpdate >; + [TableName.SshCertificate]: KnexOriginal.CompositeTableType< + TSshCertificates, + TSshCertificatesInsert, + TSshCertificatesUpdate + >; [TableName.CertificateAuthority]: KnexOriginal.CompositeTableType< TCertificateAuthorities, TCertificateAuthoritiesInsert, diff --git a/backend/src/db/migrations/20241130015511_ssh-mgmt.ts b/backend/src/db/migrations/20241130015511_ssh-mgmt.ts index 2d1681ca6..a96f49a4d 100644 --- a/backend/src/db/migrations/20241130015511_ssh-mgmt.ts +++ b/backend/src/db/migrations/20241130015511_ssh-mgmt.ts @@ -34,7 +34,7 @@ export async function up(knex: Knex): Promise { 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("name").notNullable(); t.string("ttl").notNullable(); t.string("maxTTL").notNullable(); t.specificType("allowedUsers", "text[]").notNullable(); @@ -45,9 +45,34 @@ export async function up(knex: Knex): Promise { }); await createOnUpdateTrigger(knex, TableName.SshCertificateTemplate); } + + if (!(await knex.schema.hasTable(TableName.SshCertificate))) { + await knex.schema.createTable(TableName.SshCertificate, (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.uuid("sshCertificateTemplateId"); + t.foreign("sshCertificateTemplateId") + .references("id") + .inTable(TableName.SshCertificateTemplate) + .onDelete("SET NULL"); + t.string("serialNumber").notNullable().unique(); + t.string("certType").notNullable(); // user or host + t.text("publicKey").notNullable(); // public key in OpenSSH format + t.specificType("principals", "text[]").notNullable(); + t.string("keyId").notNullable(); + t.datetime("notBefore").notNullable(); + t.datetime("notAfter").notNullable(); + }); + await createOnUpdateTrigger(knex, TableName.SshCertificateTemplate); + } } export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.SshCertificate); + await dropOnUpdateTrigger(knex, TableName.SshCertificate); + await knex.schema.dropTableIfExists(TableName.SshCertificateTemplate); await dropOnUpdateTrigger(knex, TableName.SshCertificateTemplate); diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 2572f2bd3..348c39c70 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -108,6 +108,7 @@ export * from "./slack-integrations"; export * from "./ssh-certificate-authorities"; export * from "./ssh-certificate-authority-secrets"; export * from "./ssh-certificate-templates"; +export * from "./ssh-certificates"; export * from "./super-admin"; export * from "./totp-configs"; export * from "./trusted-ips"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 156d6196e..b7de13188 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -5,6 +5,7 @@ export enum TableName { SshCertificateAuthority = "ssh_certificate_authorities", SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets", SshCertificateTemplate = "ssh_certificate_templates", + SshCertificate = "ssh_certificates", CertificateAuthority = "certificate_authorities", CertificateTemplateEstConfig = "certificate_template_est_configs", CertificateAuthorityCert = "certificate_authority_certs", diff --git a/backend/src/db/schemas/ssh-certificates.ts b/backend/src/db/schemas/ssh-certificates.ts new file mode 100644 index 000000000..238a71e00 --- /dev/null +++ b/backend/src/db/schemas/ssh-certificates.ts @@ -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 { TImmutableDBKeys } from "./models"; + +export const SshCertificatesSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + sshCaId: z.string().uuid(), + sshCertificateTemplateId: z.string().uuid().nullable().optional(), + serialNumber: z.string(), + certType: z.string(), + publicKey: z.string(), + principals: z.string().array(), + keyId: z.string(), + notBefore: z.date(), + notAfter: z.date() +}); + +export type TSshCertificates = z.infer; +export type TSshCertificatesInsert = Omit, TImmutableDBKeys>; +export type TSshCertificatesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index dffdce7bf..89c4459d8 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -7,15 +7,6 @@ export enum OrgPermissionActions { Delete = "delete" } -export enum OrgPermissionSshCertificateTemplateActions { - Read = "read", - Create = "create", - Edit = "edit", - Delete = "delete", - SignSshKey = "sign-ssh-key", - IssueSshCredentials = "issue-ssh-credentials" -} - export enum OrgPermissionAdminConsoleAction { AccessAllProjects = "access-all-projects" } @@ -37,6 +28,7 @@ export enum OrgPermissionSubjects { AdminConsole = "organization-admin-console", AuditLogs = "audit-logs", ProjectTemplates = "project-templates", + SshCertificates = "ssh-certificates", SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificateTemplates = "ssh-certificate-templates" } @@ -59,7 +51,8 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] | [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities] - | [OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects.SshCertificateTemplates]; + | [OrgPermissionActions, OrgPermissionSubjects.SshCertificates] + | [OrgPermissionActions, OrgPermissionSubjects.SshCertificateTemplates]; const buildAdminPermission = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); @@ -136,22 +129,18 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates); can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates); + can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificates); + can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates); + can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateAuthorities); - can( - [ - OrgPermissionSshCertificateTemplateActions.Read, - OrgPermissionSshCertificateTemplateActions.Create, - OrgPermissionSshCertificateTemplateActions.Edit, - OrgPermissionSshCertificateTemplateActions.Delete, - OrgPermissionSshCertificateTemplateActions.SignSshKey, - OrgPermissionSshCertificateTemplateActions.IssueSshCredentials - ], - OrgPermissionSubjects.SshCertificateTemplates - ); + 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); @@ -184,9 +173,9 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities); - can(OrgPermissionSshCertificateTemplateActions.Read, OrgPermissionSubjects.SshCertificateTemplates); - can(OrgPermissionSshCertificateTemplateActions.SignSshKey, OrgPermissionSubjects.SshCertificateTemplates); - can(OrgPermissionSshCertificateTemplateActions.IssueSshCredentials, OrgPermissionSubjects.SshCertificateTemplates); + can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificates); + can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates); + can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates); return rules; }; diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts index 88e3259b2..8553eae5c 100644 --- a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts @@ -1,10 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import ms from "ms"; -import { - OrgPermissionSshCertificateTemplateActions, - OrgPermissionSubjects -} from "@app/ee/services/permission/org-permission"; +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"; @@ -61,7 +58,7 @@ export const sshCertificateTemplateServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionSshCertificateTemplateActions.Create, + OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificateTemplates ); @@ -124,7 +121,7 @@ export const sshCertificateTemplateServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionSshCertificateTemplateActions.Edit, + OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateTemplates ); @@ -183,7 +180,7 @@ export const sshCertificateTemplateServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionSshCertificateTemplateActions.Delete, + OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateTemplates ); @@ -209,7 +206,7 @@ export const sshCertificateTemplateServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionSshCertificateTemplateActions.Read, + OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates ); diff --git a/backend/src/ee/services/ssh-certificate/ssh-certificate-dal.ts b/backend/src/ee/services/ssh-certificate/ssh-certificate-dal.ts new file mode 100644 index 000000000..95cc4766e --- /dev/null +++ b/backend/src/ee/services/ssh-certificate/ssh-certificate-dal.ts @@ -0,0 +1,38 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +export type TSshCertificateDALFactory = ReturnType; + +export const sshCertificateDALFactory = (db: TDbClient) => { + const sshCertificateOrm = ormify(db, TableName.SshCertificate); + + const countSshCertificatesInOrg = async (orgId: string) => { + try { + interface CountResult { + count: string; + } + + const query = db + .replicaNode()(TableName.SshCertificate) + .join( + TableName.SshCertificateAuthority, + `${TableName.SshCertificate}.sshCaId`, + `${TableName.SshCertificateAuthority}.id` + ) + .join(TableName.Organization, `${TableName.SshCertificateAuthority}.orgId`, `${TableName.Organization}.id`) + .where(`${TableName.Organization}.id`, orgId); + + const count = await query.count("*").first(); + + return parseInt((count as unknown as CountResult).count || "0", 10); + } catch (error) { + throw new DatabaseError({ error, name: "Count all SSH certificates in organization" }); + } + }; + return { + ...sshCertificateOrm, + countSshCertificatesInOrg + }; +}; diff --git a/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts b/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts new file mode 100644 index 000000000..27f6ef7ed --- /dev/null +++ b/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts @@ -0,0 +1,12 @@ +import { SshCertificatesSchema } from "@app/db/schemas"; + +export const sanitizedSshCertificate = SshCertificatesSchema.pick({ + id: true, + sshCaId: true, + sshCertificateTemplateId: true, + serialNumber: true, + certType: true, + publicKey: true, + principals: true, + keyId: true +}); diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts index 27fd3b554..690a71a7b 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts @@ -1,15 +1,12 @@ import { ForbiddenError } from "@casl/ability"; -import { - OrgPermissionActions, - OrgPermissionSshCertificateTemplateActions, - OrgPermissionSubjects -} from "@app/ee/services/permission/org-permission"; +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 { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal"; -import { NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { @@ -38,6 +35,7 @@ type TSshCertificateAuthorityServiceFactoryDep = { >; sshCertificateAuthoritySecretDAL: Pick; sshCertificateTemplateDAL: Pick; + sshCertificateDAL: Pick; kmsService: Pick; permissionService: Pick; }; @@ -48,6 +46,7 @@ export const sshCertificateAuthorityServiceFactory = ({ sshCertificateAuthorityDAL, sshCertificateAuthoritySecretDAL, sshCertificateTemplateDAL, + sshCertificateDAL, kmsService, permissionService }: TSshCertificateAuthorityServiceFactoryDep) => { @@ -252,10 +251,13 @@ export const sshCertificateAuthorityServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionSshCertificateTemplateActions.IssueSshCredentials, - OrgPermissionSubjects.SshCertificateTemplates - ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates); + + if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) { + throw new BadRequestError({ + message: "SSH CA is disabled" + }); + } // validate if the requested [certType] is allowed under the template configuration validateSshCertificateType(sshCertificateTemplate, certType); @@ -295,6 +297,18 @@ export const sshCertificateAuthorityServiceFactory = ({ certType }); + await sshCertificateDAL.create({ + sshCaId: sshCertificateTemplate.sshCaId, + sshCertificateTemplateId: sshCertificateTemplate.id, + serialNumber, + certType, + publicKey, + principals, + keyId, + notBefore: new Date(), + notAfter: new Date(Date.now() + ttl * 1000) + }); + return { serialNumber, signedPublicKey, @@ -337,10 +351,13 @@ export const sshCertificateAuthorityServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionSshCertificateTemplateActions.SignSshKey, - OrgPermissionSubjects.SshCertificateTemplates - ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates); + + if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) { + throw new BadRequestError({ + message: "SSH CA is disabled" + }); + } // validate if the requested [certType] is allowed under the template configuration validateSshCertificateType(sshCertificateTemplate, certType); @@ -377,6 +394,18 @@ export const sshCertificateAuthorityServiceFactory = ({ certType }); + await sshCertificateDAL.create({ + sshCaId: sshCertificateTemplate.sshCaId, + sshCertificateTemplateId: sshCertificateTemplate.id, + serialNumber, + certType, + publicKey, + principals, + keyId, + notBefore: new Date(), + notAfter: new Date(Date.now() + ttl * 1000) + }); + return { serialNumber, signedPublicKey, certificateTemplate: sshCertificateTemplate, ttl, keyId }; }; @@ -399,7 +428,7 @@ export const sshCertificateAuthorityServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionSshCertificateTemplateActions.Read, + OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates ); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index f1fce544b..d881a5233 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -387,6 +387,14 @@ export const ORGANIZATIONS = { }, LIST_SSH_CAS: { organizationId: "The ID of the organization to list SSH CAs for." + }, + LIST_SSH_CERTIFICATES: { + organizationId: "The ID of the organization to list SSH certificates for.", + offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.", + limit: "The number of SSH certificates to return." + }, + LIST_SSH_CERTIFICATE_TEMPLATES: { + organizationId: "The ID of the organization to list SSH certificate templates for." } } as const; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 187753d54..02ba4eba2 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -78,6 +78,7 @@ import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/sna 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 { sshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; 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"; @@ -347,6 +348,7 @@ export const registerRoutes = async ( const dynamicSecretDAL = dynamicSecretDALFactory(db); const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db); + const sshCertificateDAL = sshCertificateDALFactory(db); const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db); const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db); const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db); @@ -564,7 +566,9 @@ export const registerRoutes = async ( orgBotDAL, oidcConfigDAL, projectBotService, - sshCertificateAuthorityDAL + sshCertificateAuthorityDAL, + sshCertificateDAL, + sshCertificateTemplateDAL }); const signupService = authSignupServiceFactory({ tokenService, @@ -716,6 +720,7 @@ export const registerRoutes = async ( sshCertificateAuthorityDAL, sshCertificateAuthoritySecretDAL, sshCertificateTemplateDAL, + sshCertificateDAL, kmsService, permissionService }); diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index d6ab7f36c..c1b8881e9 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -12,6 +12,8 @@ import { } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema"; +import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema"; +import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema"; import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; import { getLastMidnightDateISO } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -406,6 +408,73 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/:organizationId/ssh-certificates", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + organizationId: z.string().trim().describe(ORGANIZATIONS.LIST_SSH_CAS.organizationId) + }), + querystring: z.object({ + offset: z.coerce.number().default(0).describe(ORGANIZATIONS.LIST_SSH_CERTIFICATES.offset), + limit: z.coerce.number().default(25).describe(ORGANIZATIONS.LIST_SSH_CERTIFICATES.limit) + }), + response: { + 200: z.object({ + certificates: z.array(sanitizedSshCertificate), + totalCount: z.number() // TODO + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificates, totalCount } = await server.services.org.listOrgSshCertificates({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + orgId: req.params.organizationId, + offset: req.query.offset, + limit: req.query.limit + }); + + return { certificates, totalCount }; + } + }); + + server.route({ + method: "GET", + url: "/:organizationId/ssh-certificate-templates", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + organizationId: z.string().trim().describe(ORGANIZATIONS.LIST_SSH_CERTIFICATE_TEMPLATES.organizationId) + }), + response: { + 200: z.object({ + certificateTemplates: z.array(sanitizedSshCertificateTemplate) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificateTemplates } = await server.services.org.listOrgSshCertificateTemplates({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + orgId: req.params.organizationId + }); + + return { certificateTemplates }; + } + }); + server.route({ method: "GET", url: "/:organizationId/ssh-cas", diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index b6bd95101..0871b9766 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -25,6 +25,8 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services 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 { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; +import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; +import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal"; import { getConfig } from "@app/lib/config/env"; import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; @@ -64,6 +66,8 @@ import { TGetOrgMembershipDTO, TInviteUserToOrgDTO, TListOrgSshCasDTO, + TListOrgSshCertificatesDTO, + TListOrgSshCertificateTemplatesDTO, TListProjectMembershipsByOrgMembershipIdDTO, TUpdateOrgDTO, TUpdateOrgMembershipDTO, @@ -101,6 +105,8 @@ type TOrgServiceFactoryDep = { projectUserMembershipRoleDAL: Pick; projectBotService: Pick; sshCertificateAuthorityDAL: Pick; + sshCertificateDAL: Pick; + sshCertificateTemplateDAL: Pick; }; export type TOrgServiceFactory = ReturnType; @@ -129,6 +135,8 @@ export const orgServiceFactory = ({ projectUserMembershipRoleDAL, identityMetadataDAL, sshCertificateAuthorityDAL, + sshCertificateDAL, + sshCertificateTemplateDAL, projectBotService }: TOrgServiceFactoryDep) => { /* @@ -1132,7 +1140,7 @@ export const orgServiceFactory = ({ }; /** - * Return list of SSH CAs for project + * Return list of SSH CAs for organization */ const listOrgSshCas = async ({ actorId, actorOrgId, actorAuthMethod, actor, orgId }: TListOrgSshCasDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); @@ -1152,6 +1160,70 @@ export const orgServiceFactory = ({ return cas; }; + /** + * Return list of SSH certificates for organization + */ + const listOrgSshCertificates = async ({ + limit = 25, + offset = 0, + actorId, + actorOrgId, + actorAuthMethod, + actor, + orgId + }: TListOrgSshCertificatesDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificates); + + const cas = await sshCertificateAuthorityDAL.find({ + orgId + }); + + const certificates = await sshCertificateDAL.find( + { + $in: { + sshCaId: cas.map((ca) => ca.id) + } + }, + { offset, limit, sort: [["updatedAt", "desc"]] } + ); + + const count = await sshCertificateDAL.countSshCertificatesInOrg(orgId); + + return { certificates, totalCount: count }; + }; + + /** + * Return list of SSH certificate templates for organization + */ + const listOrgSshCertificateTemplates = async ({ + actorId, + actorOrgId, + actorAuthMethod, + actor, + orgId + }: TListOrgSshCertificateTemplatesDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.SshCertificateTemplates + ); + + const cas = await sshCertificateAuthorityDAL.find({ + orgId + }); + + const certificateTemplates = await sshCertificateTemplateDAL.find({ + $in: { + sshCaId: cas.map((ca) => ca.id) + } + }); + + return { certificateTemplates }; + }; + return { findOrganizationById, findAllOrgMembers, @@ -1174,6 +1246,8 @@ export const orgServiceFactory = ({ getOrgGroups, listProjectMembershipsByOrgMembershipId, findOrgBySlug, - listOrgSshCas + listOrgSshCas, + listOrgSshCertificates, + listOrgSshCertificateTemplates }; }; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 66fe6ac2e..868d3345c 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -76,6 +76,11 @@ export type TListProjectMembershipsByOrgMembershipIdDTO = { } & TOrgPermission; export type TListOrgSshCasDTO = TOrgPermission; +export type TListOrgSshCertificateTemplatesDTO = TOrgPermission; +export type TListOrgSshCertificatesDTO = { + offset: number; + limit: number; +} & TOrgPermission; export enum OrgAuthMethod { OIDC = "oidc", diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 4678de7c4..3f3eb46e5 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -34,7 +34,8 @@ export enum OrgPermissionSubjects { AuditLogs = "audit-logs", ProjectTemplates = "project-templates", SshCertificateAuthorities = "ssh-certificate-authorities", - SshCertificateTemplates = "ssh-certificate-templates" + SshCertificateTemplates = "ssh-certificate-templates", + SshCertificates = "ssh-certificates" } export enum OrgPermissionAdminConsoleAction { @@ -60,6 +61,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities] + | [OrgPermissionActions, OrgPermissionSubjects.SshCertificates] | [OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects.SshCertificateTemplates]; export type TOrgPermission = MongoAbility; diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index 622a600b4..477b3bd8b 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -20,5 +20,7 @@ export { useGetOrgTaxIds, useGetOrgTrialUrl, useListOrgSshCas, + useListOrgSshCertificates, + useListOrgSshCertificateTemplates, useUpdateOrg, useUpdateOrgBillingDetails} from "./queries"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index f34c0ead1..9f4c83c1c 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -4,7 +4,8 @@ import { apiRequest } from "@app/config/request"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { TGroupOrgMembership } from "../groups/types"; -import { TSshCertificateAuthority } from "../ssh-ca/types"; +import { TSshCertificate,TSshCertificateAuthority } from "../ssh-ca/types"; +import { TSshCertificateTemplate } from "../sshCertificateTemplates/types"; import { IntegrationAuth } from "../types"; import { BillingDetails, @@ -43,7 +44,11 @@ export const organizationKeys = { [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const, getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const, - getOrgSshCas: ({ orgId }: { orgId: string }) => [{ orgId }, "org-ssh-cas"] as const + getOrgSshCas: ({ orgId }: { orgId: string }) => [{ orgId }, "org-ssh-cas"] as const, + allOrgSshCertificates: () => ["org-ssh-certificates"] as const, + specificOrgSshCertificates: ({ offset, limit }: { offset: number; limit: number }) => + [...organizationKeys.allOrgSshCertificates(), { offset, limit }] as const, + getOrgSshCertificateTemplates: () => ["org-ssh-certificate-templates"] as const }; export const fetchOrganizations = async () => { @@ -512,3 +517,48 @@ export const useListOrgSshCas = ({ orgId }: { orgId: string }) => { enabled: Boolean(orgId) }); }; + +export const useListOrgSshCertificates = ({ + orgId, + offset, + limit +}: { + orgId: string; + offset: number; + limit: number; +}) => { + return useQuery({ + queryKey: organizationKeys.specificOrgSshCertificates({ + offset, + limit + }), + queryFn: async () => { + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit) + }); + + const { data } = await apiRequest.get<{ + certificates: TSshCertificate[]; + totalCount: number; + }>(`/api/v1/organization/${orgId}/ssh-certificates`, { + params + }); + return data; + }, + enabled: Boolean(orgId) + }); +}; + +export const useListOrgSshCertificateTemplates = ({ orgId }: { orgId: string }) => { + return useQuery({ + queryKey: organizationKeys.getOrgSshCertificateTemplates(), + queryFn: async () => { + const { data } = await apiRequest.get<{ certificateTemplates: TSshCertificateTemplate[] }>( + `/api/v1/organization/${orgId}/ssh-certificate-templates` + ); + return data; + }, + enabled: Boolean(orgId) + }); +}; diff --git a/frontend/src/hooks/api/ssh-ca/enums.tsx b/frontend/src/hooks/api/ssh-ca/constants.tsx similarity index 50% rename from frontend/src/hooks/api/ssh-ca/enums.tsx rename to frontend/src/hooks/api/ssh-ca/constants.tsx index 3b5e949a9..2742a7bfa 100644 --- a/frontend/src/hooks/api/ssh-ca/enums.tsx +++ b/frontend/src/hooks/api/ssh-ca/constants.tsx @@ -7,3 +7,8 @@ export enum SshCertType { USER = "user", HOST = "host" } + +export const sshCertTypeToNameMap: { [K in SshCertType]: string } = { + [SshCertType.USER]: "User", + [SshCertType.HOST]: "Host" +}; diff --git a/frontend/src/hooks/api/ssh-ca/index.tsx b/frontend/src/hooks/api/ssh-ca/index.tsx index 0bdd4eb61..8fc57654b 100644 --- a/frontend/src/hooks/api/ssh-ca/index.tsx +++ b/frontend/src/hooks/api/ssh-ca/index.tsx @@ -1,8 +1,9 @@ -export { SshCaStatus } from "./enums"; +export { SshCaStatus } from "./constants"; export { useCreateSshCa, useDeleteSshCa, useIssueSshCreds, useSignSshKey, - useUpdateSshCa} from "./mutations"; + useUpdateSshCa +} from "./mutations"; export { useGetSshCaById, useGetSshCaCertTemplates } from "./queries"; diff --git a/frontend/src/hooks/api/ssh-ca/mutations.tsx b/frontend/src/hooks/api/ssh-ca/mutations.tsx index a2eb61ed4..811e09d18 100644 --- a/frontend/src/hooks/api/ssh-ca/mutations.tsx +++ b/frontend/src/hooks/api/ssh-ca/mutations.tsx @@ -11,7 +11,8 @@ import { TSignSshKeyDTO, TSignSshKeyResponse, TSshCertificateAuthority, - TUpdateSshCaDTO} from "./types"; + TUpdateSshCaDTO +} from "./types"; export const sshCaKeys = { getSshCaById: (caId: string) => [{ caId }, "ssh-ca"] @@ -64,19 +65,27 @@ export const useDeleteSshCa = () => { }; export const useSignSshKey = () => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: async (body) => { const { data } = await apiRequest.post("/api/v1/ssh/sign", body); return data; + }, + onSuccess: () => { + queryClient.invalidateQueries(organizationKeys.allOrgSshCertificates()); } }); }; export const useIssueSshCreds = () => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: async (body) => { const { data } = await apiRequest.post("/api/v1/ssh/issue", body); return data; + }, + onSuccess: () => { + queryClient.invalidateQueries(organizationKeys.allOrgSshCertificates()); } }); }; diff --git a/frontend/src/hooks/api/ssh-ca/types.ts b/frontend/src/hooks/api/ssh-ca/types.ts index 454e0f41a..747802c32 100644 --- a/frontend/src/hooks/api/ssh-ca/types.ts +++ b/frontend/src/hooks/api/ssh-ca/types.ts @@ -1,5 +1,16 @@ import { CertKeyAlgorithm } from "../certificates/enums"; -import { SshCaStatus, SshCertType } from "./enums"; +import { SshCaStatus, SshCertType } from "./constants"; + +export type TSshCertificate = { + id: string; + sshCaId: string; + sshCertificateTemplateId: string; + serialNumber: string; + certType: SshCertType; + publicKey: string; + principals: string[]; + keyId: string; +}; export type TSshCertificateAuthority = { id: string; diff --git a/frontend/src/pages/org/[id]/ssh/index.tsx b/frontend/src/pages/org/[id]/ssh/index.tsx index 1117806a8..ccd2001b7 100644 --- a/frontend/src/pages/org/[id]/ssh/index.tsx +++ b/frontend/src/pages/org/[id]/ssh/index.tsx @@ -3,11 +3,8 @@ import Head from "next/head"; import { SshPage } from "@app/views/Org/SshPage"; -// TODO: update meta tags - const Ssh = () => { const { t } = useTranslation(); - return ( <> diff --git a/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts index 14fb5e585..aea52d8c3 100644 --- a/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts @@ -13,6 +13,13 @@ const generalPermissionSchema = z }) .optional(); +const sshCertificateSchema = z + .object({ + read: z.boolean().optional(), + create: z.boolean().optional() + }) + .optional(); + const sshCertificateTemplatePermissionSchmea = z .object({ read: z.boolean().optional(), @@ -62,6 +69,7 @@ export const formSchema = z.object({ [OrgPermissionSubjects.Kms]: generalPermissionSchema, [OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema, [OrgPermissionSubjects.SshCertificateAuthorities]: generalPermissionSchema, + [OrgPermissionSubjects.SshCertificates]: sshCertificateSchema, "ssh-certificate-templates": sshCertificateTemplatePermissionSchmea }) .optional() diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx index 3a50dc976..4a5699ec6 100644 --- a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx @@ -51,6 +51,11 @@ const PROJECT_TEMPLATES_PERMISSIONS = [ { action: "delete", label: "Remove" } ] as const; +const SSH_CERTIFICATES_PERMISSIONS = [ + { action: "read", label: "View" }, + { action: "create", label: "Create" } +] as const; + const getPermissionList = (option: string) => { switch (option) { case "secret-scanning": @@ -63,6 +68,8 @@ const getPermissionList = (option: string) => { return MEMBERS_PERMISSIONS; case OrgPermissionSubjects.ProjectTemplates: return PROJECT_TEMPLATES_PERMISSIONS; + case OrgPermissionSubjects.SshCertificates: + return SSH_CERTIFICATES_PERMISSIONS; default: return PERMISSIONS; } diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index 46310566c..f4fcc84f9 100644 --- a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -15,7 +15,6 @@ import { import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow"; import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; import { RolePermissionRow } from "./RolePermissionRow"; -import { SshCertificateTemplateRow } from "./SshCertificateTemplateRow"; const SIMPLE_PERMISSION_OPTIONS = [ { @@ -74,6 +73,14 @@ const SIMPLE_PERMISSION_OPTIONS = [ { title: "SSH Certificate Authorities", formName: OrgPermissionSubjects.SshCertificateAuthorities + }, + { + title: "SSH Certificates", + formName: OrgPermissionSubjects.SshCertificates + }, + { + title: "SSH Certificate Templates", + formName: OrgPermissionSubjects.SshCertificateTemplates } ] as const; @@ -169,11 +176,6 @@ export const RolePermissionsSection = ({ roleId }: Props) => { /> ); })} - ; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - Custom = "custom" -} - -const PERMISSION_ACTIONS = [ - { action: "read", label: "Read" }, - { action: "create", label: "Create" }, - { action: "edit", label: "Modify" }, - { action: "delete", label: "Remove" }, - { action: "sign-ssh-key", label: "Sign SSH Key" }, - { action: "issue-ssh-credentials", label: "Issue SSH Credentials" } -] as const; - -export const SshCertificateTemplateRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.ssh-certificate-templates" - }); - - const selectedPermissionCategory = useMemo(() => { - if (rule?.create) { - return Permission.Custom; - } - return Permission.NoAccess; - }, [rule, isCustom]); - - useEffect(() => { - if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); - else setIsCustom.off(); - }, [selectedPermissionCategory]); - - useEffect(() => { - const isRowCustom = selectedPermissionCategory === Permission.Custom; - if (isRowCustom) { - setIsRowExpanded.on(); - } - }, []); - - const handlePermissionChange = (val: Permission) => { - if (!val) return; - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - if (val === Permission.NoAccess) { - setValue("permissions.workspace", { create: false }, { shouldDirty: true }); - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - SSH Certificate Templates - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.${OrgPermissionSubjects.SshCertificateTemplates}.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/views/Org/SshCaPage/components/SshCertificateContent.tsx b/frontend/src/views/Org/SshCaPage/components/SshCertificateContent.tsx index fb462742e..83aff5730 100644 --- a/frontend/src/views/Org/SshCaPage/components/SshCertificateContent.tsx +++ b/frontend/src/views/Org/SshCaPage/components/SshCertificateContent.tsx @@ -164,7 +164,7 @@ export const SshCertificateContent = ({ -
+

{publicKey}

diff --git a/frontend/src/views/Org/SshCaPage/components/SshCertificateModal.tsx b/frontend/src/views/Org/SshCaPage/components/SshCertificateModal.tsx index aa9ccbc88..0aa8cdaa8 100644 --- a/frontend/src/views/Org/SshCaPage/components/SshCertificateModal.tsx +++ b/frontend/src/views/Org/SshCaPage/components/SshCertificateModal.tsx @@ -13,7 +13,8 @@ import { Select, SelectItem } from "@app/components/v2"; -import { useGetSshCaCertTemplates, useIssueSshCreds, useSignSshKey } from "@app/hooks/api"; +import { useOrganization } from "@app/context"; +import { useIssueSshCreds, useListOrgSshCertificateTemplates, useSignSshKey } from "@app/hooks/api"; import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants"; import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; import { SshCertType } from "@app/hooks/api/ssh-ca/enums"; @@ -62,6 +63,7 @@ enum SshCertificateOperation { } export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => { + const { currentOrg } = useOrganization(); const [operation, setOperation] = useState( SshCertificateOperation.SIGN_SSH_KEY ); @@ -72,7 +74,9 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => { const popUpData = popUp?.sshCertificate?.data as { sshCaId: string; templateName: string }; - const { data: templatesData } = useGetSshCaCertTemplates(popUpData?.sshCaId || ""); + const { data: templatesData } = useListOrgSshCertificateTemplates({ + orgId: currentOrg?.id || "" + }); const { control, @@ -91,6 +95,8 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => { useEffect(() => { if (popUpData) { setValue("templateName", popUpData.templateName); + } else if (templatesData && templatesData.certificateTemplates.length > 0) { + setValue("templateName", templatesData.certificateTemplates[0].name); } }, [popUpData]); @@ -114,6 +120,7 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => { ttl, keyId }); + setCertificateDetails({ serialNumber, signedKey @@ -186,7 +193,7 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => { {...field} onValueChange={(e) => onChange(e)} className="w-full" - isDisabled + isDisabled={Boolean(popUpData?.sshCaId)} > {(templatesData?.certificateTemplates || []).map(({ id, name }) => ( diff --git a/frontend/src/views/Org/SshPage/SshPage.tsx b/frontend/src/views/Org/SshPage/SshPage.tsx index 653973faf..ff373749a 100644 --- a/frontend/src/views/Org/SshPage/SshPage.tsx +++ b/frontend/src/views/Org/SshPage/SshPage.tsx @@ -1,7 +1,15 @@ +import { motion } from "framer-motion"; + +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { withPermission } from "@app/hoc"; -import { SshCaSection } from "./components"; +import { SshCaSection, SshCertificatesSection } from "./components"; + +enum TabSections { + SshCa = "ssh-certificate-authorities", + SshCertificates = "ssh-certificates" +} export const SshPage = withPermission( () => { @@ -9,7 +17,34 @@ export const SshPage = withPermission(

SSH

- + + + SSH Certificates + Certificate Authorities + + + + + + + + + + + +
); diff --git a/frontend/src/views/Org/SshPage/components/SshCertificatesSection.tsx b/frontend/src/views/Org/SshPage/components/SshCertificatesSection.tsx new file mode 100644 index 000000000..49b4cfc4a --- /dev/null +++ b/frontend/src/views/Org/SshPage/components/SshCertificatesSection.tsx @@ -0,0 +1,36 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { SshCertificateModal } from "../../SshCaPage/components/SshCertificateModal"; +import { SshCertificatesTable } from "./SshCertificatesTable"; + +export const SshCertificatesSection = () => { + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["sshCertificate"] as const); + return ( +
+
+

Certificates

+ + {(isAllowed) => ( + + )} + +
+ + +
+ ); +}; diff --git a/frontend/src/views/Org/SshPage/components/SshCertificatesTable.tsx b/frontend/src/views/Org/SshPage/components/SshCertificatesTable.tsx new file mode 100644 index 000000000..4ef0e78ab --- /dev/null +++ b/frontend/src/views/Org/SshPage/components/SshCertificatesTable.tsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import { faCertificate } from "@fortawesome/free-solid-svg-icons"; + +import { + EmptyState, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useListOrgSshCertificates } from "@app/hooks/api"; +import { sshCertTypeToNameMap } from "@app/hooks/api/ssh-ca/constants"; + +const PER_PAGE_INIT = 25; + +export const SshCertificatesTable = () => { + const { currentOrg } = useOrganization(); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(PER_PAGE_INIT); + + const { data, isLoading } = useListOrgSshCertificates({ + orgId: currentOrg?.id ?? "", + offset: (page - 1) * perPage, + limit: perPage + }); + + return ( + + + + + + + + + + + {isLoading && } + {!isLoading && + data?.certificates?.map((certificate) => { + return ( + + + + + + ); + })} + +
Serial NumberCertificate TypePrincipals
{certificate.serialNumber}{sshCertTypeToNameMap[certificate.certType]}{certificate.principals.join(", ")}
+ {!isLoading && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {!isLoading && !data?.certificates?.length && ( + + )} +
+ ); +}; diff --git a/frontend/src/views/Org/SshPage/components/index.tsx b/frontend/src/views/Org/SshPage/components/index.tsx index 0ba2a04c7..efb307725 100644 --- a/frontend/src/views/Org/SshPage/components/index.tsx +++ b/frontend/src/views/Org/SshPage/components/index.tsx @@ -1 +1,2 @@ export { SshCaSection } from "./SshCaSection"; +export { SshCertificatesSection } from "./SshCertificatesSection";