mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 11:27:32 +00:00
Add permissioning to SSH, add publicKey return for SSH CA, polish
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-8
@@ -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[];
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
+12
-1
@@ -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;
|
||||||
|
|||||||
+9
-1
@@ -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,6 +66,10 @@ export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionSshCertificateTemplateActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.SshCertificateTemplates}
|
||||||
|
>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handlePopUpOpen("sshCertificateTemplate", {
|
handlePopUpOpen("sshCertificateTemplate", {
|
||||||
@@ -76,8 +80,9 @@ export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props
|
|||||||
>
|
>
|
||||||
Edit Template
|
Edit Template
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
</OrgPermissionCan>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Delete}
|
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>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user