Add pricing matrix

This commit is contained in:
Owen
2026-02-09 18:04:18 -08:00
parent 66f3fabbae
commit 1b5cfaa49b
8 changed files with 148 additions and 41 deletions

View File

@@ -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, string[]> = {
[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"]
};

View File

@@ -32,7 +32,8 @@ import { resourcePassword } from "@server/db";
import { hashPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; 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 = { export type ProxyResourcesResults = {
proxyResource: Resource; proxyResource: Resource;
@@ -212,7 +213,7 @@ export async function updateProxyResources(
} else { } else {
// Update existing resource // Update existing resource
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.maintencePage);
if (!isLicensed) { if (!isLicensed) {
resourceData.maintenance = undefined; 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) { if (!isLicensed) {
resourceData.maintenance = undefined; resourceData.maintenance = undefined;
} }

View File

@@ -20,6 +20,7 @@ import { sendTerminateClient } from "@server/routers/client/terminate";
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm"; import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm";
import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations"; import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations";
import { OlmErrorCodes } from "@server/routers/olm/error"; import { OlmErrorCodes } from "@server/routers/olm/error";
import { tierMatrix } from "./billing/tierMatrix";
export async function calculateUserClientsForOrgs( export async function calculateUserClientsForOrgs(
userId: string, userId: string,
@@ -189,7 +190,8 @@ export async function calculateUserClientsForOrgs(
const niceId = await getUniqueClientName(orgId); const niceId = await getUniqueClientName(orgId);
const isOrgLicensed = await isLicensedOrSubscribed( const isOrgLicensed = await isLicensedOrSubscribed(
userOrg.orgId userOrg.orgId,
tierMatrix.deviceApprovals
); );
const requireApproval = const requireApproval =
build !== "oss" && build !== "oss" &&

View File

@@ -45,7 +45,7 @@ export function verifyValidSubscription(tiers: string[]) {
const { tier, active } = await getOrgTierData(orgId); const { tier, active } = await getOrgTierData(orgId);
const isTier = tiers.includes(tier || ""); const isTier = tiers.includes(tier || "");
if (!isTier || !active) { if (!active) {
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, 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(); return next();
} catch (e) { } catch (e) {

View File

@@ -52,6 +52,7 @@ import {
authenticated as a, authenticated as a,
authRouter as aa authRouter as aa
} from "@server/routers/external"; } from "@server/routers/external";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export const authenticated = a; export const authenticated = a;
export const unauthenticated = ua; export const unauthenticated = ua;
@@ -76,7 +77,7 @@ unauthenticated.post(
authenticated.put( authenticated.put(
"/org/:orgId/idp/oidc", "/org/:orgId/idp/oidc",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.orgOidc),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createIdp), verifyUserHasAction(ActionsEnum.createIdp),
logActionAudit(ActionsEnum.createIdp), logActionAudit(ActionsEnum.createIdp),
@@ -86,7 +87,7 @@ authenticated.put(
authenticated.post( authenticated.post(
"/org/:orgId/idp/:idpId/oidc", "/org/:orgId/idp/:idpId/oidc",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(), verifyValidSubscription(tierMatrix.orgOidc),
verifyOrgAccess, verifyOrgAccess,
verifyIdpAccess, verifyIdpAccess,
verifyUserHasAction(ActionsEnum.updateIdp), verifyUserHasAction(ActionsEnum.updateIdp),
@@ -279,7 +280,7 @@ authenticated.delete(
authenticated.put( authenticated.put(
"/org/:orgId/login-page", "/org/:orgId/login-page",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.customAuthenticationDomain),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createLoginPage), verifyUserHasAction(ActionsEnum.createLoginPage),
logActionAudit(ActionsEnum.createLoginPage), logActionAudit(ActionsEnum.createLoginPage),
@@ -289,7 +290,7 @@ authenticated.put(
authenticated.post( authenticated.post(
"/org/:orgId/login-page/:loginPageId", "/org/:orgId/login-page/:loginPageId",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.customAuthenticationDomain),
verifyOrgAccess, verifyOrgAccess,
verifyLoginPageAccess, verifyLoginPageAccess,
verifyUserHasAction(ActionsEnum.updateLoginPage), verifyUserHasAction(ActionsEnum.updateLoginPage),
@@ -318,7 +319,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/approvals", "/org/:orgId/approvals",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.deviceApprovals),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listApprovals), verifyUserHasAction(ActionsEnum.listApprovals),
logActionAudit(ActionsEnum.listApprovals), logActionAudit(ActionsEnum.listApprovals),
@@ -335,7 +336,7 @@ authenticated.get(
authenticated.put( authenticated.put(
"/org/:orgId/approvals/:approvalId", "/org/:orgId/approvals/:approvalId",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.deviceApprovals),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.updateApprovals), verifyUserHasAction(ActionsEnum.updateApprovals),
logActionAudit(ActionsEnum.updateApprovals), logActionAudit(ActionsEnum.updateApprovals),
@@ -345,7 +346,7 @@ authenticated.put(
authenticated.get( authenticated.get(
"/org/:orgId/login-page-branding", "/org/:orgId/login-page-branding",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.loginPageBranding),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.getLoginPage), verifyUserHasAction(ActionsEnum.getLoginPage),
logActionAudit(ActionsEnum.getLoginPage), logActionAudit(ActionsEnum.getLoginPage),
@@ -355,7 +356,7 @@ authenticated.get(
authenticated.put( authenticated.put(
"/org/:orgId/login-page-branding", "/org/:orgId/login-page-branding",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.loginPageBranding),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.updateLoginPage), verifyUserHasAction(ActionsEnum.updateLoginPage),
logActionAudit(ActionsEnum.updateLoginPage), logActionAudit(ActionsEnum.updateLoginPage),
@@ -365,7 +366,6 @@ authenticated.put(
authenticated.delete( authenticated.delete(
"/org/:orgId/login-page-branding", "/org/:orgId/login-page-branding",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription,
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.deleteLoginPage), verifyUserHasAction(ActionsEnum.deleteLoginPage),
logActionAudit(ActionsEnum.deleteLoginPage), logActionAudit(ActionsEnum.deleteLoginPage),
@@ -433,7 +433,7 @@ authenticated.post(
authenticated.get( authenticated.get(
"/org/:orgId/logs/action", "/org/:orgId/logs/action",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.actionLogs),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs), verifyUserHasAction(ActionsEnum.exportLogs),
logs.queryActionAuditLogs logs.queryActionAuditLogs
@@ -442,7 +442,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/logs/action/export", "/org/:orgId/logs/action/export",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.logExport),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs), verifyUserHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs), logActionAudit(ActionsEnum.exportLogs),
@@ -452,7 +452,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/logs/access", "/org/:orgId/logs/access",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.accessLogs),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs), verifyUserHasAction(ActionsEnum.exportLogs),
logs.queryAccessAuditLogs logs.queryAccessAuditLogs
@@ -461,7 +461,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/logs/access/export", "/org/:orgId/logs/access/export",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.logExport),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs), verifyUserHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs), logActionAudit(ActionsEnum.exportLogs),
@@ -472,7 +472,7 @@ authenticated.post(
"/re-key/:clientId/regenerate-client-secret", "/re-key/:clientId/regenerate-client-secret",
verifyClientAccess, // this is first to set the org id verifyClientAccess, // this is first to set the org id
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.rotateCredentials),
verifyUserHasAction(ActionsEnum.reGenerateSecret), verifyUserHasAction(ActionsEnum.reGenerateSecret),
reKey.reGenerateClientSecret reKey.reGenerateClientSecret
); );
@@ -481,7 +481,7 @@ authenticated.post(
"/re-key/:siteId/regenerate-site-secret", "/re-key/:siteId/regenerate-site-secret",
verifySiteAccess, // this is first to set the org id verifySiteAccess, // this is first to set the org id
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.rotateCredentials),
verifyUserHasAction(ActionsEnum.reGenerateSecret), verifyUserHasAction(ActionsEnum.reGenerateSecret),
reKey.reGenerateSiteSecret reKey.reGenerateSiteSecret
); );
@@ -489,7 +489,7 @@ authenticated.post(
authenticated.put( authenticated.put(
"/re-key/:orgId/regenerate-remote-exit-node-secret", "/re-key/:orgId/regenerate-remote-exit-node-secret",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(tierMatrix.rotateCredentials),
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.reGenerateSecret), verifyUserHasAction(ActionsEnum.reGenerateSecret),
reKey.reGenerateExitNodeSecret reKey.reGenerateExitNodeSecret

View File

@@ -13,6 +13,7 @@ import { OpenAPITags, registry } from "@server/openApi";
import { getUserDeviceName } from "@server/db/names"; import { getUserDeviceName } from "@server/db/names";
import { build } from "@server/build"; import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const getClientSchema = z.strictObject({ const getClientSchema = z.strictObject({
clientId: z clientId: z
@@ -327,7 +328,8 @@ export async function getClient(
client.currentFingerprint client.currentFingerprint
); );
const isOrgLicensed = await isLicensedOrSubscribed( const isOrgLicensed = await isLicensedOrSubscribed(
client.clients.orgId client.clients.orgId,
tierMatrix.devicePosture
); );
const postureData: PostureData | null = rawPosture const postureData: PostureData | null = rawPosture
? isOrgLicensed ? isOrgLicensed

View File

@@ -18,7 +18,7 @@ import config from "@server/lib/config";
import { APP_VERSION } from "@server/lib/consts"; import { APP_VERSION } from "@server/lib/consts";
export const newtGetTokenBodySchema = z.object({ export const newtGetTokenBodySchema = z.object({
newtId: z.string(), // newtId: z.string(),
secret: z.string(), secret: z.string(),
token: z.string().optional() token: z.string().optional()
}); });

View File

@@ -12,7 +12,8 @@ import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build"; import { build } from "@server/build";
import { cache } from "@server/lib/cache"; import { cache } from "@server/lib/cache";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; 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({ const updateOrgParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -87,26 +88,83 @@ export async function updateOrg(
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
const isLicensed = await isLicensedOrSubscribed(orgId); // Check 2FA enforcement feature
if (!isLicensed) { const has2FAFeature = await isLicensedOrSubscribed(
orgId,
tierMatrix[TierFeature.TwoFactorEnforcement]
);
if (!has2FAFeature) {
parsedBody.data.requireTwoFactor = undefined; parsedBody.data.requireTwoFactor = undefined;
parsedBody.data.maxSessionLengthHours = undefined;
parsedBody.data.passwordExpiryDays = undefined;
} }
const subscribed = await isSubscribed(orgId); // Check session duration policies feature
if ( const hasSessionDurationFeature = await isLicensedOrSubscribed(
build == "saas" && orgId,
subscribed && tierMatrix[TierFeature.SessionDurationPolicies]
parsedBody.data.settingsLogRetentionDaysRequest && );
parsedBody.data.settingsLogRetentionDaysRequest > 30 if (!hasSessionDurationFeature) {
) { parsedBody.data.maxSessionLengthHours = undefined;
return next( }
createHttpError(
HttpCode.FORBIDDEN, // Check password expiration policies feature
"You are not allowed to set log retention days greater than 30 with your current subscription" 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 const updatedOrg = await db