Add permissioning to SSH, add publicKey return for SSH CA, polish

This commit is contained in:
Tuan Dang
2024-12-03 17:38:23 -08:00
parent 4fc8c509ac
commit 07d9398aad
25 changed files with 397 additions and 106 deletions
@@ -29,7 +29,9 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
ca: sanitizedSshCa ca: sanitizedSshCa.extend({
publicKey: z.string()
})
}) })
} }
}, },
@@ -74,7 +76,9 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
ca: sanitizedSshCa ca: sanitizedSshCa.extend({
publicKey: z.string()
})
}) })
} }
}, },
@@ -109,7 +113,7 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
method: "PATCH", method: "PATCH",
url: "/:sshCaId", url: "/:sshCaId",
config: { config: {
rateLimit: readLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
@@ -118,6 +122,7 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.sshCaId) sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.sshCaId)
}), }),
body: z.object({ body: z.object({
friendlyName: z.string().optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.friendlyName),
status: z status: z
.enum([SshCaStatus.ACTIVE, SshCaStatus.DISABLED]) .enum([SshCaStatus.ACTIVE, SshCaStatus.DISABLED])
.optional() .optional()
@@ -125,7 +130,9 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
ca: sanitizedSshCa ca: sanitizedSshCa.extend({
publicKey: z.string()
})
}) })
} }
}, },
@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms"; import ms from "ms";
import { z } from "zod"; import { z } from "zod";
@@ -61,7 +62,14 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
schema: { schema: {
body: z.object({ body: z.object({
sshCaId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.sshCaId), sshCaId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.sshCaId),
name: z.string().min(1).describe(SSH_CERTIFICATE_TEMPLATES.CREATE.name), name: z
.string()
.min(1)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Name must be a valid slug"
})
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.name),
ttl: z ttl: z
.string() .string()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
@@ -128,7 +136,15 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
}, },
schema: { schema: {
body: z.object({ body: z.object({
name: z.string().min(1).optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.name), name: z
.string()
.min(1)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.name),
ttl: z ttl: z
.string() .string()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
+34 -19
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types"; import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { CERTIFICATE_AUTHORITIES, CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; // TODO: update to SSH CA import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -20,21 +20,27 @@ export const registerSshRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Sign SSH public key", description: "Sign SSH public key",
body: z.object({ body: z.object({
name: z.string(), // name of SSH certificate template templateName: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.templateName),
publicKey: z.string(), publicKey: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.publicKey),
certType: z.nativeEnum(SshCertType).default(SshCertType.USER), certType: z
principals: z.array(z.string().transform((val) => val.trim())).nonempty("Principals array must not be empty"), .nativeEnum(SshCertType)
.default(SshCertType.USER)
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.certType),
principals: z
.array(z.string().transform((val) => val.trim()))
.nonempty("Principals array must not be empty")
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.principals),
ttl: z ttl: z
.string() .string()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional() .optional()
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl), .describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.ttl),
keyId: z.string().optional() keyId: z.string().trim().optional().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.keyId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
serialNumber: z.string(), serialNumber: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.serialNumber),
signedKey: z.string() signedKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.signedKey)
}) })
} }
}, },
@@ -80,26 +86,35 @@ export const registerSshRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Issue SSH credentials (certificate + key)", description: "Issue SSH credentials (certificate + key)",
body: z.object({ body: z.object({
name: z.string(), // name of SSH certificate template templateName: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.templateName),
keyAlgorithm: z keyAlgorithm: z
.nativeEnum(CertKeyAlgorithm) .nativeEnum(CertKeyAlgorithm)
.default(CertKeyAlgorithm.RSA_2048) .default(CertKeyAlgorithm.RSA_2048)
.describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm), .describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm),
certType: z.nativeEnum(SshCertType).default(SshCertType.USER), certType: z
principals: z.array(z.string().transform((val) => val.trim())).nonempty("Principals array must not be empty"), .nativeEnum(SshCertType)
.default(SshCertType.USER)
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certType),
principals: z
.array(z.string().transform((val) => val.trim()))
.nonempty("Principals array must not be empty")
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.principals),
ttl: z ttl: z
.string() .string()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional() .optional()
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl), .describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.ttl),
keyId: z.string().optional() keyId: z.string().trim().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
serialNumber: z.string(), serialNumber: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.serialNumber),
signedKey: z.string(), signedKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.signedKey),
privateKey: z.string(), privateKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.privateKey),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm) publicKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.publicKey),
keyAlgorithm: z
.nativeEnum(CertKeyAlgorithm)
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
}) })
} }
}, },
@@ -7,6 +7,15 @@ 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"
} }
@@ -50,7 +59,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities] | [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateTemplates]; | [OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects.SshCertificateTemplates];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@@ -132,10 +141,17 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates); can(
can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificateTemplates); [
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateTemplates); OrgPermissionSshCertificateTemplateActions.Read,
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateTemplates); OrgPermissionSshCertificateTemplateActions.Create,
OrgPermissionSshCertificateTemplateActions.Edit,
OrgPermissionSshCertificateTemplateActions.Delete,
OrgPermissionSshCertificateTemplateActions.SignSshKey,
OrgPermissionSshCertificateTemplateActions.IssueSshCredentials
],
OrgPermissionSubjects.SshCertificateTemplates
);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
@@ -168,7 +184,9 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities); can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates); can(OrgPermissionSshCertificateTemplateActions.Read, OrgPermissionSubjects.SshCertificateTemplates);
can(OrgPermissionSshCertificateTemplateActions.SignSshKey, OrgPermissionSubjects.SshCertificateTemplates);
can(OrgPermissionSshCertificateTemplateActions.IssueSshCredentials, OrgPermissionSubjects.SshCertificateTemplates);
return rules; return rules;
}; };
@@ -34,5 +34,30 @@ export const sshCertificateTemplateDALFactory = (db: TDbClient) => {
} }
}; };
return { ...sshCertificateTemplateOrm, getById }; const getByName = async (name: string, orgId: string, tx?: Knex) => {
try {
const certTemplate = await (tx || db.replicaNode())(TableName.SshCertificateTemplate)
.join(
TableName.SshCertificateAuthority,
`${TableName.SshCertificateAuthority}.id`,
`${TableName.SshCertificateTemplate}.sshCaId`
)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.SshCertificateAuthority}.orgId`)
.where(`${TableName.SshCertificateTemplate}.name`, "=", name)
.where(`${TableName.Organization}.id`, "=", orgId)
.select(selectAllTableCols(TableName.SshCertificateTemplate))
.select(
db.ref("orgId").withSchema(TableName.SshCertificateAuthority),
db.ref("friendlyName").as("caName").withSchema(TableName.SshCertificateAuthority),
db.ref("status").as("caStatus").withSchema(TableName.SshCertificateAuthority)
)
.first();
return certTemplate;
} catch (error) {
throw new DatabaseError({ error, name: "Get SSH certificate template by name" });
}
};
return { ...sshCertificateTemplateOrm, getById, getByName };
}; };
@@ -1,7 +1,10 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import {
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";
@@ -58,10 +61,17 @@ export const sshCertificateTemplateServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionSshCertificateTemplateActions.Create,
OrgPermissionSubjects.SshCertificateTemplates OrgPermissionSubjects.SshCertificateTemplates
); );
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, ca.orgId);
if (existingTemplate) {
throw new BadRequestError({
message: `SSH certificate template with name ${name} already exists`
});
}
if (ms(ttl) > ms(maxTTL)) { if (ms(ttl) > ms(maxTTL)) {
throw new BadRequestError({ throw new BadRequestError({
message: "TTL cannot be greater than max TTL" message: "TTL cannot be greater than max TTL"
@@ -114,10 +124,19 @@ export const sshCertificateTemplateServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit, OrgPermissionSshCertificateTemplateActions.Edit,
OrgPermissionSubjects.SshCertificateAuthorities OrgPermissionSubjects.SshCertificateTemplates
); );
if (name) {
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, actorOrgId);
if (existingTemplate) {
throw new BadRequestError({
message: `SSH certificate template with name ${name} already exists`
});
}
}
if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) { if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) {
throw new BadRequestError({ throw new BadRequestError({
message: "TTL cannot be greater than max TTL" message: "TTL cannot be greater than max TTL"
@@ -164,8 +183,8 @@ export const sshCertificateTemplateServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete, OrgPermissionSshCertificateTemplateActions.Delete,
OrgPermissionSubjects.SshCertificateAuthorities OrgPermissionSubjects.SshCertificateTemplates
); );
await sshCertificateTemplateDAL.deleteById(certificateTemplate.id); await sshCertificateTemplateDAL.deleteById(certificateTemplate.id);
@@ -190,8 +209,8 @@ export const sshCertificateTemplateServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionSshCertificateTemplateActions.Read,
OrgPermissionSubjects.SshCertificateAuthorities OrgPermissionSubjects.SshCertificateTemplates
); );
return certTemplate; return certTemplate;
@@ -67,6 +67,32 @@ export const createSshKeyPair = (keyAlgorithm: CertKeyAlgorithm, comment: string
return { publicKey, privateKey }; return { publicKey, privateKey };
}; };
/**
* Return the SSH public key for the given SSH private key.
* @param privateKey - The SSH private key to get the public key for
*/
export const getSshPublicKey = (privateKey: string) => {
const uniqueId = crypto.randomBytes(8).toString("hex");
const privateKeyFile = `ssh_key_${uniqueId}`;
const publicKeyFile = `${privateKeyFile}.pub`;
if (fs.existsSync(publicKeyFile)) fs.unlinkSync(publicKeyFile);
if (fs.existsSync(privateKeyFile)) fs.unlinkSync(privateKeyFile);
fs.writeFileSync(privateKeyFile, privateKey);
fs.chmodSync(privateKeyFile, 0o600);
const command = `ssh-keygen -y -f ${privateKeyFile} > ${publicKeyFile}`;
execSync(command);
const publicKey = fs.readFileSync(publicKeyFile, "utf8");
fs.unlinkSync(privateKeyFile);
fs.unlinkSync(publicKeyFile);
return publicKey;
};
/** /**
* Validate the requested SSH certificate type based on the SSH certificate template configuration. * Validate the requested SSH certificate type based on the SSH certificate template configuration.
* @param template - The SSH certificate template configuration * @param template - The SSH certificate template configuration
@@ -160,9 +186,11 @@ export const createSshCert = ({ caPrivateKey, userPublicKey, keyId, principals,
const uniqueId = crypto.randomBytes(8).toString("hex"); const uniqueId = crypto.randomBytes(8).toString("hex");
const publicKeyFile = `user_key_${uniqueId}.pub`; const publicKeyFile = `user_key_${uniqueId}.pub`;
const privateKeyFile = `ssh_ca_key_${uniqueId}`; const privateKeyFile = `ssh_ca_key_${uniqueId}`;
const signedPublicKeyFile = `user_key_${uniqueId}-cert.pub`;
if (fs.existsSync(publicKeyFile)) fs.unlinkSync(publicKeyFile); if (fs.existsSync(publicKeyFile)) fs.unlinkSync(publicKeyFile);
if (fs.existsSync(privateKeyFile)) fs.unlinkSync(privateKeyFile); if (fs.existsSync(privateKeyFile)) fs.unlinkSync(privateKeyFile);
if (fs.existsSync(signedPublicKeyFile)) fs.unlinkSync(signedPublicKeyFile);
// write public and private keys to temp files // write public and private keys to temp files
fs.writeFileSync(publicKeyFile, userPublicKey); fs.writeFileSync(publicKeyFile, userPublicKey);
@@ -170,7 +198,6 @@ export const createSshCert = ({ caPrivateKey, userPublicKey, keyId, principals,
fs.chmodSync(privateKeyFile, 0o600); fs.chmodSync(privateKeyFile, 0o600);
const serialNumber = createSshCertSerialNumber(); const serialNumber = createSshCertSerialNumber();
console.log("signSshKey serialNumber: ", serialNumber);
const certOptions = [ const certOptions = [
`-s ${privateKeyFile}`, // path to SSH CA private key `-s ${privateKeyFile}`, // path to SSH CA private key
@@ -189,10 +216,11 @@ export const createSshCert = ({ caPrivateKey, userPublicKey, keyId, principals,
// Execute the signing process // Execute the signing process
execSync(command); execSync(command);
const signedPublicKey = fs.readFileSync(publicKeyFile, "utf8"); const signedPublicKey = fs.readFileSync(signedPublicKeyFile, "utf8");
fs.unlinkSync(publicKeyFile); fs.unlinkSync(publicKeyFile);
fs.unlinkSync(privateKeyFile); fs.unlinkSync(privateKeyFile);
fs.unlinkSync(signedPublicKeyFile);
return { serialNumber, signedPublicKey }; return { serialNumber, signedPublicKey };
}; };
@@ -1,6 +1,10 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import {
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";
@@ -12,6 +16,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { import {
createSshCert, createSshCert,
createSshKeyPair, createSshKeyPair,
getSshPublicKey,
validateSshCertificatePrincipals, validateSshCertificatePrincipals,
validateSshCertificateTtl, validateSshCertificateTtl,
validateSshCertificateType validateSshCertificateType
@@ -33,7 +38,7 @@ type TSshCertificateAuthorityServiceFactoryDep = {
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
>; >;
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create" | "findOne">; sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create" | "findOne">;
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find" | "findOne">; sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find" | "getByName">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "getOrgKmsKeyId">; kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "getOrgKmsKeyId">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
@@ -76,14 +81,14 @@ export const sshCertificateAuthorityServiceFactory = ({
const ca = await sshCertificateAuthorityDAL.create( const ca = await sshCertificateAuthorityDAL.create(
{ {
orgId: actorOrgId, orgId: actorOrgId,
friendlyName: friendlyName || "", friendlyName,
status: SshCaStatus.ACTIVE, status: SshCaStatus.ACTIVE,
keyAlgorithm keyAlgorithm
}, },
tx tx
); );
const { privateKey } = createSshKeyPair(keyAlgorithm, ca.friendlyName); const { publicKey, privateKey } = createSshKeyPair(keyAlgorithm, ca.friendlyName);
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId); const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId);
const kmsEncryptor = await kmsService.encryptWithKmsKey({ const kmsEncryptor = await kmsService.encryptWithKmsKey({
@@ -102,7 +107,7 @@ export const sshCertificateAuthorityServiceFactory = ({
tx tx
); );
return ca; return { ...ca, publicKey };
}); });
return newCa; return newCa;
@@ -118,7 +123,7 @@ export const sshCertificateAuthorityServiceFactory = ({
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, ca.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
@@ -128,21 +133,43 @@ export const sshCertificateAuthorityServiceFactory = ({
OrgPermissionSubjects.SshCertificateAuthorities OrgPermissionSubjects.SshCertificateAuthorities
); );
return ca; const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id });
// decrypt secret
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId);
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: orgKmsKeyId
});
const decryptedCaPrivateKey = await kmsDecryptor({
cipherTextBlob: sshCaSecret.encryptedPrivateKey
});
const publicKey = getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
return { ...ca, publicKey };
}; };
/** /**
* Update SSH CA with id [caId] * Update SSH CA with id [caId]
* Note: Used to enable/disable CA * Note: Used to enable/disable CA
*/ */
const updateSshCaById = async ({ caId, status, actor, actorId, actorAuthMethod, actorOrgId }: TUpdateSshCaDTO) => { const updateSshCaById = async ({
caId,
friendlyName,
status,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TUpdateSshCaDTO) => {
const ca = await sshCertificateAuthorityDAL.findById(caId); const ca = await sshCertificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, ca.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
@@ -152,9 +179,23 @@ export const sshCertificateAuthorityServiceFactory = ({
OrgPermissionSubjects.SshCertificateAuthorities OrgPermissionSubjects.SshCertificateAuthorities
); );
const updatedCa = await sshCertificateAuthorityDAL.updateById(caId, { status }); const updatedCa = await sshCertificateAuthorityDAL.updateById(caId, { friendlyName, status });
return updatedCa; const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id });
// decrypt secret
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId);
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: orgKmsKeyId
});
const decryptedCaPrivateKey = await kmsDecryptor({
cipherTextBlob: sshCaSecret.encryptedPrivateKey
});
const publicKey = getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
return { ...updatedCa, publicKey };
}; };
/** /**
@@ -167,7 +208,7 @@ export const sshCertificateAuthorityServiceFactory = ({
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, ca.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
@@ -184,10 +225,10 @@ export const sshCertificateAuthorityServiceFactory = ({
/** /**
* Return SSH certificate and corresponding new SSH public-private key pair where * Return SSH certificate and corresponding new SSH public-private key pair where
* SSH public key is signed using CA behind SSH certificate with name [name]. * SSH public key is signed using CA behind SSH certificate with name [templateName].
*/ */
const issueSshCreds = async ({ const issueSshCreds = async ({
name, templateName,
keyAlgorithm, keyAlgorithm,
certType, certType,
principals, principals,
@@ -198,7 +239,13 @@ export const sshCertificateAuthorityServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TIssueSshCredsDTO) => { }: TIssueSshCredsDTO) => {
// TODO: proper permission check const sshCertificateTemplate = await sshCertificateTemplateDAL.getByName(templateName, actorOrgId);
if (!sshCertificateTemplate) {
throw new NotFoundError({
message: "No SSH certificate template found with specified name"
});
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -208,13 +255,10 @@ export const sshCertificateAuthorityServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionSshCertificateTemplateActions.IssueSshCredentials,
OrgPermissionSubjects.SshCertificateTemplates OrgPermissionSubjects.SshCertificateTemplates
); );
// TODO: adjust to find within org
const sshCertificateTemplate = await sshCertificateTemplateDAL.findOne({ name });
// validate if the requested [certType] is allowed under the template configuration // validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(sshCertificateTemplate, certType); validateSshCertificateType(sshCertificateTemplate, certType);
@@ -266,10 +310,10 @@ export const sshCertificateAuthorityServiceFactory = ({
/** /**
* Return SSH certificate by signing SSH public key [publicKey] * Return SSH certificate by signing SSH public key [publicKey]
* using CA behind SSH certificate template with name [name] * using CA behind SSH certificate template with name [templateName]
*/ */
const signSshKey = async ({ const signSshKey = async ({
name, templateName,
publicKey, publicKey,
certType, certType,
principals, principals,
@@ -280,7 +324,13 @@ export const sshCertificateAuthorityServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TSignSshKeyDTO) => { }: TSignSshKeyDTO) => {
// TODO: proper permission check const sshCertificateTemplate = await sshCertificateTemplateDAL.getByName(templateName, actorOrgId);
if (!sshCertificateTemplate) {
throw new NotFoundError({
message: "No SSH certificate template found with specified name"
});
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -290,13 +340,10 @@ export const sshCertificateAuthorityServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create, OrgPermissionSshCertificateTemplateActions.SignSshKey,
OrgPermissionSubjects.SshCertificateTemplates OrgPermissionSubjects.SshCertificateTemplates
); );
// TODO: adjust to find within org
const sshCertificateTemplate = await sshCertificateTemplateDAL.findOne({ name });
// validate if the requested [certType] is allowed under the template configuration // validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(sshCertificateTemplate, certType); validateSshCertificateType(sshCertificateTemplate, certType);
@@ -354,7 +401,7 @@ export const sshCertificateAuthorityServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionSshCertificateTemplateActions.Read,
OrgPermissionSubjects.SshCertificateTemplates OrgPermissionSubjects.SshCertificateTemplates
); );
@@ -12,7 +12,7 @@ export enum SshCertType {
} }
export type TCreateSshCaDTO = { export type TCreateSshCaDTO = {
friendlyName?: string; friendlyName: string;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
@@ -22,6 +22,7 @@ export type TGetSshCaDTO = {
export type TUpdateSshCaDTO = { export type TUpdateSshCaDTO = {
caId: string; caId: string;
friendlyName?: string;
status?: SshCaStatus; status?: SshCaStatus;
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
@@ -30,7 +31,7 @@ export type TDeleteSshCaDTO = {
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
export type TIssueSshCredsDTO = { export type TIssueSshCredsDTO = {
name: string; // name of SSH certificate template templateName: string;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
certType: SshCertType; certType: SshCertType;
principals: string[]; principals: string[];
@@ -39,7 +40,7 @@ export type TIssueSshCredsDTO = {
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
export type TSignSshKeyDTO = { export type TSignSshKeyDTO = {
name: string; // name of SSH certificate template templateName: string;
publicKey: string; publicKey: string;
certType: SshCertType; certType: SshCertType;
principals: string[]; principals: string[];
+23
View File
@@ -1148,6 +1148,7 @@ export const SSH_CERTIFICATE_AUTHORITIES = {
}, },
UPDATE: { UPDATE: {
sshCaId: "The ID of the SSH CA to update.", sshCaId: "The ID of the SSH CA to update.",
friendlyName: "A friendly name for the SSH CA to update to.",
status: "The status of the SSH CA to update to. This can be one of active or disabled." status: "The status of the SSH CA to update to. This can be one of active or disabled."
}, },
DELETE: { DELETE: {
@@ -1155,6 +1156,28 @@ export const SSH_CERTIFICATE_AUTHORITIES = {
}, },
GET_CERTIFICATE_TEMPLATES: { GET_CERTIFICATE_TEMPLATES: {
sshCaId: "The ID of the SSH CA to get the certificate templates for." sshCaId: "The ID of the SSH CA to get the certificate templates for."
},
SIGN_SSH_KEY: {
templateName: "The name of the SSH certificate template to sign the SSH public key with.",
publicKey: "The SSH public key to sign.",
certType: "The type of certificate to issue. This can be one of user or host.",
principals: "The list of principals (usernames, hostnames) to include in the certificate.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, ... If not specified, the default TTL for the template will be used.",
keyId: "The key ID to include in the certificate. If not specified, a default key ID will be generated.",
serialNumber: "The serial number of the issued SSH certificate.",
signedKey: "The SSH certificate or signed SSH public key."
},
ISSUE_SSH_CREDENTIALS: {
templateName: "The name of the SSH certificate template to issue the SSH credentials with.",
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH CA.",
certType: "The type of certificate to issue. This can be one of user or host.",
principals: "The list of principals (usernames, hostnames) to include in the certificate.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, ... If not specified, the default TTL for the template will be used.",
keyId: "The key ID to include in the certificate. If not specified, a default key ID will be generated.",
serialNumber: "The serial number of the issued SSH certificate.",
signedKey: "The SSH certificate or signed SSH public key.",
privateKey: "The private key corresponding to the issued SSH certificate.",
publicKey: "The public key of the issued SSH certificate."
} }
}; };
@@ -1,3 +1,6 @@
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext"; export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./types"; export type { TOrgPermission } from "./types";
export { OrgPermissionActions, OrgPermissionSubjects } from "./types"; export {
OrgPermissionActions,
OrgPermissionSshCertificateTemplateActions,
OrgPermissionSubjects} from "./types";
@@ -7,6 +7,15 @@ 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 OrgPermissionSubjects { export enum OrgPermissionSubjects {
Workspace = "workspace", Workspace = "workspace",
Role = "role", Role = "role",
@@ -51,6 +60,6 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities] | [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateTemplates]; | [OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects.SshCertificateTemplates];
export type TOrgPermission = MongoAbility<OrgPermissionSet>; export type TOrgPermission = MongoAbility<OrgPermissionSet>;
+1
View File
@@ -4,6 +4,7 @@ export type { TOrgPermission } from "./OrgPermissionContext";
export { export {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionProvider, OrgPermissionProvider,
OrgPermissionSshCertificateTemplateActions,
OrgPermissionSubjects, OrgPermissionSubjects,
useOrgPermission useOrgPermission
} from "./OrgPermissionContext"; } from "./OrgPermissionContext";
@@ -504,7 +504,7 @@ export const useListOrgSshCas = ({ orgId }: { orgId: string }) => {
queryFn: async () => { queryFn: async () => {
const { const {
data: { cas } data: { cas }
} = await apiRequest.get<{ cas: TSshCertificateAuthority[] }>( } = await apiRequest.get<{ cas: Omit<TSshCertificateAuthority, "publicKey">[] }>(
`/api/v1/organization/${orgId}/ssh-cas` `/api/v1/organization/${orgId}/ssh-cas`
); );
return cas; return cas;
+2
View File
@@ -9,6 +9,7 @@ export type TSshCertificateAuthority = {
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
publicKey: string;
}; };
export type TCreateSshCaDTO = { export type TCreateSshCaDTO = {
@@ -18,6 +19,7 @@ export type TCreateSshCaDTO = {
export type TUpdateSshCaDTO = { export type TUpdateSshCaDTO = {
caId: string; caId: string;
friendlyName?: string;
status?: SshCaStatus; status?: SshCaStatus;
}; };
@@ -13,6 +13,17 @@ const generalPermissionSchema = z
}) })
.optional(); .optional();
const sshCertificateTemplatePermissionSchmea = z
.object({
read: z.boolean().optional(),
edit: z.boolean().optional(),
delete: z.boolean().optional(),
create: z.boolean().optional(),
"sign-ssh-key": z.boolean().optional(),
"issue-ssh-credentials": z.boolean().optional()
})
.optional();
const adminConsolePermissionSchmea = z const adminConsolePermissionSchmea = z
.object({ .object({
"access-all-projects": z.boolean().optional() "access-all-projects": z.boolean().optional()
@@ -49,7 +60,9 @@ export const formSchema = z.object({
identity: generalPermissionSchema, identity: generalPermissionSchema,
"organization-admin-console": adminConsolePermissionSchmea, "organization-admin-console": adminConsolePermissionSchmea,
[OrgPermissionSubjects.Kms]: generalPermissionSchema, [OrgPermissionSubjects.Kms]: generalPermissionSchema,
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema [OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
[OrgPermissionSubjects.SshCertificateAuthorities]: generalPermissionSchema,
[OrgPermissionSubjects.SshCertificateTemplates]: sshCertificateTemplatePermissionSchmea
}) })
.optional() .optional()
}); });
@@ -70,12 +70,6 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
const onFormSubmit = async ({ name, description, slug }: FormData) => { const onFormSubmit = async ({ name, description, slug }: FormData) => {
try { try {
console.log("onFormSubmit args: ", {
name,
description,
slug
});
if (!orgId) return; if (!orgId) return;
if (role) { if (role) {
@@ -51,6 +51,15 @@ const PROJECT_TEMPLATES_PERMISSIONS = [
{ action: "delete", label: "Remove" } { action: "delete", label: "Remove" }
] as const; ] as const;
const SSH_CERTIFICATE_TEMPLATES_PERMISSIONS = [
{ 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;
const getPermissionList = (option: string) => { const getPermissionList = (option: string) => {
switch (option) { switch (option) {
case "secret-scanning": case "secret-scanning":
@@ -63,6 +72,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.SshCertificateTemplates:
return SSH_CERTIFICATE_TEMPLATES_PERMISSIONS;
default: default:
return PERMISSIONS; return PERMISSIONS;
} }
@@ -97,7 +108,7 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = getPermissionList(formName).length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number); const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
@@ -69,7 +69,15 @@ const SIMPLE_PERMISSION_OPTIONS = [
title: "External KMS", title: "External KMS",
formName: OrgPermissionSubjects.Kms formName: OrgPermissionSubjects.Kms
}, },
{ title: "Project Templates", formName: OrgPermissionSubjects.ProjectTemplates } { title: "Project Templates", formName: OrgPermissionSubjects.ProjectTemplates },
{
title: "SSH Certificate Authorities",
formName: OrgPermissionSubjects.SshCertificateAuthorities
},
{
title: "SSH Certificate Templates",
formName: OrgPermissionSubjects.SshCertificateTemplates
}
] as const; ] as const;
type Props = { type Props = {
@@ -26,6 +26,7 @@ import { withPermission } from "@app/hoc";
import { useDeleteSshCa, useGetSshCaById } from "@app/hooks/api"; import { useDeleteSshCa, useGetSshCaById } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { SshCaModal } from "../SshPage/components/SshCaModal";
import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components"; import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components";
export const SshCaPage = withPermission( export const SshCaPage = withPermission(
@@ -123,6 +124,7 @@ export const SshCaPage = withPermission(
</div> </div>
</div> </div>
)} )}
<SshCaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteSshCa.isOpen} isOpen={popUp.deleteSshCa.isOpen}
title="Are you sure want to remove the SSH CA from the project?" title="Are you sure want to remove the SSH CA from the project?"
@@ -1,5 +1,6 @@
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faCopy, faDownload,faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import FileSaver from "file-saver";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2"; import { IconButton, Tooltip } from "@app/components/v2";
@@ -19,13 +20,21 @@ export const SshCaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({ const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard" initialState: "Copy ID to clipboard"
}); });
const [downloadText, isDownloading, setDownloadText] = useTimedReset<string>({
initialState: "Save public key"
});
const { data: ca } = useGetSshCaById(caId); const { data: ca } = useGetSshCaById(caId);
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return ca ? ( return ca ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3> <h3 className="text-lg font-semibold text-mineshaft-100">SSH CA Details</h3>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.SshCertificateAuthorities} a={OrgPermissionSubjects.SshCertificateAuthorities}
@@ -82,10 +91,31 @@ export const SshCaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<p className="text-sm font-semibold text-mineshaft-300">Status</p> <p className="text-sm font-semibold text-mineshaft-300">Status</p>
<p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p> <p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p>
</div> </div>
<div> <div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Key Algorithm</p> <p className="text-sm font-semibold text-mineshaft-300">Key Algorithm</p>
<p className="text-sm text-mineshaft-300">{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}</p> <p className="text-sm text-mineshaft-300">{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}</p>
</div> </div>
<div>
<p className="text-sm font-semibold text-mineshaft-300">Public Key</p>
<div className="group flex align-top">
<p className="flex-1 text-sm text-mineshaft-300">{ca.publicKey.substring(0, 20)}...</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={downloadText}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
setDownloadText("Saved");
downloadTxtFile("ssh_ca.pub", ca.publicKey);
}}
>
<FontAwesomeIcon icon={isDownloading ? faCheck : faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
</div> </div>
</div> </div>
) : ( ) : (
@@ -185,7 +185,7 @@ export const SshCertificateTemplateModal = ({ popUp, handlePopUpToggle, sshCaId
errorText={error?.message} errorText={error?.message}
isRequired isRequired
> >
<Input {...field} placeholder="My SSH Certificate Template" /> <Input {...field} placeholder="administrator" />
</FormControl> </FormControl>
)} )}
/> />
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, IconButton } from "@app/components/v2"; import { DeleteActionModal, IconButton } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionSshCertificateTemplateActions,OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteSshCertTemplate } from "@app/hooks/api"; import { useDeleteSshCertTemplate } from "@app/hooks/api";
@@ -50,7 +50,7 @@ export const SshCertificateTemplatesSection = ({ caId }: Props) => {
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3> <h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Create} I={OrgPermissionSshCertificateTemplateActions.Create}
a={OrgPermissionSubjects.SshCertificateTemplates} a={OrgPermissionSubjects.SshCertificateTemplates}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -19,7 +19,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionSshCertificateTemplateActions,OrgPermissionSubjects } from "@app/context";
import { useGetSshCaCertTemplates } from "@app/hooks/api"; import { useGetSshCaCertTemplates } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -66,18 +66,23 @@ export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={() =>
handlePopUpOpen("sshCertificateTemplate", {
id: certificateTemplate.id
})
}
icon={<FontAwesomeIcon icon={faFileAlt} size="sm" className="mr-1" />}
>
Edit Template
</DropdownMenuItem>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Delete} I={OrgPermissionSshCertificateTemplateActions.Edit}
a={OrgPermissionSubjects.SshCertificateTemplates}
>
<DropdownMenuItem
onClick={() =>
handlePopUpOpen("sshCertificateTemplate", {
id: certificateTemplate.id
})
}
icon={<FontAwesomeIcon icon={faFileAlt} size="sm" className="mr-1" />}
>
Edit Template
</DropdownMenuItem>
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionSshCertificateTemplateActions.Delete}
a={OrgPermissionSubjects.SshCertificateTemplates} a={OrgPermissionSubjects.SshCertificateTemplates}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
@@ -55,15 +56,28 @@ export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
} }
}); });
useEffect(() => {
if (ca) {
reset({
friendlyName: ca.friendlyName,
keyAlgorithm: ca.keyAlgorithm
});
} else {
reset({
friendlyName: "",
keyAlgorithm: CertKeyAlgorithm.RSA_2048
});
}
}, [ca]);
const onFormSubmit = async ({ friendlyName, keyAlgorithm }: FormData) => { const onFormSubmit = async ({ friendlyName, keyAlgorithm }: FormData) => {
try { try {
if (ca) { if (ca) {
// update
await updateMutateAsync({ await updateMutateAsync({
caId: ca.id caId: ca.id,
friendlyName
}); });
} else { } else {
// create
await createMutateAsync({ await createMutateAsync({
friendlyName, friendlyName,
keyAlgorithm keyAlgorithm
@@ -112,7 +126,7 @@ export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
errorText={error?.message} errorText={error?.message}
isRequired isRequired
> >
<Input {...field} placeholder="My SSH CA" isDisabled={Boolean(ca)} /> <Input {...field} placeholder="My SSH CA" />
</FormControl> </FormControl>
)} )}
/> />