Add SSH certificate tab + data structure

This commit is contained in:
Tuan Dang
2024-12-05 23:01:28 -08:00
parent 5b618b07fa
commit 6e720c2f64
33 changed files with 604 additions and 210 deletions
+8
View File
@@ -317,6 +317,9 @@ import {
TSshCertificateAuthoritySecrets, TSshCertificateAuthoritySecrets,
TSshCertificateAuthoritySecretsInsert, TSshCertificateAuthoritySecretsInsert,
TSshCertificateAuthoritySecretsUpdate, TSshCertificateAuthoritySecretsUpdate,
TSshCertificates,
TSshCertificatesInsert,
TSshCertificatesUpdate,
TSshCertificateTemplates, TSshCertificateTemplates,
TSshCertificateTemplatesInsert, TSshCertificateTemplatesInsert,
TSshCertificateTemplatesUpdate, TSshCertificateTemplatesUpdate,
@@ -396,6 +399,11 @@ declare module "knex/types/tables" {
TSshCertificateTemplatesInsert, TSshCertificateTemplatesInsert,
TSshCertificateTemplatesUpdate TSshCertificateTemplatesUpdate
>; >;
[TableName.SshCertificate]: KnexOriginal.CompositeTableType<
TSshCertificates,
TSshCertificatesInsert,
TSshCertificatesUpdate
>;
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType< [TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
TCertificateAuthorities, TCertificateAuthorities,
TCertificateAuthoritiesInsert, TCertificateAuthoritiesInsert,
@@ -34,7 +34,7 @@ export async function up(knex: Knex): Promise<void> {
t.timestamps(true, true, true); t.timestamps(true, true, true);
t.uuid("sshCaId").notNullable(); t.uuid("sshCaId").notNullable();
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE"); t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
t.string("name").notNullable(); // note: how do we handle this being unique? across orgs? t.string("name").notNullable();
t.string("ttl").notNullable(); t.string("ttl").notNullable();
t.string("maxTTL").notNullable(); t.string("maxTTL").notNullable();
t.specificType("allowedUsers", "text[]").notNullable(); t.specificType("allowedUsers", "text[]").notNullable();
@@ -45,9 +45,34 @@ export async function up(knex: Knex): Promise<void> {
}); });
await createOnUpdateTrigger(knex, TableName.SshCertificateTemplate); 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<void> { export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SshCertificate);
await dropOnUpdateTrigger(knex, TableName.SshCertificate);
await knex.schema.dropTableIfExists(TableName.SshCertificateTemplate); await knex.schema.dropTableIfExists(TableName.SshCertificateTemplate);
await dropOnUpdateTrigger(knex, TableName.SshCertificateTemplate); await dropOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
+1
View File
@@ -108,6 +108,7 @@ export * from "./slack-integrations";
export * from "./ssh-certificate-authorities"; export * from "./ssh-certificate-authorities";
export * from "./ssh-certificate-authority-secrets"; export * from "./ssh-certificate-authority-secrets";
export * from "./ssh-certificate-templates"; export * from "./ssh-certificate-templates";
export * from "./ssh-certificates";
export * from "./super-admin"; export * from "./super-admin";
export * from "./totp-configs"; export * from "./totp-configs";
export * from "./trusted-ips"; export * from "./trusted-ips";
+1
View File
@@ -5,6 +5,7 @@ export enum TableName {
SshCertificateAuthority = "ssh_certificate_authorities", SshCertificateAuthority = "ssh_certificate_authorities",
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets", SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
SshCertificateTemplate = "ssh_certificate_templates", SshCertificateTemplate = "ssh_certificate_templates",
SshCertificate = "ssh_certificates",
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,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<typeof SshCertificatesSchema>;
export type TSshCertificatesInsert = Omit<z.input<typeof SshCertificatesSchema>, TImmutableDBKeys>;
export type TSshCertificatesUpdate = Partial<Omit<z.input<typeof SshCertificatesSchema>, TImmutableDBKeys>>;
@@ -7,15 +7,6 @@ export enum OrgPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum OrgPermissionSshCertificateTemplateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
SignSshKey = "sign-ssh-key",
IssueSshCredentials = "issue-ssh-credentials"
}
export enum OrgPermissionAdminConsoleAction { export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects" AccessAllProjects = "access-all-projects"
} }
@@ -37,6 +28,7 @@ export enum OrgPermissionSubjects {
AdminConsole = "organization-admin-console", AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs", AuditLogs = "audit-logs",
ProjectTemplates = "project-templates", ProjectTemplates = "project-templates",
SshCertificates = "ssh-certificates",
SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificateAuthorities = "ssh-certificate-authorities",
SshCertificateTemplates = "ssh-certificate-templates" SshCertificateTemplates = "ssh-certificate-templates"
} }
@@ -59,7 +51,8 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities] | [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities]
| [OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects.SshCertificateTemplates]; | [OrgPermissionActions, OrgPermissionSubjects.SshCertificates]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateTemplates];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@@ -136,22 +129,18 @@ 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.SshCertificates);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateAuthorities);
can( can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates);
[ can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificateTemplates);
OrgPermissionSshCertificateTemplateActions.Read, can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateTemplates);
OrgPermissionSshCertificateTemplateActions.Create, can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateTemplates);
OrgPermissionSshCertificateTemplateActions.Edit,
OrgPermissionSshCertificateTemplateActions.Delete,
OrgPermissionSshCertificateTemplateActions.SignSshKey,
OrgPermissionSshCertificateTemplateActions.IssueSshCredentials
],
OrgPermissionSubjects.SshCertificateTemplates
);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
@@ -184,9 +173,9 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionSshCertificateTemplateActions.Read, OrgPermissionSubjects.SshCertificateTemplates); can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificates);
can(OrgPermissionSshCertificateTemplateActions.SignSshKey, OrgPermissionSubjects.SshCertificateTemplates); can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates);
can(OrgPermissionSshCertificateTemplateActions.IssueSshCredentials, OrgPermissionSubjects.SshCertificateTemplates); can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates);
return rules; return rules;
}; };
@@ -1,10 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
OrgPermissionSshCertificateTemplateActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -61,7 +58,7 @@ export const sshCertificateTemplateServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.Create, OrgPermissionActions.Create,
OrgPermissionSubjects.SshCertificateTemplates OrgPermissionSubjects.SshCertificateTemplates
); );
@@ -124,7 +121,7 @@ export const sshCertificateTemplateServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.Edit, OrgPermissionActions.Edit,
OrgPermissionSubjects.SshCertificateTemplates OrgPermissionSubjects.SshCertificateTemplates
); );
@@ -183,7 +180,7 @@ export const sshCertificateTemplateServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.Delete, OrgPermissionActions.Delete,
OrgPermissionSubjects.SshCertificateTemplates OrgPermissionSubjects.SshCertificateTemplates
); );
@@ -209,7 +206,7 @@ export const sshCertificateTemplateServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.SshCertificateTemplates OrgPermissionSubjects.SshCertificateTemplates
); );
@@ -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<typeof sshCertificateDALFactory>;
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
};
};
@@ -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
});
@@ -1,15 +1,12 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
OrgPermissionActions,
OrgPermissionSshCertificateTemplateActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-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 { 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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { import {
@@ -38,6 +35,7 @@ type TSshCertificateAuthorityServiceFactoryDep = {
>; >;
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create" | "findOne">; sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create" | "findOne">;
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find" | "getByName">; sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find" | "getByName">;
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "getOrgKmsKeyId">; kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "getOrgKmsKeyId">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
}; };
@@ -48,6 +46,7 @@ export const sshCertificateAuthorityServiceFactory = ({
sshCertificateAuthorityDAL, sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL, sshCertificateAuthoritySecretDAL,
sshCertificateTemplateDAL, sshCertificateTemplateDAL,
sshCertificateDAL,
kmsService, kmsService,
permissionService permissionService
}: TSshCertificateAuthorityServiceFactoryDep) => { }: TSshCertificateAuthorityServiceFactoryDep) => {
@@ -252,10 +251,13 @@ export const sshCertificateAuthorityServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates);
OrgPermissionSshCertificateTemplateActions.IssueSshCredentials,
OrgPermissionSubjects.SshCertificateTemplates if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) {
); throw new BadRequestError({
message: "SSH CA is disabled"
});
}
// validate if the requested [certType] is allowed under the template configuration // validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(sshCertificateTemplate, certType); validateSshCertificateType(sshCertificateTemplate, certType);
@@ -295,6 +297,18 @@ export const sshCertificateAuthorityServiceFactory = ({
certType 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 { return {
serialNumber, serialNumber,
signedPublicKey, signedPublicKey,
@@ -337,10 +351,13 @@ export const sshCertificateAuthorityServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates);
OrgPermissionSshCertificateTemplateActions.SignSshKey,
OrgPermissionSubjects.SshCertificateTemplates if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) {
); throw new BadRequestError({
message: "SSH CA is disabled"
});
}
// validate if the requested [certType] is allowed under the template configuration // validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(sshCertificateTemplate, certType); validateSshCertificateType(sshCertificateTemplate, certType);
@@ -377,6 +394,18 @@ export const sshCertificateAuthorityServiceFactory = ({
certType 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 }; return { serialNumber, signedPublicKey, certificateTemplate: sshCertificateTemplate, ttl, keyId };
}; };
@@ -399,7 +428,7 @@ export const sshCertificateAuthorityServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.SshCertificateTemplates OrgPermissionSubjects.SshCertificateTemplates
); );
+8
View File
@@ -387,6 +387,14 @@ export const ORGANIZATIONS = {
}, },
LIST_SSH_CAS: { LIST_SSH_CAS: {
organizationId: "The ID of the organization to list SSH CAs for." 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; } as const;
+6 -1
View File
@@ -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 { sshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
import { sshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-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 { 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 { 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 { 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";
@@ -347,6 +348,7 @@ export const registerRoutes = async (
const dynamicSecretDAL = dynamicSecretDALFactory(db); const dynamicSecretDAL = dynamicSecretDALFactory(db);
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db); const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
const sshCertificateDAL = sshCertificateDALFactory(db);
const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db); const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db);
const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db); const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db);
const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db); const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db);
@@ -564,7 +566,9 @@ export const registerRoutes = async (
orgBotDAL, orgBotDAL,
oidcConfigDAL, oidcConfigDAL,
projectBotService, projectBotService,
sshCertificateAuthorityDAL sshCertificateAuthorityDAL,
sshCertificateDAL,
sshCertificateTemplateDAL
}); });
const signupService = authSignupServiceFactory({ const signupService = authSignupServiceFactory({
tokenService, tokenService,
@@ -716,6 +720,7 @@ export const registerRoutes = async (
sshCertificateAuthorityDAL, sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL, sshCertificateAuthoritySecretDAL,
sshCertificateTemplateDAL, sshCertificateTemplateDAL,
sshCertificateDAL,
kmsService, kmsService,
permissionService permissionService
}); });
@@ -12,6 +12,8 @@ import {
} 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 { 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 { 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";
@@ -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({ server.route({
method: "GET", method: "GET",
url: "/:organizationId/ssh-cas", url: "/:organizationId/ssh-cas",
+76 -2
View File
@@ -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 { 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 { 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 { 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";
@@ -64,6 +66,8 @@ import {
TGetOrgMembershipDTO, TGetOrgMembershipDTO,
TInviteUserToOrgDTO, TInviteUserToOrgDTO,
TListOrgSshCasDTO, TListOrgSshCasDTO,
TListOrgSshCertificatesDTO,
TListOrgSshCertificateTemplatesDTO,
TListProjectMembershipsByOrgMembershipIdDTO, TListProjectMembershipsByOrgMembershipIdDTO,
TUpdateOrgDTO, TUpdateOrgDTO,
TUpdateOrgMembershipDTO, TUpdateOrgMembershipDTO,
@@ -101,6 +105,8 @@ type TOrgServiceFactoryDep = {
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find">; sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find">;
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInOrg">;
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
}; };
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>; export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@@ -129,6 +135,8 @@ export const orgServiceFactory = ({
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityMetadataDAL, identityMetadataDAL,
sshCertificateAuthorityDAL, sshCertificateAuthorityDAL,
sshCertificateDAL,
sshCertificateTemplateDAL,
projectBotService projectBotService
}: TOrgServiceFactoryDep) => { }: 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 listOrgSshCas = async ({ actorId, actorOrgId, actorAuthMethod, actor, orgId }: TListOrgSshCasDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
@@ -1152,6 +1160,70 @@ export const orgServiceFactory = ({
return cas; 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 { return {
findOrganizationById, findOrganizationById,
findAllOrgMembers, findAllOrgMembers,
@@ -1174,6 +1246,8 @@ export const orgServiceFactory = ({
getOrgGroups, getOrgGroups,
listProjectMembershipsByOrgMembershipId, listProjectMembershipsByOrgMembershipId,
findOrgBySlug, findOrgBySlug,
listOrgSshCas listOrgSshCas,
listOrgSshCertificates,
listOrgSshCertificateTemplates
}; };
}; };
+5
View File
@@ -76,6 +76,11 @@ export type TListProjectMembershipsByOrgMembershipIdDTO = {
} & TOrgPermission; } & TOrgPermission;
export type TListOrgSshCasDTO = TOrgPermission; export type TListOrgSshCasDTO = TOrgPermission;
export type TListOrgSshCertificateTemplatesDTO = TOrgPermission;
export type TListOrgSshCertificatesDTO = {
offset: number;
limit: number;
} & TOrgPermission;
export enum OrgAuthMethod { export enum OrgAuthMethod {
OIDC = "oidc", OIDC = "oidc",
@@ -34,7 +34,8 @@ export enum OrgPermissionSubjects {
AuditLogs = "audit-logs", AuditLogs = "audit-logs",
ProjectTemplates = "project-templates", ProjectTemplates = "project-templates",
SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificateAuthorities = "ssh-certificate-authorities",
SshCertificateTemplates = "ssh-certificate-templates" SshCertificateTemplates = "ssh-certificate-templates",
SshCertificates = "ssh-certificates"
} }
export enum OrgPermissionAdminConsoleAction { export enum OrgPermissionAdminConsoleAction {
@@ -60,6 +61,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities] | [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificates]
| [OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects.SshCertificateTemplates]; | [OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects.SshCertificateTemplates];
export type TOrgPermission = MongoAbility<OrgPermissionSet>; export type TOrgPermission = MongoAbility<OrgPermissionSet>;
@@ -20,5 +20,7 @@ export {
useGetOrgTaxIds, useGetOrgTaxIds,
useGetOrgTrialUrl, useGetOrgTrialUrl,
useListOrgSshCas, useListOrgSshCas,
useListOrgSshCertificates,
useListOrgSshCertificateTemplates,
useUpdateOrg, useUpdateOrg,
useUpdateOrgBillingDetails} from "./queries"; useUpdateOrgBillingDetails} from "./queries";
@@ -4,7 +4,8 @@ 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 { TSshCertificate,TSshCertificateAuthority } from "../ssh-ca/types";
import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
import { IntegrationAuth } from "../types"; import { IntegrationAuth } from "../types";
import { import {
BillingDetails, BillingDetails,
@@ -43,7 +44,11 @@ export const organizationKeys = {
[...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 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 () => { export const fetchOrganizations = async () => {
@@ -512,3 +517,48 @@ export const useListOrgSshCas = ({ orgId }: { orgId: string }) => {
enabled: Boolean(orgId) 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)
});
};
@@ -7,3 +7,8 @@ export enum SshCertType {
USER = "user", USER = "user",
HOST = "host" HOST = "host"
} }
export const sshCertTypeToNameMap: { [K in SshCertType]: string } = {
[SshCertType.USER]: "User",
[SshCertType.HOST]: "Host"
};
+3 -2
View File
@@ -1,8 +1,9 @@
export { SshCaStatus } from "./enums"; export { SshCaStatus } from "./constants";
export { export {
useCreateSshCa, useCreateSshCa,
useDeleteSshCa, useDeleteSshCa,
useIssueSshCreds, useIssueSshCreds,
useSignSshKey, useSignSshKey,
useUpdateSshCa} from "./mutations"; useUpdateSshCa
} from "./mutations";
export { useGetSshCaById, useGetSshCaCertTemplates } from "./queries"; export { useGetSshCaById, useGetSshCaCertTemplates } from "./queries";
+10 -1
View File
@@ -11,7 +11,8 @@ import {
TSignSshKeyDTO, TSignSshKeyDTO,
TSignSshKeyResponse, TSignSshKeyResponse,
TSshCertificateAuthority, TSshCertificateAuthority,
TUpdateSshCaDTO} from "./types"; TUpdateSshCaDTO
} from "./types";
export const sshCaKeys = { export const sshCaKeys = {
getSshCaById: (caId: string) => [{ caId }, "ssh-ca"] getSshCaById: (caId: string) => [{ caId }, "ssh-ca"]
@@ -64,19 +65,27 @@ export const useDeleteSshCa = () => {
}; };
export const useSignSshKey = () => { export const useSignSshKey = () => {
const queryClient = useQueryClient();
return useMutation<TSignSshKeyResponse, {}, TSignSshKeyDTO>({ return useMutation<TSignSshKeyResponse, {}, TSignSshKeyDTO>({
mutationFn: async (body) => { mutationFn: async (body) => {
const { data } = await apiRequest.post<TSignSshKeyResponse>("/api/v1/ssh/sign", body); const { data } = await apiRequest.post<TSignSshKeyResponse>("/api/v1/ssh/sign", body);
return data; return data;
},
onSuccess: () => {
queryClient.invalidateQueries(organizationKeys.allOrgSshCertificates());
} }
}); });
}; };
export const useIssueSshCreds = () => { export const useIssueSshCreds = () => {
const queryClient = useQueryClient();
return useMutation<TIssueSshCredsResponse, {}, TIssueSshCredsDTO>({ return useMutation<TIssueSshCredsResponse, {}, TIssueSshCredsDTO>({
mutationFn: async (body) => { mutationFn: async (body) => {
const { data } = await apiRequest.post<TIssueSshCredsResponse>("/api/v1/ssh/issue", body); const { data } = await apiRequest.post<TIssueSshCredsResponse>("/api/v1/ssh/issue", body);
return data; return data;
},
onSuccess: () => {
queryClient.invalidateQueries(organizationKeys.allOrgSshCertificates());
} }
}); });
}; };
+12 -1
View File
@@ -1,5 +1,16 @@
import { CertKeyAlgorithm } from "../certificates/enums"; 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 = { export type TSshCertificateAuthority = {
id: string; id: string;
@@ -3,11 +3,8 @@ import Head from "next/head";
import { SshPage } from "@app/views/Org/SshPage"; import { SshPage } from "@app/views/Org/SshPage";
// TODO: update meta tags
const Ssh = () => { const Ssh = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
<Head> <Head>
@@ -13,6 +13,13 @@ const generalPermissionSchema = z
}) })
.optional(); .optional();
const sshCertificateSchema = z
.object({
read: z.boolean().optional(),
create: z.boolean().optional()
})
.optional();
const sshCertificateTemplatePermissionSchmea = z const sshCertificateTemplatePermissionSchmea = z
.object({ .object({
read: z.boolean().optional(), read: z.boolean().optional(),
@@ -62,6 +69,7 @@ export const formSchema = z.object({
[OrgPermissionSubjects.Kms]: generalPermissionSchema, [OrgPermissionSubjects.Kms]: generalPermissionSchema,
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema, [OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
[OrgPermissionSubjects.SshCertificateAuthorities]: generalPermissionSchema, [OrgPermissionSubjects.SshCertificateAuthorities]: generalPermissionSchema,
[OrgPermissionSubjects.SshCertificates]: sshCertificateSchema,
"ssh-certificate-templates": sshCertificateTemplatePermissionSchmea "ssh-certificate-templates": sshCertificateTemplatePermissionSchmea
}) })
.optional() .optional()
@@ -51,6 +51,11 @@ const PROJECT_TEMPLATES_PERMISSIONS = [
{ action: "delete", label: "Remove" } { action: "delete", label: "Remove" }
] as const; ] as const;
const SSH_CERTIFICATES_PERMISSIONS = [
{ action: "read", label: "View" },
{ action: "create", label: "Create" }
] as const;
const getPermissionList = (option: string) => { const getPermissionList = (option: string) => {
switch (option) { switch (option) {
case "secret-scanning": case "secret-scanning":
@@ -63,6 +68,8 @@ const getPermissionList = (option: string) => {
return MEMBERS_PERMISSIONS; return MEMBERS_PERMISSIONS;
case OrgPermissionSubjects.ProjectTemplates: case OrgPermissionSubjects.ProjectTemplates:
return PROJECT_TEMPLATES_PERMISSIONS; return PROJECT_TEMPLATES_PERMISSIONS;
case OrgPermissionSubjects.SshCertificates:
return SSH_CERTIFICATES_PERMISSIONS;
default: default:
return PERMISSIONS; return PERMISSIONS;
} }
@@ -15,7 +15,6 @@ import {
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow"; import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
import { RolePermissionRow } from "./RolePermissionRow"; import { RolePermissionRow } from "./RolePermissionRow";
import { SshCertificateTemplateRow } from "./SshCertificateTemplateRow";
const SIMPLE_PERMISSION_OPTIONS = [ const SIMPLE_PERMISSION_OPTIONS = [
{ {
@@ -74,6 +73,14 @@ const SIMPLE_PERMISSION_OPTIONS = [
{ {
title: "SSH Certificate Authorities", title: "SSH Certificate Authorities",
formName: OrgPermissionSubjects.SshCertificateAuthorities formName: OrgPermissionSubjects.SshCertificateAuthorities
},
{
title: "SSH Certificates",
formName: OrgPermissionSubjects.SshCertificates
},
{
title: "SSH Certificate Templates",
formName: OrgPermissionSubjects.SshCertificateTemplates
} }
] as const; ] as const;
@@ -169,11 +176,6 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
/> />
); );
})} })}
<SshCertificateTemplateRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgRoleWorkspaceRow <OrgRoleWorkspaceRow
control={control} control={control}
setValue={setValue} setValue={setValue}
@@ -1,137 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { OrgPermissionSubjects } from "@app/context";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
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 (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>SSH Certificate Templates</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.${OrgPermissionSubjects.SshCertificateTemplates}.${action}`}
key={`permissions.${OrgPermissionSubjects.SshCertificateTemplates}.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.${OrgPermissionSubjects.SshCertificateTemplates}.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};
@@ -164,7 +164,7 @@ export const SshCertificateContent = ({
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400"> <div className="flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 whitespace-pre-wrap break-all">{publicKey}</p> <p className="mr-4 whitespace-pre-wrap break-all">{publicKey}</p>
</div> </div>
</> </>
@@ -13,7 +13,8 @@ import {
Select, Select,
SelectItem SelectItem
} from "@app/components/v2"; } 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 { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { SshCertType } from "@app/hooks/api/ssh-ca/enums"; import { SshCertType } from "@app/hooks/api/ssh-ca/enums";
@@ -62,6 +63,7 @@ enum SshCertificateOperation {
} }
export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => { export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const [operation, setOperation] = useState<SshCertificateOperation>( const [operation, setOperation] = useState<SshCertificateOperation>(
SshCertificateOperation.SIGN_SSH_KEY 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 popUpData = popUp?.sshCertificate?.data as { sshCaId: string; templateName: string };
const { data: templatesData } = useGetSshCaCertTemplates(popUpData?.sshCaId || ""); const { data: templatesData } = useListOrgSshCertificateTemplates({
orgId: currentOrg?.id || ""
});
const { const {
control, control,
@@ -91,6 +95,8 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
useEffect(() => { useEffect(() => {
if (popUpData) { if (popUpData) {
setValue("templateName", popUpData.templateName); setValue("templateName", popUpData.templateName);
} else if (templatesData && templatesData.certificateTemplates.length > 0) {
setValue("templateName", templatesData.certificateTemplates[0].name);
} }
}, [popUpData]); }, [popUpData]);
@@ -114,6 +120,7 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
ttl, ttl,
keyId keyId
}); });
setCertificateDetails({ setCertificateDetails({
serialNumber, serialNumber,
signedKey signedKey
@@ -186,7 +193,7 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
{...field} {...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full" className="w-full"
isDisabled isDisabled={Boolean(popUpData?.sshCaId)}
> >
{(templatesData?.certificateTemplates || []).map(({ id, name }) => ( {(templatesData?.certificateTemplates || []).map(({ id, name }) => (
<SelectItem value={name} key={`ssh-cert-template-${id}`}> <SelectItem value={name} key={`ssh-cert-template-${id}`}>
+37 -2
View File
@@ -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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc"; 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( export const SshPage = withPermission(
() => { () => {
@@ -9,7 +17,34 @@ export const SshPage = withPermission(
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <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"> <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> <p className="mr-4 mb-4 text-3xl font-semibold text-white">SSH</p>
<SshCaSection /> <Tabs defaultValue={TabSections.SshCertificates}>
<TabList>
<Tab value={TabSections.SshCertificates}>SSH Certificates</Tab>
<Tab value={TabSections.SshCa}>Certificate Authorities</Tab>
</TabList>
<TabPanel value={TabSections.SshCertificates}>
<motion.div
key="panel-ssh-certificate-s"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<SshCertificatesSection />
</motion.div>
</TabPanel>
<TabPanel value={TabSections.SshCa}>
<motion.div
key="panel-ssh-certificate-authorities"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<SshCaSection />
</motion.div>
</TabPanel>
</Tabs>
</div> </div>
</div> </div>
); );
@@ -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 (
<div className="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">Certificates</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.SshCertificates}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("sshCertificate")}
isDisabled={!isAllowed}
>
Request
</Button>
)}
</OrgPermissionCan>
</div>
<SshCertificatesTable />
<SshCertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</div>
);
};
@@ -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 (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Serial Number</Th>
<Th>Certificate Type</Th>
<Th>Principals</Th>
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-ssh-certificates" />}
{!isLoading &&
data?.certificates?.map((certificate) => {
return (
<Tr className="h-10" key={`certificate-${certificate.id}`}>
<Td>{certificate.serialNumber}</Td>
<Td>{sshCertTypeToNameMap[certificate.certType]}</Td>
<Td>{certificate.principals.join(", ")}</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isLoading && !data?.certificates?.length && (
<EmptyState title="No SSH certificates have been issued" icon={faCertificate} />
)}
</TableContainer>
);
};
@@ -1 +1,2 @@
export { SshCaSection } from "./SshCaSection"; export { SshCaSection } from "./SshCaSection";
export { SshCertificatesSection } from "./SshCertificatesSection";