From 1b5cfaa49b177b25c7c2fa1226321e8ec1e6b4c9 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 9 Feb 2026 18:04:18 -0800 Subject: [PATCH] Add pricing matrix --- server/lib/billing/tierMatrix.ts | 36 +++++++ server/lib/blueprints/proxyResources.ts | 7 +- server/lib/calculateUserClientsForOrgs.ts | 4 +- .../private/middlewares/verifySubscription.ts | 10 +- server/private/routers/external.ts | 32 +++---- server/routers/client/getClient.ts | 4 +- server/routers/newt/getNewtToken.ts | 2 +- server/routers/org/updateOrg.ts | 94 +++++++++++++++---- 8 files changed, 148 insertions(+), 41 deletions(-) create mode 100644 server/lib/billing/tierMatrix.ts diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts new file mode 100644 index 00000000..3e5eb0f6 --- /dev/null +++ b/server/lib/billing/tierMatrix.ts @@ -0,0 +1,36 @@ +export enum TierFeature { + OrgOidc = "orgOidc", + CustomAuthenticationDomain = "customAuthenticationDomain", + DeviceApprovals = "deviceApprovals", + LoginPageBranding = "loginPageBranding", + LogExport = "logExport", + AccessLogs = "accessLogs", + ActionLogs = "actionLogs", + RotateCredentials = "rotateCredentials", + MaintencePage = "maintencePage", + DevicePosture = "devicePosture", + TwoFactorEnforcement = "twoFactorEnforcement", + SessionDurationPolicies = "sessionDurationPolicies", + PasswordExpirationPolicies = "passwordExpirationPolicies" +} + +export const tierMatrix: Record = { + [TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"], + [TierFeature.CustomAuthenticationDomain]: [ + "tier1", + "tier2", + "tier3", + "enterprise" + ], + [TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"], + [TierFeature.LoginPageBranding]: ["tier1", "tier3", "enterprise"], + [TierFeature.LogExport]: ["tier3", "enterprise"], + [TierFeature.AccessLogs]: ["tier2", "tier3", "enterprise"], + [TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"], + [TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"], + [TierFeature.MaintencePage]: ["tier1", "tier2", "tier3", "enterprise"], + [TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"], + [TierFeature.TwoFactorEnforcement]: ["tier1", "tier2", "tier3", "enterprise"], + [TierFeature.SessionDurationPolicies]: ["tier1", "tier2", "tier3", "enterprise"], + [TierFeature.PasswordExpirationPolicies]: ["tier1", "tier2", "tier3", "enterprise"] +}; diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index c0faad63..93ddfdfb 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -32,7 +32,8 @@ import { resourcePassword } from "@server/db"; import { hashPassword } from "@server/auth/password"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; -import { build } from "@server/build"; +import { tierMatrix } from "../billing/tierMatrix"; +import { t } from "@faker-js/faker/dist/airline-DF6RqYmq"; export type ProxyResourcesResults = { proxyResource: Resource; @@ -212,7 +213,7 @@ export async function updateProxyResources( } else { // Update existing resource - const isLicensed = await isLicensedOrSubscribed(orgId); + const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.maintencePage); if (!isLicensed) { resourceData.maintenance = undefined; } @@ -648,7 +649,7 @@ export async function updateProxyResources( ); } - const isLicensed = await isLicensedOrSubscribed(orgId); + const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.maintencePage); if (!isLicensed) { resourceData.maintenance = undefined; } diff --git a/server/lib/calculateUserClientsForOrgs.ts b/server/lib/calculateUserClientsForOrgs.ts index 15837890..4be76ddd 100644 --- a/server/lib/calculateUserClientsForOrgs.ts +++ b/server/lib/calculateUserClientsForOrgs.ts @@ -20,6 +20,7 @@ import { sendTerminateClient } from "@server/routers/client/terminate"; import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm"; import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations"; import { OlmErrorCodes } from "@server/routers/olm/error"; +import { tierMatrix } from "./billing/tierMatrix"; export async function calculateUserClientsForOrgs( userId: string, @@ -189,7 +190,8 @@ export async function calculateUserClientsForOrgs( const niceId = await getUniqueClientName(orgId); const isOrgLicensed = await isLicensedOrSubscribed( - userOrg.orgId + userOrg.orgId, + tierMatrix.deviceApprovals ); const requireApproval = build !== "oss" && diff --git a/server/private/middlewares/verifySubscription.ts b/server/private/middlewares/verifySubscription.ts index 3ab351a1..0c28f7aa 100644 --- a/server/private/middlewares/verifySubscription.ts +++ b/server/private/middlewares/verifySubscription.ts @@ -45,7 +45,7 @@ export function verifyValidSubscription(tiers: string[]) { const { tier, active } = await getOrgTierData(orgId); const isTier = tiers.includes(tier || ""); - if (!isTier || !active) { + if (!active) { return next( createHttpError( HttpCode.FORBIDDEN, @@ -53,6 +53,14 @@ export function verifyValidSubscription(tiers: string[]) { ) ); } + if (!isTier) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Organization subscription tier does not have access to this feature" + ) + ); + } return next(); } catch (e) { diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 37048c34..a2ffae05 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -52,6 +52,7 @@ import { authenticated as a, authRouter as aa } from "@server/routers/external"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; export const authenticated = a; export const unauthenticated = ua; @@ -76,7 +77,7 @@ unauthenticated.post( authenticated.put( "/org/:orgId/idp/oidc", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.orgOidc), verifyOrgAccess, verifyUserHasAction(ActionsEnum.createIdp), logActionAudit(ActionsEnum.createIdp), @@ -86,7 +87,7 @@ authenticated.put( authenticated.post( "/org/:orgId/idp/:idpId/oidc", verifyValidLicense, - verifyValidSubscription(), + verifyValidSubscription(tierMatrix.orgOidc), verifyOrgAccess, verifyIdpAccess, verifyUserHasAction(ActionsEnum.updateIdp), @@ -279,7 +280,7 @@ authenticated.delete( authenticated.put( "/org/:orgId/login-page", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.customAuthenticationDomain), verifyOrgAccess, verifyUserHasAction(ActionsEnum.createLoginPage), logActionAudit(ActionsEnum.createLoginPage), @@ -289,7 +290,7 @@ authenticated.put( authenticated.post( "/org/:orgId/login-page/:loginPageId", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.customAuthenticationDomain), verifyOrgAccess, verifyLoginPageAccess, verifyUserHasAction(ActionsEnum.updateLoginPage), @@ -318,7 +319,7 @@ authenticated.get( authenticated.get( "/org/:orgId/approvals", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.deviceApprovals), verifyOrgAccess, verifyUserHasAction(ActionsEnum.listApprovals), logActionAudit(ActionsEnum.listApprovals), @@ -335,7 +336,7 @@ authenticated.get( authenticated.put( "/org/:orgId/approvals/:approvalId", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.deviceApprovals), verifyOrgAccess, verifyUserHasAction(ActionsEnum.updateApprovals), logActionAudit(ActionsEnum.updateApprovals), @@ -345,7 +346,7 @@ authenticated.put( authenticated.get( "/org/:orgId/login-page-branding", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.loginPageBranding), verifyOrgAccess, verifyUserHasAction(ActionsEnum.getLoginPage), logActionAudit(ActionsEnum.getLoginPage), @@ -355,7 +356,7 @@ authenticated.get( authenticated.put( "/org/:orgId/login-page-branding", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.loginPageBranding), verifyOrgAccess, verifyUserHasAction(ActionsEnum.updateLoginPage), logActionAudit(ActionsEnum.updateLoginPage), @@ -365,7 +366,6 @@ authenticated.put( authenticated.delete( "/org/:orgId/login-page-branding", verifyValidLicense, - verifyValidSubscription, verifyOrgAccess, verifyUserHasAction(ActionsEnum.deleteLoginPage), logActionAudit(ActionsEnum.deleteLoginPage), @@ -433,7 +433,7 @@ authenticated.post( authenticated.get( "/org/:orgId/logs/action", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.actionLogs), verifyOrgAccess, verifyUserHasAction(ActionsEnum.exportLogs), logs.queryActionAuditLogs @@ -442,7 +442,7 @@ authenticated.get( authenticated.get( "/org/:orgId/logs/action/export", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.logExport), verifyOrgAccess, verifyUserHasAction(ActionsEnum.exportLogs), logActionAudit(ActionsEnum.exportLogs), @@ -452,7 +452,7 @@ authenticated.get( authenticated.get( "/org/:orgId/logs/access", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.accessLogs), verifyOrgAccess, verifyUserHasAction(ActionsEnum.exportLogs), logs.queryAccessAuditLogs @@ -461,7 +461,7 @@ authenticated.get( authenticated.get( "/org/:orgId/logs/access/export", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.logExport), verifyOrgAccess, verifyUserHasAction(ActionsEnum.exportLogs), logActionAudit(ActionsEnum.exportLogs), @@ -472,7 +472,7 @@ authenticated.post( "/re-key/:clientId/regenerate-client-secret", verifyClientAccess, // this is first to set the org id verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.rotateCredentials), verifyUserHasAction(ActionsEnum.reGenerateSecret), reKey.reGenerateClientSecret ); @@ -481,7 +481,7 @@ authenticated.post( "/re-key/:siteId/regenerate-site-secret", verifySiteAccess, // this is first to set the org id verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.rotateCredentials), verifyUserHasAction(ActionsEnum.reGenerateSecret), reKey.reGenerateSiteSecret ); @@ -489,7 +489,7 @@ authenticated.post( authenticated.put( "/re-key/:orgId/regenerate-remote-exit-node-secret", verifyValidLicense, - verifyValidSubscription, + verifyValidSubscription(tierMatrix.rotateCredentials), verifyOrgAccess, verifyUserHasAction(ActionsEnum.reGenerateSecret), reKey.reGenerateExitNodeSecret diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index b8b5594e..bb2ff8fd 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -13,6 +13,7 @@ import { OpenAPITags, registry } from "@server/openApi"; import { getUserDeviceName } from "@server/db/names"; import { build } from "@server/build"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; const getClientSchema = z.strictObject({ clientId: z @@ -327,7 +328,8 @@ export async function getClient( client.currentFingerprint ); const isOrgLicensed = await isLicensedOrSubscribed( - client.clients.orgId + client.clients.orgId, + tierMatrix.devicePosture ); const postureData: PostureData | null = rawPosture ? isOrgLicensed diff --git a/server/routers/newt/getNewtToken.ts b/server/routers/newt/getNewtToken.ts index 63797358..8f7e01d2 100644 --- a/server/routers/newt/getNewtToken.ts +++ b/server/routers/newt/getNewtToken.ts @@ -18,7 +18,7 @@ import config from "@server/lib/config"; import { APP_VERSION } from "@server/lib/consts"; export const newtGetTokenBodySchema = z.object({ - newtId: z.string(), + // newtId: z.string(), secret: z.string(), token: z.string().optional() }); diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 4762c32f..707691f5 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -12,7 +12,8 @@ import { OpenAPITags, registry } from "@server/openApi"; import { build } from "@server/build"; import { cache } from "@server/lib/cache"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; -import { isSubscribed } from "#dynamic/lib/isSubscribed"; +import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; +import { getOrgTierData } from "#dynamic/lib/billing"; const updateOrgParamsSchema = z.strictObject({ orgId: z.string() @@ -87,26 +88,83 @@ export async function updateOrg( const { orgId } = parsedParams.data; - const isLicensed = await isLicensedOrSubscribed(orgId); - if (!isLicensed) { + // Check 2FA enforcement feature + const has2FAFeature = await isLicensedOrSubscribed( + orgId, + tierMatrix[TierFeature.TwoFactorEnforcement] + ); + if (!has2FAFeature) { parsedBody.data.requireTwoFactor = undefined; - parsedBody.data.maxSessionLengthHours = undefined; - parsedBody.data.passwordExpiryDays = undefined; } - const subscribed = await isSubscribed(orgId); - if ( - build == "saas" && - subscribed && - parsedBody.data.settingsLogRetentionDaysRequest && - parsedBody.data.settingsLogRetentionDaysRequest > 30 - ) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "You are not allowed to set log retention days greater than 30 with your current subscription" - ) - ); + // Check session duration policies feature + const hasSessionDurationFeature = await isLicensedOrSubscribed( + orgId, + tierMatrix[TierFeature.SessionDurationPolicies] + ); + if (!hasSessionDurationFeature) { + parsedBody.data.maxSessionLengthHours = undefined; + } + + // Check password expiration policies feature + const hasPasswordExpirationFeature = await isLicensedOrSubscribed( + orgId, + tierMatrix[TierFeature.PasswordExpirationPolicies] + ); + if (!hasPasswordExpirationFeature) { + parsedBody.data.passwordExpiryDays = undefined; + } + if (build == "saas") { + const { tier } = await getOrgTierData(orgId); + + // Determine max allowed retention days based on tier + let maxRetentionDays: number | null = null; + if (!tier) { + maxRetentionDays = 0; + } else if (tier === "tier1") { + maxRetentionDays = 7; + } else if (tier === "tier2") { + maxRetentionDays = 30; + } else if (tier === "tier3") { + maxRetentionDays = 90; + } + // For enterprise tier, no check (maxRetentionDays remains null) + + if (maxRetentionDays !== null) { + if ( + parsedBody.data.settingsLogRetentionDaysRequest !== undefined && + parsedBody.data.settingsLogRetentionDaysRequest > maxRetentionDays + ) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + `You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription` + ) + ); + } + if ( + parsedBody.data.settingsLogRetentionDaysAccess !== undefined && + parsedBody.data.settingsLogRetentionDaysAccess > maxRetentionDays + ) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + `You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription` + ) + ); + } + if ( + parsedBody.data.settingsLogRetentionDaysAction !== undefined && + parsedBody.data.settingsLogRetentionDaysAction > maxRetentionDays + ) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + `You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription` + ) + ); + } + } } const updatedOrg = await db