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 { 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;
}

View File

@@ -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" &&

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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()
});

View File

@@ -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