Merge branch 'heads/main' into daniel/hsm-improvements-prep-work

This commit is contained in:
Daniel Hougaard
2025-10-21 23:19:40 +04:00
427 changed files with 26805 additions and 3823 deletions
@@ -8,6 +8,35 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const getIdentifierType = async (identifier: string): Promise<"template" | "profile" | null> => {
try {
// Try to find as profile first using internal access
await server.services.certificateProfile.getEstConfigurationByProfile({
profileId: identifier,
isInternal: true
});
return "profile";
} catch (profileError) {
try {
await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId: identifier
});
return "template";
} catch (templateError) {
server.log.debug(
{
identifier,
profileError: profileError instanceof Error ? profileError.message : "Unknown error",
templateError: templateError instanceof Error ? templateError.message : "Unknown error"
},
"EST identifier not found as profile or template"
);
return null;
}
}
};
// add support for CSR bodies
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
try {
@@ -59,11 +88,29 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
return;
}
const certificateTemplateId = urlFragments.slice(-2)[0];
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
const identifier = urlFragments.slice(-2)[0];
const identifierType = await getIdentifierType(identifier);
if (!identifierType) {
res.raw.statusCode = 404;
res.raw.setHeader("Content-Type", "text/plain");
res.raw.write("Certificate template or profile not found");
res.raw.flushHeaders();
return;
}
let estConfig;
if (identifierType === "profile") {
estConfig = await server.services.certificateProfile.getEstConfigurationByProfile({
profileId: identifier,
isInternal: true
});
} else {
estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId: identifier
});
}
if (!estConfig.isEnabled) {
throw new BadRequestError({
@@ -95,14 +142,14 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
server.route({
method: "POST",
url: "/:certificateTemplateId/simpleenroll",
url: "/:identifier/simpleenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
identifier: z.string().min(1)
}),
response: {
200: z.string()
@@ -112,9 +159,23 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier);
if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" });
}
if (identifierType === "profile") {
return server.services.certificateEstV3.simpleEnrollByProfile({
csr: req.body,
profileId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
return server.services.certificateEst.simpleEnroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
certificateTemplateId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
@@ -122,14 +183,14 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
server.route({
method: "POST",
url: "/:certificateTemplateId/simplereenroll",
url: "/:identifier/simplereenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
identifier: z.string().min(1)
}),
response: {
200: z.string()
@@ -139,9 +200,23 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier);
if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" });
}
if (identifierType === "profile") {
return server.services.certificateEstV3.simpleReenrollByProfile({
csr: req.body,
profileId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
return server.services.certificateEst.simpleReenroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
certificateTemplateId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
@@ -149,13 +224,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
server.route({
method: "GET",
url: "/:certificateTemplateId/cacerts",
url: "/:identifier/cacerts",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().min(1)
identifier: z.string().min(1)
}),
response: {
200: z.string()
@@ -165,8 +240,20 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier);
if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" });
}
if (identifierType === "profile") {
return server.services.certificateEstV3.getCaCertsByProfile({
profileId: identifier
});
}
return server.services.certificateEst.getCaCerts({
certificateTemplateId: req.params.certificateTemplateId
certificateTemplateId: identifier
});
}
});
+2 -2
View File
@@ -129,7 +129,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
id: z.string(),
value: z.string(),
algorithm: z.string(),
kmipMetadata: z.record(z.any()).optional()
kmipMetadata: z.record(z.any()).nullish()
})
}
},
@@ -435,7 +435,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
key: z.string(),
name: z.string(),
algorithm: z.nativeEnum(SymmetricKeyAlgorithm),
kmipMetadata: z.record(z.any()).optional()
kmipMetadata: z.record(z.any()).nullish()
}),
response: {
200: z.object({
@@ -352,9 +352,18 @@ export enum EventType {
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
LIST_CERTIFICATE_TEMPLATES = "list-certificate-templates",
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
CREATE_CERTIFICATE_PROFILE = "create-certificate-profile",
UPDATE_CERTIFICATE_PROFILE = "update-certificate-profile",
DELETE_CERTIFICATE_PROFILE = "delete-certificate-profile",
GET_CERTIFICATE_PROFILE = "get-certificate-profile",
LIST_CERTIFICATE_PROFILES = "list-certificate-profiles",
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile",
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
@@ -2512,46 +2521,6 @@ interface LoadProjectKmsBackupEvent {
metadata: Record<string, string>; // no metadata yet
}
interface CreateCertificateTemplate {
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface GetCertificateTemplate {
type: EventType.GET_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface UpdateCertificateTemplate {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface DeleteCertificateTemplate {
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface OrgAdminAccessProjectEvent {
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
metadata: {
@@ -2598,6 +2567,138 @@ interface GetCertificateTemplateEstConfig {
};
}
interface CreateCertificateTemplate {
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
metadata:
| {
certificateTemplateId: string;
name: string;
projectId: string;
}
| {
certificateTemplateId: string;
caId: string;
pkiCollectionId: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
projectId: string;
};
}
interface UpdateCertificateTemplate {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
metadata:
| {
certificateTemplateId: string;
name: string;
}
| {
certificateTemplateId: string;
caId: string;
pkiCollectionId: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
projectId: string;
};
}
interface DeleteCertificateTemplate {
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
name: string;
};
}
interface GetCertificateTemplate {
type: EventType.GET_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
name: string;
};
}
interface ListCertificateTemplates {
type: EventType.LIST_CERTIFICATE_TEMPLATES;
metadata: {
projectId: string;
};
}
interface CreateCertificateProfile {
type: EventType.CREATE_CERTIFICATE_PROFILE;
metadata: {
certificateProfileId: string;
name: string;
projectId: string;
enrollmentType: string;
};
}
interface UpdateCertificateProfile {
type: EventType.UPDATE_CERTIFICATE_PROFILE;
metadata: {
certificateProfileId: string;
name: string;
};
}
interface DeleteCertificateProfile {
type: EventType.DELETE_CERTIFICATE_PROFILE;
metadata: {
certificateProfileId: string;
name: string;
};
}
interface GetCertificateProfile {
type: EventType.GET_CERTIFICATE_PROFILE;
metadata: {
certificateProfileId: string;
name: string;
};
}
interface ListCertificateProfiles {
type: EventType.LIST_CERTIFICATE_PROFILES;
metadata: {
projectId: string;
};
}
interface IssueCertificateFromProfile {
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE;
metadata: {
certificateProfileId: string;
certificateId: string;
commonName: string;
profileName: string;
};
}
interface SignCertificateFromProfile {
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE;
metadata: {
certificateProfileId: string;
certificateId: string;
profileName: string;
commonName: string;
};
}
interface OrderCertificateFromProfile {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE;
metadata: {
certificateProfileId: string;
orderId: string;
profileName: string;
};
}
interface AttemptCreateSlackIntegration {
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
metadata: {
@@ -4051,13 +4152,22 @@ export type Event =
| LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent
| OrgAdminBypassSSOEvent
| CreateCertificateTemplate
| UpdateCertificateTemplate
| GetCertificateTemplate
| DeleteCertificateTemplate
| CreateCertificateTemplateEstConfig
| UpdateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig
| CreateCertificateTemplate
| UpdateCertificateTemplate
| DeleteCertificateTemplate
| GetCertificateTemplate
| ListCertificateTemplates
| CreateCertificateProfile
| UpdateCertificateProfile
| DeleteCertificateProfile
| GetCertificateProfile
| ListCertificateProfiles
| IssueCertificateFromProfile
| SignCertificateFromProfile
| OrderCertificateFromProfile
| GetAzureAdCsTemplatesEvent
| AttemptCreateSlackIntegration
| AttemptReinstallSlackIntegration
+1 -1
View File
@@ -78,7 +78,7 @@ export type TKmipRegisterDTO = {
name: string;
key: string;
algorithm: SymmetricKeyAlgorithm;
kmipMetadata?: Record<string, unknown>;
kmipMetadata?: Record<string, unknown> | null;
} & KmipOperationBaseDTO;
export type TSetupOrgKmipDTO = {
@@ -33,7 +33,8 @@ export const getDefaultOnPremFeatures = () => {
enterpriseSecretSyncs: false,
enterpriseCertificateSyncs: false,
enterpriseAppConnections: true,
machineIdentityAuthTemplates: false
machineIdentityAuthTemplates: false,
pkiLegacyTemplates: false
};
};
@@ -67,6 +67,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
fips: false,
eventSubscriptions: false,
machineIdentityAuthTemplates: false,
pkiLegacyTemplates: false,
pam: false
});
@@ -218,6 +218,20 @@ export const licenseServiceFactory = ({
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId);
currentPlan.identitiesUsed = identityUsed;
if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) {
try {
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
quantity: membersUsed,
quantityIdentities: identityUsed
});
} catch (error) {
logger.error(
error,
`Update seats used: encountered an error when updating plan for customer [customerId=${org.customerId}]`
);
}
}
await keyStore.setItemWithExpiry(
FEATURE_CACHE_KEY(org.id),
LICENSE_SERVER_CLOUD_PLAN_TTL,
@@ -78,6 +78,7 @@ export type TFeatureSet = {
enterpriseCertificateSyncs: false;
enterpriseAppConnections: false;
machineIdentityAuthTemplates: false;
pkiLegacyTemplates: false;
fips: false;
eventSubscriptions: false;
pam: false;
@@ -5,6 +5,7 @@ import {
ProjectPermissionAppConnectionActions,
ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionCmekActions,
ProjectPermissionCommitsActions,
ProjectPermissionDynamicSecretActions,
@@ -72,8 +73,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionPkiTemplateActions.Create,
ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionPkiTemplateActions.IssueCert,
ProjectPermissionPkiTemplateActions.ListCerts
ProjectPermissionPkiTemplateActions.IssueCert, // deprecated
ProjectPermissionPkiTemplateActions.ListCerts // deprecated
],
ProjectPermissionSub.CertificateTemplates
);
@@ -99,6 +100,17 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.Certificates
);
can(
[
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionCertificateProfileActions.Edit,
ProjectPermissionCertificateProfileActions.Create,
ProjectPermissionCertificateProfileActions.Delete,
ProjectPermissionCertificateProfileActions.IssueCert
],
ProjectPermissionSub.CertificateProfiles
);
can(
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
ProjectPermissionSub.Commits
@@ -443,6 +455,7 @@ const buildMemberPermissionRules = () => {
// double check if all CRUD are needed for CA and Certificates
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates);
can(
[
@@ -454,7 +467,15 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.Certificates
);
can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates);
can(
[
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionCertificateProfileActions.Edit,
ProjectPermissionCertificateProfileActions.Create,
ProjectPermissionCertificateProfileActions.Delete
],
ProjectPermissionSub.CertificateProfiles
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
@@ -111,6 +111,14 @@ export enum ProjectPermissionPkiSubscriberActions {
ListCerts = "list-certs"
}
export enum ProjectPermissionCertificateProfileActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueCert = "issue-cert"
}
export enum ProjectPermissionSecretSyncActions {
Read = "read",
Create = "create",
@@ -249,7 +257,8 @@ export enum ProjectPermissionSub {
PamFolders = "pam-folders",
PamResources = "pam-resources",
PamAccounts = "pam-accounts",
PamSessions = "pam-sessions"
PamSessions = "pam-sessions",
CertificateProfiles = "certificate-profiles"
}
export type SecretSubjectFields = {
@@ -438,7 +447,8 @@ export type ProjectPermissionSet =
ProjectPermissionPamAccountActions,
ProjectPermissionSub.PamAccounts | (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields)
]
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions];
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]
| [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles];
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
@@ -1109,6 +1119,13 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(ProjectPermissionSub.CertificateProfiles).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateProfileActions).describe(
"Describe what action an entity can take."
)
}),
...GeneralPermissionSchema
]);