mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-21 20:19:07 +02:00
Merge branch 'mtls' into dev
This commit is contained in:
@@ -848,6 +848,28 @@ authenticated.delete(
|
||||
resource.deleteResourceRule
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/mtls/certificate",
|
||||
verifyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createResourceMtlsCertificate),
|
||||
logActionAudit(ActionsEnum.createResourceMtlsCertificate),
|
||||
resource.createResourceMtlsCertificate
|
||||
);
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/mtls/certificates",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.listResourceMtlsCertificates),
|
||||
resource.listResourceMtlsCertificates
|
||||
);
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId/mtls/certificate/:mtlsCertificateId",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteResourceMtlsCertificate),
|
||||
logActionAudit(ActionsEnum.deleteResourceMtlsCertificate),
|
||||
resource.deleteResourceMtlsCertificate
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/target/:targetId",
|
||||
verifyTargetAccess,
|
||||
|
||||
@@ -738,6 +738,39 @@ authenticated.delete(
|
||||
resource.deleteResourceRule
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
[
|
||||
"/resource/:resourceId/mtls/certificate",
|
||||
"/public-resource/:resourceId/mtls/certificate"
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResourceMtlsCertificate),
|
||||
logActionAudit(ActionsEnum.createResourceMtlsCertificate),
|
||||
resource.createResourceMtlsCertificate
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
[
|
||||
"/resource/:resourceId/mtls/certificates",
|
||||
"/public-resource/:resourceId/mtls/certificates"
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listResourceMtlsCertificates),
|
||||
resource.listResourceMtlsCertificates
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
[
|
||||
"/resource/:resourceId/mtls/certificate/:mtlsCertificateId",
|
||||
"/public-resource/:resourceId/mtls/certificate/:mtlsCertificateId"
|
||||
],
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteResourceMtlsCertificate),
|
||||
logActionAudit(ActionsEnum.deleteResourceMtlsCertificate),
|
||||
resource.deleteResourceMtlsCertificate
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/target/:targetId",
|
||||
verifyApiKeyTargetAccess,
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, resourceMtlsCertificates, resources } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import {
|
||||
MAX_MTLS_CERTIFICATES_PER_RESOURCE,
|
||||
parseCaCertificates
|
||||
} from "@server/lib/mtlsCertificate";
|
||||
|
||||
const createResourceMtlsCertificateParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const createResourceMtlsCertificateBodySchema = z.strictObject({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
certificate: z.string().min(1).max(100_000)
|
||||
});
|
||||
|
||||
export type CreateResourceMtlsCertificateResponse = {
|
||||
certificates: {
|
||||
mtlsCertificateId: number;
|
||||
resourceId: number;
|
||||
name: string | null;
|
||||
subject: string | null;
|
||||
issuer: string | null;
|
||||
serialNumber: string | null;
|
||||
fingerprint: string | null;
|
||||
notBefore: number | null;
|
||||
notAfter: number | null;
|
||||
createdAt: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
const description =
|
||||
"Add a trusted CA certificate that clients must present a certificate signed by when mTLS is enabled on a resource. Accepts a PEM bundle; each certificate in the bundle is stored separately.";
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/resource/{resourceId}/mtls/certificate",
|
||||
description,
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: createResourceMtlsCertificateParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createResourceMtlsCertificateBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/public-resource/{resourceId}/mtls/certificate",
|
||||
description,
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: createResourceMtlsCertificateParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createResourceMtlsCertificateBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createResourceMtlsCertificate(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams =
|
||||
createResourceMtlsCertificateParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = createResourceMtlsCertificateBodySchema.safeParse(
|
||||
req.body
|
||||
);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
const { name, certificate } = parsedBody.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (resource.mode !== "http") {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"mTLS is only supported on HTTP resources"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(
|
||||
resource.orgId,
|
||||
tierMatrix.mtls
|
||||
);
|
||||
if (!isLicensed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"mTLS is not supported on your current plan. Please upgrade to access this feature."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let parsedCerts;
|
||||
try {
|
||||
parsedCerts = parseCaCertificates(certificate);
|
||||
} catch (e) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
e instanceof Error ? e.message : "Invalid certificate"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const existing = await db
|
||||
.select({ fingerprint: resourceMtlsCertificates.fingerprint })
|
||||
.from(resourceMtlsCertificates)
|
||||
.where(eq(resourceMtlsCertificates.resourceId, resourceId));
|
||||
|
||||
const existingFingerprints = new Set(existing.map((c) => c.fingerprint));
|
||||
const newCerts = parsedCerts.filter(
|
||||
(c) => !existingFingerprints.has(c.fingerprint)
|
||||
);
|
||||
|
||||
if (newCerts.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
parsedCerts.length === 1
|
||||
? "This CA certificate has already been added to this resource"
|
||||
: "All certificates in this bundle have already been added to this resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
existing.length + newCerts.length >
|
||||
MAX_MTLS_CERTIFICATES_PER_RESOURCE
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`A resource can have at most ${MAX_MTLS_CERTIFICATES_PER_RESOURCE} trusted CA certificates`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const inserted = await db
|
||||
.insert(resourceMtlsCertificates)
|
||||
.values(
|
||||
newCerts.map((c) => ({
|
||||
resourceId,
|
||||
// a label only makes sense for a single certificate
|
||||
name: newCerts.length === 1 ? (name ?? null) : null,
|
||||
certificate: c.certificate,
|
||||
subject: c.subject,
|
||||
issuer: c.issuer,
|
||||
serialNumber: c.serialNumber,
|
||||
fingerprint: c.fingerprint,
|
||||
notBefore: c.notBefore,
|
||||
notAfter: c.notAfter,
|
||||
createdAt: now
|
||||
}))
|
||||
)
|
||||
.returning({
|
||||
mtlsCertificateId: resourceMtlsCertificates.mtlsCertificateId,
|
||||
resourceId: resourceMtlsCertificates.resourceId,
|
||||
name: resourceMtlsCertificates.name,
|
||||
subject: resourceMtlsCertificates.subject,
|
||||
issuer: resourceMtlsCertificates.issuer,
|
||||
serialNumber: resourceMtlsCertificates.serialNumber,
|
||||
fingerprint: resourceMtlsCertificates.fingerprint,
|
||||
notBefore: resourceMtlsCertificates.notBefore,
|
||||
notAfter: resourceMtlsCertificates.notAfter,
|
||||
createdAt: resourceMtlsCertificates.createdAt
|
||||
});
|
||||
|
||||
return response<CreateResourceMtlsCertificateResponse>(res, {
|
||||
data: { certificates: inserted },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "CA certificate added successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, resourceMtlsCertificates, resources } from "@server/db";
|
||||
import { and, count, eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const deleteResourceMtlsCertificateParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive(),
|
||||
mtlsCertificateId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const description =
|
||||
"Remove a trusted CA certificate from a resource. If it is the last one, mTLS is disabled on the resource since it can no longer verify any client.";
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/resource/{resourceId}/mtls/certificate/{mtlsCertificateId}",
|
||||
description,
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: deleteResourceMtlsCertificateParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/public-resource/{resourceId}/mtls/certificate/{mtlsCertificateId}",
|
||||
description,
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: deleteResourceMtlsCertificateParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export type DeleteResourceMtlsCertificateResponse = {
|
||||
// true when removing the last CA certificate forced mTLS off
|
||||
mtlsDisabled: boolean;
|
||||
};
|
||||
|
||||
export async function deleteResourceMtlsCertificate(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams =
|
||||
deleteResourceMtlsCertificateParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
const { resourceId, mtlsCertificateId } = parsedParams.data;
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(resourceMtlsCertificates)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
resourceMtlsCertificates.mtlsCertificateId,
|
||||
mtlsCertificateId
|
||||
),
|
||||
eq(resourceMtlsCertificates.resourceId, resourceId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`CA certificate with ID ${mtlsCertificateId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let mtlsDisabled = false;
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(resourceMtlsCertificates)
|
||||
.where(
|
||||
eq(
|
||||
resourceMtlsCertificates.mtlsCertificateId,
|
||||
mtlsCertificateId
|
||||
)
|
||||
);
|
||||
|
||||
const [{ remaining }] = await trx
|
||||
.select({ remaining: count() })
|
||||
.from(resourceMtlsCertificates)
|
||||
.where(eq(resourceMtlsCertificates.resourceId, resourceId));
|
||||
|
||||
// With no CA left there is nothing to verify clients against, so
|
||||
// leaving mTLS "enabled" would either lock everyone out or be
|
||||
// silently ignored by the Traefik config builder.
|
||||
if (remaining === 0) {
|
||||
const updated = await trx
|
||||
.update(resources)
|
||||
.set({ mtlsEnabled: false })
|
||||
.where(
|
||||
and(
|
||||
eq(resources.resourceId, resourceId),
|
||||
eq(resources.mtlsEnabled, true)
|
||||
)
|
||||
)
|
||||
.returning({ resourceId: resources.resourceId });
|
||||
mtlsDisabled = updated.length > 0;
|
||||
}
|
||||
});
|
||||
|
||||
return response<DeleteResourceMtlsCertificateResponse>(res, {
|
||||
data: { mtlsDisabled },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "CA certificate removed successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,9 @@ export * from "./createResourceRule";
|
||||
export * from "./deleteResourceRule";
|
||||
export * from "./listResourceRules";
|
||||
export * from "./updateResourceRule";
|
||||
export * from "./createResourceMtlsCertificate";
|
||||
export * from "./deleteResourceMtlsCertificate";
|
||||
export * from "./listResourceMtlsCertificates";
|
||||
export * from "./getUserResources";
|
||||
export * from "./listUserResourceAliases";
|
||||
export * from "./setResourceHeaderAuth";
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, resourceMtlsCertificates, resources } from "@server/db";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const listResourceMtlsCertificatesParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
function queryResourceMtlsCertificates(resourceId: number) {
|
||||
return db
|
||||
.select({
|
||||
mtlsCertificateId: resourceMtlsCertificates.mtlsCertificateId,
|
||||
resourceId: resourceMtlsCertificates.resourceId,
|
||||
name: resourceMtlsCertificates.name,
|
||||
subject: resourceMtlsCertificates.subject,
|
||||
issuer: resourceMtlsCertificates.issuer,
|
||||
serialNumber: resourceMtlsCertificates.serialNumber,
|
||||
fingerprint: resourceMtlsCertificates.fingerprint,
|
||||
notBefore: resourceMtlsCertificates.notBefore,
|
||||
notAfter: resourceMtlsCertificates.notAfter,
|
||||
createdAt: resourceMtlsCertificates.createdAt
|
||||
})
|
||||
.from(resourceMtlsCertificates)
|
||||
.where(eq(resourceMtlsCertificates.resourceId, resourceId))
|
||||
.orderBy(asc(resourceMtlsCertificates.mtlsCertificateId));
|
||||
}
|
||||
|
||||
export type ListResourceMtlsCertificatesResponse = {
|
||||
certificates: Awaited<ReturnType<typeof queryResourceMtlsCertificates>>;
|
||||
};
|
||||
|
||||
const description = "List the trusted CA certificates for a resource's mTLS.";
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/mtls/certificates",
|
||||
description,
|
||||
tags: [OpenAPITags.PublicResourceLegacy],
|
||||
request: {
|
||||
params: listResourceMtlsCertificatesParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/public-resource/{resourceId}/mtls/certificates",
|
||||
description,
|
||||
tags: [OpenAPITags.PublicResource],
|
||||
request: {
|
||||
params: listResourceMtlsCertificatesParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listResourceMtlsCertificates(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams =
|
||||
listResourceMtlsCertificatesParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select({ resourceId: resources.resourceId })
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const certificates = await queryResourceMtlsCertificates(resourceId);
|
||||
|
||||
return response<ListResourceMtlsCertificatesResponse>(res, {
|
||||
data: { certificates },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "CA certificates retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
loginPage,
|
||||
resourceHeaderAuth,
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
resourceMtlsCertificates,
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
resourceRules,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
resourcePolicies,
|
||||
resources
|
||||
} from "@server/db";
|
||||
import { eq, and, ne } from "drizzle-orm";
|
||||
import { eq, and, ne, count } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -117,6 +118,12 @@ const updateHttpResourceBodySchema = z
|
||||
maintenanceTitle: z.string().max(255).nullable().optional(),
|
||||
maintenanceMessage: z.string().max(2000).nullable().optional(),
|
||||
maintenanceEstimatedTime: z.string().max(100).nullable().optional(),
|
||||
mtlsEnabled: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"Require clients to present a certificate signed by one of the resource's trusted CA certificates. Requires SSL and at least one CA certificate."
|
||||
),
|
||||
postAuthPath: z.string().nullable().optional(),
|
||||
// SSH settings
|
||||
pamMode: z.enum(["passthrough", "push"]).optional(),
|
||||
@@ -508,8 +515,58 @@ async function updateHttpResource(
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveSsl = updateData.ssl ?? resource.ssl;
|
||||
const effectiveMtlsEnabled = updateData.mtlsEnabled ?? resource.mtlsEnabled;
|
||||
|
||||
if (updateData.mtlsEnabled === true) {
|
||||
if (resource.mode !== "http") {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"mTLS is only supported on HTTP resources."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const isMtlsLicensed = await isLicensedOrSubscribed(
|
||||
resource.orgId,
|
||||
tierMatrix.mtls
|
||||
);
|
||||
if (!isMtlsLicensed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"mTLS is not supported on your current plan. Please upgrade to access this feature."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [{ certificateCount }] = await db
|
||||
.select({ certificateCount: count() })
|
||||
.from(resourceMtlsCertificates)
|
||||
.where(eq(resourceMtlsCertificates.resourceId, resource.resourceId));
|
||||
|
||||
if (certificateCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Add at least one trusted CA certificate before enabling mTLS."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveMtlsEnabled && !effectiveSsl) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"mTLS requires SSL to be enabled for this resource. Disable mTLS before turning SSL off."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// catch when the resource policy changes or gets cleared
|
||||
if (updateData.resourcePolicyId !== undefined &&
|
||||
if (updateData.resourcePolicyId !== undefined &&
|
||||
resource.resourcePolicyId !== updateData.resourcePolicyId) {
|
||||
await clearResourceSpecificSettings(
|
||||
resource.resourceId,
|
||||
|
||||
Reference in New Issue
Block a user