mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-18 06:58:36 +00:00
PKI revamp - backend initial changes
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
import {
|
||||
EnrollmentType,
|
||||
TCertificateProfileCertificate,
|
||||
TCertificateProfileInsert,
|
||||
TCertificateProfileMetrics,
|
||||
TCertificateProfileUpdate
|
||||
} from "./certificate-profile-types";
|
||||
|
||||
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
|
||||
|
||||
export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
const certificateProfileOrm = ormify(db, TableName.CertificateProfile);
|
||||
|
||||
const create = async (data: TCertificateProfileInsert, tx?: Knex) => {
|
||||
try {
|
||||
const [certificateProfile] = await (tx || db)(TableName.CertificateProfile).insert(data).returning("*");
|
||||
return certificateProfile;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Create certificate profile" });
|
||||
}
|
||||
};
|
||||
|
||||
const updateById = async (id: string, data: TCertificateProfileUpdate, tx?: Knex) => {
|
||||
try {
|
||||
const [certificateProfile] = await (tx || db)(TableName.CertificateProfile)
|
||||
.where({ id })
|
||||
.update(data)
|
||||
.returning("*");
|
||||
return certificateProfile;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update certificate profile" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const [certificateProfile] = await (tx || db)(TableName.CertificateProfile).where({ id }).del().returning("*");
|
||||
return certificateProfile;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Delete certificate profile" });
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const certificateProfile = await (tx || db)(TableName.CertificateProfile).where({ id }).first();
|
||||
return certificateProfile;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find certificate profile by id" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByIdWithConfigs = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db)(TableName.CertificateProfile)
|
||||
.select(
|
||||
selectAllTableCols(TableName.CertificateProfile),
|
||||
(tx || db).ref("id").withSchema(TableName.CertificateAuthority).as("caId"),
|
||||
(tx || db).ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"),
|
||||
(tx || db).ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"),
|
||||
(tx || db).ref("name").withSchema(TableName.CertificateAuthority).as("caName"),
|
||||
(tx || db).ref("id").withSchema(TableName.CertificateTemplateV2).as("templateId"),
|
||||
(tx || db).ref("projectId").withSchema(TableName.CertificateTemplateV2).as("templateProjectId"),
|
||||
(tx || db).ref("name").withSchema(TableName.CertificateTemplateV2).as("templateName"),
|
||||
(tx || db).ref("description").withSchema(TableName.CertificateTemplateV2).as("templateDescription"),
|
||||
(tx || db).ref("id").withSchema(TableName.EstEnrollmentConfig).as("estConfigId"),
|
||||
(tx || db)
|
||||
.ref("disableBootstrapCaValidation")
|
||||
.withSchema(TableName.EstEnrollmentConfig)
|
||||
.as("estConfigDisableBootstrapCaValidation"),
|
||||
(tx || db).ref("hashedPassphrase").withSchema(TableName.EstEnrollmentConfig).as("estConfigHashedPassphrase"),
|
||||
(tx || db).ref("encryptedCaChain").withSchema(TableName.EstEnrollmentConfig).as("estConfigEncryptedCaChain"),
|
||||
(tx || db).ref("id").withSchema(TableName.ApiEnrollmentConfig).as("apiConfigId"),
|
||||
(tx || db).ref("autoRenew").withSchema(TableName.ApiEnrollmentConfig).as("apiConfigAutoRenew"),
|
||||
(tx || db).ref("autoRenewDays").withSchema(TableName.ApiEnrollmentConfig).as("apiConfigAutoRenewDays")
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.CertificateAuthority,
|
||||
`${TableName.CertificateProfile}.caId`,
|
||||
`${TableName.CertificateAuthority}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.CertificateTemplateV2,
|
||||
`${TableName.CertificateProfile}.certificateTemplateId`,
|
||||
`${TableName.CertificateTemplateV2}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.EstEnrollmentConfig,
|
||||
`${TableName.CertificateProfile}.estConfigId`,
|
||||
`${TableName.EstEnrollmentConfig}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.ApiEnrollmentConfig,
|
||||
`${TableName.CertificateProfile}.apiConfigId`,
|
||||
`${TableName.ApiEnrollmentConfig}.id`
|
||||
)
|
||||
.where(`${TableName.CertificateProfile}.id`, id)
|
||||
.first();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find certificate profile by id with configs" });
|
||||
}
|
||||
};
|
||||
|
||||
const findBySlugAndProjectId = async (slug: string, projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const certificateProfile = await (tx || db)(TableName.CertificateProfile).where({ slug, projectId }).first();
|
||||
return certificateProfile;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find certificate profile by slug and project id" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectId = async (
|
||||
projectId: string,
|
||||
options: {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
enrollmentType?: EnrollmentType;
|
||||
caId?: string;
|
||||
} = {},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
|
||||
|
||||
let query = (tx || db)(TableName.CertificateProfile).where(
|
||||
`${TableName.CertificateProfile}.projectId`,
|
||||
projectId
|
||||
);
|
||||
|
||||
if (search) {
|
||||
query = query.where((builder) => {
|
||||
void builder
|
||||
.whereILike(`${TableName.CertificateProfile}.name`, `%${search}%`)
|
||||
.orWhereILike(`${TableName.CertificateProfile}.description`, `%${search}%`)
|
||||
.orWhereILike(`${TableName.CertificateProfile}.slug`, `%${search}%`);
|
||||
});
|
||||
}
|
||||
|
||||
if (enrollmentType) {
|
||||
query = query.where(`${TableName.CertificateProfile}.enrollmentType`, enrollmentType);
|
||||
}
|
||||
|
||||
if (caId) {
|
||||
query = query.where(`${TableName.CertificateProfile}.caId`, caId);
|
||||
}
|
||||
|
||||
const certificateProfiles = await query
|
||||
.select(selectAllTableCols(TableName.CertificateProfile))
|
||||
.orderBy(`${TableName.CertificateProfile}.createdAt`, "desc")
|
||||
.offset(offset)
|
||||
.limit(limit);
|
||||
|
||||
return certificateProfiles;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find certificate profiles by project id" });
|
||||
}
|
||||
};
|
||||
|
||||
const countByProjectId = async (
|
||||
projectId: string,
|
||||
options: {
|
||||
search?: string;
|
||||
enrollmentType?: EnrollmentType;
|
||||
caId?: string;
|
||||
} = {},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const { search, enrollmentType, caId } = options;
|
||||
|
||||
let query = (tx || db)(TableName.CertificateProfile).where({ projectId });
|
||||
|
||||
if (search) {
|
||||
query = query.where((builder) => {
|
||||
void builder
|
||||
.whereILike("name", `%${search}%`)
|
||||
.orWhereILike("description", `%${search}%`)
|
||||
.orWhereILike("slug", `%${search}%`);
|
||||
});
|
||||
}
|
||||
|
||||
if (enrollmentType) {
|
||||
query = query.where({ enrollmentType });
|
||||
}
|
||||
|
||||
if (caId) {
|
||||
query = query.where({ caId });
|
||||
}
|
||||
|
||||
const result = await query.count("*").first();
|
||||
return parseInt((result as unknown as { count: string }).count || "0", 10);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Count certificate profiles by project id" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const certificateProfile = await (tx || db)(TableName.CertificateProfile).where({ name, projectId }).first();
|
||||
return certificateProfile;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find certificate profile by name and project id" });
|
||||
}
|
||||
};
|
||||
|
||||
const getCertificatesByProfile = async (
|
||||
profileId: string,
|
||||
options: {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
status?: "active" | "expired" | "revoked";
|
||||
search?: string;
|
||||
} = {},
|
||||
tx?: Knex
|
||||
): Promise<TCertificateProfileCertificate[]> => {
|
||||
try {
|
||||
const { offset = 0, limit = 20, status, search } = options;
|
||||
const now = new Date();
|
||||
|
||||
let query = (tx || db)(TableName.Certificate).where("profileId", profileId);
|
||||
|
||||
if (search) {
|
||||
query = query.where((builder) => {
|
||||
void builder.whereILike("cn", `%${search}%`).orWhereILike("serialNumber", `%${search}%`);
|
||||
});
|
||||
}
|
||||
|
||||
if (status) {
|
||||
switch (status) {
|
||||
case "active":
|
||||
query = query.where("notAfter", ">", now).where("isRevoked", false);
|
||||
break;
|
||||
case "expired":
|
||||
query = query.where("notAfter", "<=", now).where("isRevoked", false);
|
||||
break;
|
||||
case "revoked":
|
||||
query = query.where("isRevoked", true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const certificates = await query
|
||||
.select((tx || db).ref("id").withSchema(TableName.Certificate))
|
||||
.select((tx || db).ref("serialNumber").withSchema(TableName.Certificate))
|
||||
.select((tx || db).ref("cn").withSchema(TableName.Certificate))
|
||||
.select((tx || db).ref("status").withSchema(TableName.Certificate))
|
||||
.select((tx || db).ref("notBefore").withSchema(TableName.Certificate))
|
||||
.select((tx || db).ref("notAfter").withSchema(TableName.Certificate))
|
||||
.select((tx || db).ref("isRevoked").withSchema(TableName.Certificate))
|
||||
.select((tx || db).ref("createdAt").withSchema(TableName.Certificate))
|
||||
.orderBy("createdAt", "desc")
|
||||
.offset(offset)
|
||||
.limit(limit);
|
||||
|
||||
return certificates;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Get certificates by profile" });
|
||||
}
|
||||
};
|
||||
|
||||
const getProfileMetrics = async (
|
||||
profileId: string,
|
||||
expiringDays: number = 30,
|
||||
tx?: Knex
|
||||
): Promise<TCertificateProfileMetrics> => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const expiringDate = new Date();
|
||||
expiringDate.setDate(now.getDate() + expiringDays);
|
||||
|
||||
const metrics = await (tx || db)(TableName.Certificate)
|
||||
.where("profileId", profileId)
|
||||
.select(
|
||||
db.raw("COUNT(*) as total_certificates"),
|
||||
db.raw("COUNT(CASE WHEN NOT is_revoked AND not_after > ? THEN 1 END) as active_certificates", [now]),
|
||||
db.raw("COUNT(CASE WHEN NOT is_revoked AND not_after <= ? THEN 1 END) as expired_certificates", [now]),
|
||||
db.raw(
|
||||
"COUNT(CASE WHEN NOT is_revoked AND not_after > ? AND not_after <= ? THEN 1 END) as expiring_certificates",
|
||||
[now, expiringDate]
|
||||
),
|
||||
db.raw("COUNT(CASE WHEN is_revoked THEN 1 END) as revoked_certificates")
|
||||
)
|
||||
.first();
|
||||
|
||||
return {
|
||||
profileId,
|
||||
totalCertificates: parseInt(String((metrics as Record<string, unknown>)?.total_certificates || 0), 10),
|
||||
activeCertificates: parseInt(String((metrics as Record<string, unknown>)?.active_certificates || 0), 10),
|
||||
expiredCertificates: parseInt(String((metrics as Record<string, unknown>)?.expired_certificates || 0), 10),
|
||||
expiringCertificates: parseInt(String((metrics as Record<string, unknown>)?.expiring_certificates || 0), 10),
|
||||
revokedCertificates: parseInt(String((metrics as Record<string, unknown>)?.revoked_certificates || 0), 10)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Get certificate profile metrics" });
|
||||
}
|
||||
};
|
||||
|
||||
const isProfileInUse = async (profileId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
|
||||
|
||||
return parseInt((doc as unknown as { count: string }).count || "0", 10);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Check if certificate profile is in use" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...certificateProfileOrm,
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
findById,
|
||||
findByIdWithConfigs,
|
||||
findBySlugAndProjectId,
|
||||
findByProjectId,
|
||||
countByProjectId,
|
||||
findByNameAndProjectId,
|
||||
getCertificatesByProfile,
|
||||
getProfileMetrics,
|
||||
isProfileInUse
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EnrollmentType } from "./certificate-profile-types";
|
||||
|
||||
export const createCertificateProfileSchema = z
|
||||
.object({
|
||||
projectId: z.string().min(1),
|
||||
caId: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid(),
|
||||
name: z.string().min(1).max(255),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
||||
description: z.string().max(1000).optional(),
|
||||
enrollmentType: z.nativeEnum(EnrollmentType),
|
||||
estConfig: z
|
||||
.object({
|
||||
disableBootstrapCaValidation: z.boolean().default(false),
|
||||
passphrase: z.string().min(1),
|
||||
encryptedCaChain: z.string()
|
||||
})
|
||||
.optional(),
|
||||
apiConfig: z
|
||||
.object({
|
||||
autoRenew: z.boolean().default(false),
|
||||
autoRenewDays: z.number().min(1).max(365).optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
|
||||
return false;
|
||||
}
|
||||
if (data.enrollmentType === EnrollmentType.API && !data.apiConfig) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Config must be provided based on enrollment type"
|
||||
}
|
||||
);
|
||||
|
||||
export const updateCertificateProfileSchema = z.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens")
|
||||
.optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||
estConfig: z
|
||||
.object({
|
||||
disableBootstrapCaValidation: z.boolean().default(false),
|
||||
passphrase: z.string().min(1),
|
||||
encryptedCaChain: z.string()
|
||||
})
|
||||
.optional(),
|
||||
apiConfig: z
|
||||
.object({
|
||||
autoRenew: z.boolean().default(false),
|
||||
autoRenewDays: z.number().min(1).max(365).optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
export const getCertificateProfileByIdSchema = z.object({
|
||||
id: z.string().uuid()
|
||||
});
|
||||
|
||||
export const getCertificateProfileBySlugSchema = z.object({
|
||||
projectId: z.string().min(1),
|
||||
slug: z.string().min(1)
|
||||
});
|
||||
|
||||
export const listCertificateProfilesSchema = z.object({
|
||||
projectId: z.string().min(1),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
search: z.string().optional(),
|
||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||
caId: z.string().uuid().optional()
|
||||
});
|
||||
|
||||
export const deleteCertificateProfileSchema = z.object({
|
||||
id: z.string().uuid()
|
||||
});
|
||||
|
||||
export const listCertificatesByProfileSchema = z.object({
|
||||
profileId: z.string().uuid(),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
status: z.enum(["active", "expired", "revoked"]).optional(),
|
||||
search: z.string().optional()
|
||||
});
|
||||
|
||||
export const getCertificateProfileMetricsSchema = z.object({
|
||||
profileId: z.string().uuid(),
|
||||
expiringDays: z.coerce.number().min(1).max(365).default(30)
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,622 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import {
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
||||
import { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
||||
import { TApiConfigData, TEstConfigData } from "../enrollment-config/enrollment-config-types";
|
||||
import { TEstEnrollmentConfigDALFactory } from "../enrollment-config/est-enrollment-config-dal";
|
||||
import { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
||||
import {
|
||||
EnrollmentType,
|
||||
TCertificateProfile,
|
||||
TCertificateProfileCertificate,
|
||||
TCertificateProfileInsert,
|
||||
TCertificateProfileMetrics,
|
||||
TCertificateProfileUpdate,
|
||||
TCertificateProfileWithConfigs
|
||||
} from "./certificate-profile-types";
|
||||
|
||||
export type TCertificateProfileCreateData = Omit<TCertificateProfileInsert, "estConfigId" | "apiConfigId"> & {
|
||||
estConfig?: TEstConfigData;
|
||||
apiConfig?: TApiConfigData;
|
||||
};
|
||||
|
||||
type TCertificateProfileServiceFactoryDep = {
|
||||
certificateProfileDAL: TCertificateProfileDALFactory;
|
||||
certificateTemplateV2DAL: TCertificateTemplateV2DALFactory;
|
||||
apiEnrollmentConfigDAL: TApiEnrollmentConfigDALFactory;
|
||||
estEnrollmentConfigDAL: TEstEnrollmentConfigDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TCertificateProfileServiceFactory = ReturnType<typeof certificateProfileServiceFactory>;
|
||||
|
||||
const convertDalToService = (dalResult: Record<string, unknown>): TCertificateProfile => {
|
||||
return {
|
||||
...dalResult,
|
||||
enrollmentType: dalResult.enrollmentType as EnrollmentType
|
||||
} as TCertificateProfile;
|
||||
};
|
||||
|
||||
const convertDalArrayToService = (dalResults: Record<string, unknown>[]): TCertificateProfile[] => {
|
||||
return dalResults.map(convertDalToService);
|
||||
};
|
||||
|
||||
const validateEnrollmentConfig = async (data: {
|
||||
enrollmentType: EnrollmentType;
|
||||
estConfig?: TEstConfigData | null;
|
||||
apiConfig?: TApiConfigData | null;
|
||||
}): Promise<void> => {
|
||||
if (data.enrollmentType === EnrollmentType.EST) {
|
||||
if (!data.estConfig) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "EST enrollment type requires EST configuration"
|
||||
});
|
||||
}
|
||||
if (data.apiConfig) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "EST enrollment type cannot have API configuration"
|
||||
});
|
||||
}
|
||||
} else if (data.enrollmentType === EnrollmentType.API) {
|
||||
if (!data.apiConfig) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "API enrollment type requires API configuration"
|
||||
});
|
||||
}
|
||||
if (data.estConfig) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "API enrollment type cannot have EST configuration"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateEnrollmentConfigForUpdate = async (data: {
|
||||
enrollmentType: EnrollmentType;
|
||||
estConfigId?: string | null;
|
||||
apiConfigId?: string | null;
|
||||
}): Promise<void> => {
|
||||
if (data.enrollmentType === EnrollmentType.EST) {
|
||||
if (!data.estConfigId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "EST enrollment type requires EST configuration ID"
|
||||
});
|
||||
}
|
||||
if (data.apiConfigId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "EST enrollment type cannot have API configuration ID"
|
||||
});
|
||||
}
|
||||
} else if (data.enrollmentType === EnrollmentType.API) {
|
||||
if (!data.apiConfigId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "API enrollment type requires API configuration ID"
|
||||
});
|
||||
}
|
||||
if (data.estConfigId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "API enrollment type cannot have EST configuration ID"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const hasEnrollmentConfigChanges = (data: TCertificateProfileUpdate): boolean => {
|
||||
return !!(data.enrollmentType || data.estConfigId || data.apiConfigId);
|
||||
};
|
||||
|
||||
export const certificateProfileServiceFactory = ({
|
||||
certificateProfileDAL,
|
||||
certificateTemplateV2DAL,
|
||||
apiEnrollmentConfigDAL,
|
||||
estEnrollmentConfigDAL,
|
||||
permissionService
|
||||
}: TCertificateProfileServiceFactoryDep) => {
|
||||
const createProfile = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
data
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
data: Omit<TCertificateProfileCreateData, "projectId">;
|
||||
}): Promise<TCertificateProfile> => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Create,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
// Validate that certificate template exists and belongs to the same project
|
||||
if (data.certificateTemplateId) {
|
||||
const template = await certificateTemplateV2DAL.findById(data.certificateTemplateId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found" });
|
||||
}
|
||||
if (template.projectId !== projectId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Certificate template must belong to the same project"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for slug uniqueness within project
|
||||
const existingSlugProfile = await certificateProfileDAL.findBySlugAndProjectId(data.slug, projectId);
|
||||
if (existingSlugProfile) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Certificate profile with this slug already exists in project"
|
||||
});
|
||||
}
|
||||
|
||||
// Validate enrollment type configuration
|
||||
await validateEnrollmentConfig({
|
||||
enrollmentType: data.enrollmentType,
|
||||
estConfig: data.estConfig,
|
||||
apiConfig: data.apiConfig
|
||||
});
|
||||
|
||||
// Create enrollment configs based on type
|
||||
let estConfigId: string | null = null;
|
||||
let apiConfigId: string | null = null;
|
||||
|
||||
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
|
||||
const appCfg = getConfig();
|
||||
// Hash the passphrase
|
||||
const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphrase, appCfg.SALT_ROUNDS);
|
||||
|
||||
const estConfig = await estEnrollmentConfigDAL.create({
|
||||
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation,
|
||||
hashedPassphrase,
|
||||
encryptedCaChain: Buffer.from(data.estConfig.encryptedCaChain, "base64")
|
||||
});
|
||||
estConfigId = estConfig.id;
|
||||
} else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) {
|
||||
const apiConfig = await apiEnrollmentConfigDAL.create({
|
||||
autoRenew: data.apiConfig.autoRenew,
|
||||
autoRenewDays: data.apiConfig.autoRenewDays
|
||||
});
|
||||
apiConfigId = apiConfig.id;
|
||||
}
|
||||
|
||||
// Create the profile with the created config IDs
|
||||
const { estConfig, apiConfig, ...profileData } = data;
|
||||
const profile = await certificateProfileDAL.create({
|
||||
...profileData,
|
||||
projectId,
|
||||
estConfigId,
|
||||
apiConfigId
|
||||
});
|
||||
|
||||
return convertDalToService(profile);
|
||||
};
|
||||
|
||||
const updateProfile = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
data
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
data: TCertificateProfileUpdate;
|
||||
}): Promise<TCertificateProfile> => {
|
||||
const existingProfile = await certificateProfileDAL.findById(profileId);
|
||||
if (!existingProfile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: existingProfile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Edit,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
if (data.certificateTemplateId) {
|
||||
const template = await certificateTemplateV2DAL.findById(data.certificateTemplateId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found" });
|
||||
}
|
||||
if (template.projectId !== existingProfile.projectId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Certificate template must belong to the same project"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.slug && data.slug !== existingProfile.slug) {
|
||||
const conflictingProfile = await certificateProfileDAL.findBySlugAndProjectId(
|
||||
data.slug,
|
||||
existingProfile.projectId
|
||||
);
|
||||
if (conflictingProfile && conflictingProfile.id !== profileId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Certificate profile with this slug already exists in project"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (hasEnrollmentConfigChanges(data)) {
|
||||
const mergedData = { ...existingProfile, ...data };
|
||||
await validateEnrollmentConfigForUpdate({
|
||||
enrollmentType: mergedData.enrollmentType as EnrollmentType,
|
||||
estConfigId: mergedData.estConfigId,
|
||||
apiConfigId: mergedData.apiConfigId
|
||||
});
|
||||
}
|
||||
|
||||
const updatedProfile = await certificateProfileDAL.updateById(profileId, data);
|
||||
return convertDalToService(updatedProfile);
|
||||
};
|
||||
|
||||
const getProfileById = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
}): Promise<TCertificateProfile> => {
|
||||
const profile = await certificateProfileDAL.findById(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
return convertDalToService(profile);
|
||||
};
|
||||
|
||||
const getProfileByIdWithConfigs = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
}): Promise<TCertificateProfileWithConfigs> => {
|
||||
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
return {
|
||||
...profile,
|
||||
enrollmentType: profile.enrollmentType as EnrollmentType
|
||||
};
|
||||
};
|
||||
|
||||
const getProfileBySlug = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
slug
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
slug: string;
|
||||
}): Promise<TCertificateProfile> => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
const profile = await certificateProfileDAL.findBySlugAndProjectId(slug, projectId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
return convertDalToService(profile);
|
||||
};
|
||||
|
||||
const listProfiles = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
search,
|
||||
enrollmentType,
|
||||
caId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
enrollmentType?: EnrollmentType;
|
||||
caId?: string;
|
||||
}): Promise<{
|
||||
profiles: TCertificateProfile[];
|
||||
totalCount: number;
|
||||
}> => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
const profiles = await certificateProfileDAL.findByProjectId(projectId, {
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
enrollmentType,
|
||||
caId
|
||||
});
|
||||
|
||||
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
||||
search,
|
||||
enrollmentType,
|
||||
caId
|
||||
});
|
||||
|
||||
return {
|
||||
profiles: convertDalArrayToService(profiles),
|
||||
totalCount
|
||||
};
|
||||
};
|
||||
|
||||
const deleteProfile = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
}): Promise<TCertificateProfile> => {
|
||||
const profile = await certificateProfileDAL.findById(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Delete,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
// Check if profile is in use by any certificates
|
||||
const isInUse = await certificateProfileDAL.isProfileInUse(profileId);
|
||||
if (isInUse) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Cannot delete certificate profile that has issued certificates"
|
||||
});
|
||||
}
|
||||
|
||||
const deletedProfile = await certificateProfileDAL.deleteById(profileId);
|
||||
if (!deletedProfile) {
|
||||
throw new NotFoundError({ message: "Failed to delete certificate profile" });
|
||||
}
|
||||
return convertDalToService(deletedProfile);
|
||||
};
|
||||
|
||||
const getProfileCertificates = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
status,
|
||||
search
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
status?: "active" | "expired" | "revoked";
|
||||
search?: string;
|
||||
}): Promise<TCertificateProfileCertificate[]> => {
|
||||
const profile = await certificateProfileDAL.findById(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
const certificates = await certificateProfileDAL.getCertificatesByProfile(profileId, {
|
||||
offset,
|
||||
limit,
|
||||
status,
|
||||
search
|
||||
});
|
||||
|
||||
return certificates;
|
||||
};
|
||||
|
||||
const getProfileMetrics = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
expiringDays = 30
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
expiringDays?: number;
|
||||
}): Promise<TCertificateProfileMetrics> => {
|
||||
const profile = await certificateProfileDAL.findById(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
|
||||
return metrics;
|
||||
};
|
||||
|
||||
const getEstConfigurationByProfile = async ({ profileId }: { profileId: string }) => {
|
||||
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
if (profile.enrollmentType !== EnrollmentType.EST) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Profile is not configured for EST enrollment"
|
||||
});
|
||||
}
|
||||
|
||||
if (!profile.estConfigEncryptedCaChain) {
|
||||
throw new NotFoundError({ message: "EST configuration not found for this profile" });
|
||||
}
|
||||
|
||||
return {
|
||||
orgId: profile.projectId,
|
||||
isEnabled: true,
|
||||
caChain: profile.estConfigEncryptedCaChain.toString("base64"),
|
||||
disableBootstrapCertValidation: profile.estConfigDisableBootstrapCaValidation,
|
||||
hashedPassphrase: profile.estConfigHashedPassphrase
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createProfile,
|
||||
updateProfile,
|
||||
getProfileById,
|
||||
getProfileByIdWithConfigs,
|
||||
getProfileBySlug,
|
||||
listProfiles,
|
||||
deleteProfile,
|
||||
getProfileCertificates,
|
||||
getProfileMetrics,
|
||||
getEstConfigurationByProfile
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
TCertificateProfiles,
|
||||
TCertificateProfilesInsert,
|
||||
TCertificateProfilesUpdate
|
||||
} from "@app/db/schemas/certificate-profiles";
|
||||
|
||||
export enum EnrollmentType {
|
||||
API = "api",
|
||||
EST = "est"
|
||||
}
|
||||
|
||||
export type TCertificateProfile = Omit<TCertificateProfiles, "enrollmentType"> & {
|
||||
enrollmentType: EnrollmentType;
|
||||
};
|
||||
|
||||
export type TCertificateProfileInsert = Omit<TCertificateProfilesInsert, "enrollmentType"> & {
|
||||
enrollmentType: EnrollmentType;
|
||||
};
|
||||
|
||||
export type TCertificateProfileUpdate = Omit<TCertificateProfilesUpdate, "enrollmentType"> & {
|
||||
enrollmentType?: EnrollmentType;
|
||||
};
|
||||
|
||||
export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
||||
certificateAuthority?: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
status: string;
|
||||
name: string;
|
||||
};
|
||||
certificateTemplate?: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
estConfig?: {
|
||||
id: string;
|
||||
disableBootstrapCaValidation: boolean;
|
||||
hashedPassphrase: string;
|
||||
encryptedCaChain: Buffer;
|
||||
};
|
||||
apiConfig?: {
|
||||
id: string;
|
||||
autoRenew: boolean;
|
||||
autoRenewDays?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export interface TCertificateProfileMetrics {
|
||||
profileId: string;
|
||||
totalCertificates: number;
|
||||
activeCertificates: number;
|
||||
expiredCertificates: number;
|
||||
expiringCertificates: number;
|
||||
revokedCertificates: number;
|
||||
}
|
||||
|
||||
export interface TCertificateProfileCertificate {
|
||||
id: string;
|
||||
serialNumber: string;
|
||||
cn: string;
|
||||
status: string;
|
||||
notBefore: Date;
|
||||
notAfter: Date;
|
||||
isRevoked: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user