Merge branch 'dev' into refactor/paginated-tables

This commit is contained in:
Fred KISSIE
2026-02-13 06:03:09 +01:00
184 changed files with 6127 additions and 4176 deletions

View File

@@ -17,8 +17,7 @@ import {
ResourceHeaderAuthExtendedCompatibility,
ResourcePassword,
ResourcePincode,
ResourceRule,
resourceSessions
ResourceRule
} from "@server/db";
import config from "@server/lib/config";
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
@@ -32,7 +31,6 @@ import { fromError } from "zod-validation-error";
import { getCountryCodeForIp } from "@server/lib/geoip";
import { getAsnForIp } from "@server/lib/asn";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { verifyPassword } from "@server/auth/password";
import {
checkOrgAccessPolicy,
@@ -40,8 +38,9 @@ import {
} from "#dynamic/lib/checkOrgAccessPolicy";
import { logRequestAudit } from "./logRequestAudit";
import cache from "@server/lib/cache";
import semver from "semver";
import { APP_VERSION } from "@server/lib/consts";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const verifyResourceSessionSchema = z.object({
sessions: z.record(z.string(), z.string()).optional(),
@@ -798,8 +797,11 @@ async function notAllowed(
) {
let loginPage: LoginPage | null = null;
if (orgId) {
const { tier } = await getOrgTierData(orgId); // returns null in oss
if (tier === TierId.STANDARD) {
const subscribed = await isSubscribed(
orgId,
tierMatrix.loginPageDomain
);
if (subscribed) {
loginPage = await getOrgLoginPage(orgId);
}
}
@@ -852,8 +854,8 @@ async function headerAuthChallenged(
) {
let loginPage: LoginPage | null = null;
if (orgId) {
const { tier } = await getOrgTierData(orgId); // returns null in oss
if (tier === TierId.STANDARD) {
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain);
if (subscribed) {
loginPage = await getOrgLoginPage(orgId);
}
}
@@ -1039,7 +1041,11 @@ export function isPathAllowed(pattern: string, path: string): boolean {
const MAX_RECURSION_DEPTH = 100;
// Recursive function to try different wildcard matches
function matchSegments(patternIndex: number, pathIndex: number, depth: number = 0): boolean {
function matchSegments(
patternIndex: number,
pathIndex: number,
depth: number = 0
): boolean {
// Check recursion depth limit
if (depth > MAX_RECURSION_DEPTH) {
logger.warn(
@@ -1125,7 +1131,11 @@ export function isPathAllowed(pattern: string, path: string): boolean {
logger.debug(
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
);
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
return matchSegments(
patternIndex + 1,
pathIndex + 1,
depth + 1
);
}
logger.debug(

View File

@@ -2,6 +2,8 @@ import { Limit, Subscription, SubscriptionItem, Usage } from "@server/db";
export type GetOrgSubscriptionResponse = {
subscriptions: Array<{ subscription: Subscription; items: SubscriptionItem[] }>;
/** When build === saas, true if org has exceeded plan limits (sites, users, etc.) */
limitsExceeded?: boolean;
};
export type GetOrgUsageResponse = {

View File

@@ -26,6 +26,7 @@ import { generateId } from "@server/auth/sessions/app";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import { getUniqueClientName } from "@server/db/names";
import { build } from "@server/build";
const createClientParamsSchema = z.strictObject({
orgId: z.string()
@@ -101,7 +102,7 @@ export async function createClient(
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid subnet format. Please provide a valid CIDR notation."
"Invalid subnet format. Please provide a valid IP."
)
);
}
@@ -195,6 +196,12 @@ export async function createClient(
const randomExitNode =
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
if (!randomExitNode) {
return next(
createHttpError(HttpCode.NOT_FOUND, `No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`)
);
}
const [adminRole] = await trx
.select()
.from(roles)

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

@@ -131,7 +131,7 @@ export async function createOrgDomain(
}
const rejectDomains = await usageService.checkLimitSet(
orgId,
false,
FeatureId.DOMAINS,
{
...usage,
@@ -354,7 +354,7 @@ export async function createOrgDomain(
});
if (numOrgDomains) {
await usageService.updateDaily(
await usageService.updateCount(
orgId,
FeatureId.DOMAINS,
numOrgDomains.length

View File

@@ -86,7 +86,7 @@ export async function deleteAccountDomain(
});
if (numOrgDomains) {
await usageService.updateDaily(
await usageService.updateCount(
orgId,
FeatureId.DOMAINS,
numOrgDomains.length

View File

@@ -41,7 +41,8 @@ import {
verifyUserHasAction,
verifyUserIsOrgOwner,
verifySiteResourceAccess,
verifyOlmAccess
verifyOlmAccess,
verifyLimits
} from "@server/middlewares";
import { ActionsEnum } from "@server/auth/actions";
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
@@ -79,6 +80,7 @@ authenticated.get(
authenticated.post(
"/org/:orgId",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateOrg),
logActionAudit(ActionsEnum.updateOrg),
org.updateOrg
@@ -168,6 +170,7 @@ authenticated.get(
authenticated.put(
"/org/:orgId/client",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createClient),
logActionAudit(ActionsEnum.createClient),
client.createClient
@@ -185,6 +188,7 @@ authenticated.delete(
authenticated.post(
"/client/:clientId/archive",
verifyClientAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.archiveClient),
logActionAudit(ActionsEnum.archiveClient),
client.archiveClient
@@ -193,6 +197,7 @@ authenticated.post(
authenticated.post(
"/client/:clientId/unarchive",
verifyClientAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.unarchiveClient),
logActionAudit(ActionsEnum.unarchiveClient),
client.unarchiveClient
@@ -201,6 +206,7 @@ authenticated.post(
authenticated.post(
"/client/:clientId/block",
verifyClientAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.blockClient),
logActionAudit(ActionsEnum.blockClient),
client.blockClient
@@ -209,6 +215,7 @@ authenticated.post(
authenticated.post(
"/client/:clientId/unblock",
verifyClientAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.unblockClient),
logActionAudit(ActionsEnum.unblockClient),
client.unblockClient
@@ -217,6 +224,7 @@ authenticated.post(
authenticated.post(
"/client/:clientId",
verifyClientAccess, // this will check if the user has access to the client
verifyLimits,
verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client
logActionAudit(ActionsEnum.updateClient),
client.updateClient
@@ -231,6 +239,7 @@ authenticated.post(
authenticated.post(
"/site/:siteId",
verifySiteAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateSite),
logActionAudit(ActionsEnum.updateSite),
site.updateSite
@@ -280,6 +289,7 @@ authenticated.get(
authenticated.put(
"/org/:orgId/site-resource",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createSiteResource),
logActionAudit(ActionsEnum.createSiteResource),
siteResource.createSiteResource
@@ -310,6 +320,7 @@ authenticated.get(
authenticated.post(
"/site-resource/:siteResourceId",
verifySiteResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateSiteResource),
logActionAudit(ActionsEnum.updateSiteResource),
siteResource.updateSiteResource
@@ -348,6 +359,7 @@ authenticated.post(
"/site-resource/:siteResourceId/roles",
verifySiteResourceAccess,
verifyRoleAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
siteResource.setSiteResourceRoles
@@ -357,6 +369,7 @@ authenticated.post(
"/site-resource/:siteResourceId/users",
verifySiteResourceAccess,
verifySetResourceUsers,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.setSiteResourceUsers
@@ -366,6 +379,7 @@ authenticated.post(
"/site-resource/:siteResourceId/clients",
verifySiteResourceAccess,
verifySetResourceClients,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.setSiteResourceClients
@@ -375,6 +389,7 @@ authenticated.post(
"/site-resource/:siteResourceId/clients/add",
verifySiteResourceAccess,
verifySetResourceClients,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.addClientToSiteResource
@@ -384,6 +399,7 @@ authenticated.post(
"/site-resource/:siteResourceId/clients/remove",
verifySiteResourceAccess,
verifySetResourceClients,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.removeClientFromSiteResource
@@ -392,6 +408,7 @@ authenticated.post(
authenticated.put(
"/org/:orgId/resource",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createResource),
logActionAudit(ActionsEnum.createResource),
resource.createResource
@@ -506,6 +523,7 @@ authenticated.get(
authenticated.post(
"/resource/:resourceId",
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateResource),
logActionAudit(ActionsEnum.updateResource),
resource.updateResource
@@ -521,6 +539,7 @@ authenticated.delete(
authenticated.put(
"/resource/:resourceId/target",
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createTarget),
logActionAudit(ActionsEnum.createTarget),
target.createTarget
@@ -535,6 +554,7 @@ authenticated.get(
authenticated.put(
"/resource/:resourceId/rule",
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createResourceRule),
logActionAudit(ActionsEnum.createResourceRule),
resource.createResourceRule
@@ -548,6 +568,7 @@ authenticated.get(
authenticated.post(
"/resource/:resourceId/rule/:ruleId",
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateResourceRule),
logActionAudit(ActionsEnum.updateResourceRule),
resource.updateResourceRule
@@ -569,6 +590,7 @@ authenticated.get(
authenticated.post(
"/target/:targetId",
verifyTargetAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateTarget),
logActionAudit(ActionsEnum.updateTarget),
target.updateTarget
@@ -584,6 +606,7 @@ authenticated.delete(
authenticated.put(
"/org/:orgId/role",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createRole),
logActionAudit(ActionsEnum.createRole),
role.createRole
@@ -598,6 +621,7 @@ authenticated.get(
authenticated.post(
"/role/:roleId",
verifyRoleAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateRole),
logActionAudit(ActionsEnum.updateRole),
role.updateRole
@@ -626,6 +650,7 @@ authenticated.post(
"/role/:roleId/add/:userId",
verifyRoleAccess,
verifyUserAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.addUserRole),
logActionAudit(ActionsEnum.addUserRole),
user.addUserRole
@@ -635,6 +660,7 @@ authenticated.post(
"/resource/:resourceId/roles",
verifyResourceAccess,
verifyRoleAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
resource.setResourceRoles
@@ -644,6 +670,7 @@ authenticated.post(
"/resource/:resourceId/users",
verifyResourceAccess,
verifySetResourceUsers,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
resource.setResourceUsers
@@ -652,6 +679,7 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/password`,
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourcePassword),
logActionAudit(ActionsEnum.setResourcePassword),
resource.setResourcePassword
@@ -660,6 +688,7 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/pincode`,
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourcePincode),
logActionAudit(ActionsEnum.setResourcePincode),
resource.setResourcePincode
@@ -668,6 +697,7 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/header-auth`,
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceHeaderAuth),
logActionAudit(ActionsEnum.setResourceHeaderAuth),
resource.setResourceHeaderAuth
@@ -676,6 +706,7 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/whitelist`,
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourceWhitelist),
logActionAudit(ActionsEnum.setResourceWhitelist),
resource.setResourceWhitelist
@@ -691,6 +722,7 @@ authenticated.get(
authenticated.post(
`/resource/:resourceId/access-token`,
verifyResourceAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.generateAccessToken),
logActionAudit(ActionsEnum.generateAccessToken),
accessToken.generateAccessToken
@@ -781,6 +813,7 @@ authenticated.delete(
authenticated.put(
"/org/:orgId/user",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createOrgUser),
logActionAudit(ActionsEnum.createOrgUser),
user.createOrgUser
@@ -790,6 +823,7 @@ authenticated.post(
"/org/:orgId/user/:userId",
verifyOrgAccess,
verifyUserAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateOrgUser),
logActionAudit(ActionsEnum.updateOrgUser),
user.updateOrgUser
@@ -862,6 +896,7 @@ authenticated.post(
"/user/:userId/olm/:olmId/archive",
verifyIsLoggedInUser,
verifyOlmAccess,
verifyLimits,
olm.archiveUserOlm
);
@@ -976,6 +1011,7 @@ authenticated.post(
`/org/:orgId/api-key/:apiKeyId/actions`,
verifyOrgAccess,
verifyApiKeyAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setApiKeyActions),
logActionAudit(ActionsEnum.setApiKeyActions),
apiKeys.setApiKeyActions
@@ -992,6 +1028,7 @@ authenticated.get(
authenticated.put(
`/org/:orgId/api-key`,
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createApiKey),
logActionAudit(ActionsEnum.createApiKey),
apiKeys.createOrgApiKey
@@ -1017,6 +1054,7 @@ authenticated.get(
authenticated.put(
`/org/:orgId/domain`,
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createOrgDomain),
logActionAudit(ActionsEnum.createOrgDomain),
domain.createOrgDomain
@@ -1026,6 +1064,7 @@ authenticated.post(
`/org/:orgId/domain/:domainId/restart`,
verifyOrgAccess,
verifyDomainAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.restartOrgDomain),
logActionAudit(ActionsEnum.restartOrgDomain),
domain.restartOrgDomain
@@ -1072,6 +1111,7 @@ authenticated.get(
authenticated.put(
"/org/:orgId/blueprint",
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.applyBlueprint),
blueprints.applyYAMLBlueprint
);

View File

@@ -114,7 +114,6 @@ export async function updateSiteBandwidth(
// Aggregate usage data by organization (collected outside transaction)
const orgUsageMap = new Map<string, number>();
const orgUptimeMap = new Map<string, number>();
if (activePeers.length > 0) {
// Remove any active peers from offline tracking since they're sending data
@@ -166,14 +165,6 @@ export async function updateSiteBandwidth(
updatedSite.orgId,
currentOrgUsage + totalBandwidth
);
// Add 10 seconds of uptime for each active site
const currentOrgUptime =
orgUptimeMap.get(updatedSite.orgId) || 0;
orgUptimeMap.set(
updatedSite.orgId,
currentOrgUptime + 10 / 60
);
}
} catch (error) {
logger.error(
@@ -187,11 +178,9 @@ export async function updateSiteBandwidth(
// Process usage updates outside of site update transactions
// This separates the concerns and reduces lock contention
if (calcUsageAndLimits && (orgUsageMap.size > 0 || orgUptimeMap.size > 0)) {
if (calcUsageAndLimits && orgUsageMap.size > 0) {
// Sort org IDs to ensure consistent lock ordering
const allOrgIds = [
...new Set([...orgUsageMap.keys(), ...orgUptimeMap.keys()])
].sort();
const allOrgIds = [...new Set([...orgUsageMap.keys()])].sort();
for (const orgId of allOrgIds) {
try {
@@ -208,7 +197,7 @@ export async function updateSiteBandwidth(
usageService
.checkLimitSet(
orgId,
true,
FeatureId.EGRESS_DATA_MB,
bandwidthUsage
)
@@ -220,32 +209,6 @@ export async function updateSiteBandwidth(
});
}
}
// Process uptime usage for this org
const totalUptime = orgUptimeMap.get(orgId);
if (totalUptime) {
const uptimeUsage = await usageService.add(
orgId,
FeatureId.SITE_UPTIME,
totalUptime
);
if (uptimeUsage) {
// Fire and forget - don't block on limit checking
usageService
.checkLimitSet(
orgId,
true,
FeatureId.SITE_UPTIME,
uptimeUsage
)
.catch((error: any) => {
logger.error(
`Error checking uptime limits for org ${orgId}:`,
error
);
});
}
}
} catch (error) {
logger.error(`Error processing usage for org ${orgId}:`, error);
// Continue with other orgs

View File

@@ -93,7 +93,9 @@ export async function createOidcIdp(
name,
autoProvision,
type: "oidc",
tags
tags,
defaultOrgMapping: `'{{orgId}}'`,
defaultRoleMapping: `'Member'`
})
.returning();

View File

@@ -14,8 +14,8 @@ import jsonwebtoken from "jsonwebtoken";
import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto";
import { build } from "@server/build";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z
.object({
@@ -113,8 +113,10 @@ export async function generateOidcUrl(
}
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
const subscribed = await isSubscribed(
orgId,
tierMatrix.orgOidc
);
if (!subscribed) {
return next(
createHttpError(

View File

@@ -34,6 +34,8 @@ import { FeatureId } from "@server/lib/billing";
import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const ensureTrailingSlash = (url: string): string => {
return url;
@@ -326,6 +328,33 @@ export async function validateOidcCallback(
.where(eq(idpOrg.idpId, existingIdp.idp.idpId))
.innerJoin(orgs, eq(orgs.orgId, idpOrg.orgId));
allOrgs = idpOrgs.map((o) => o.orgs);
// TODO: when there are multiple orgs we need to do this better!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1
if (allOrgs.length > 1) {
// for some reason there is more than one org
logger.error(
"More than one organization linked to this IdP. This should not happen with auto-provisioning enabled."
);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Multiple organizations linked to this IdP. Please contact support."
)
);
}
const subscribed = await isSubscribed(
allOrgs[0].orgId,
tierMatrix.autoProvisioning
);
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
} else {
allOrgs = await db.select().from(orgs);
}
@@ -587,7 +616,7 @@ export async function validateOidcCallback(
});
for (const orgCount of orgUserCounts) {
await usageService.updateDaily(
await usageService.updateCount(
orgCount.orgId,
FeatureId.USERS,
orgCount.userCount

View File

@@ -26,7 +26,8 @@ import {
verifyApiKeyIsRoot,
verifyApiKeyClientAccess,
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients
verifyApiKeySetResourceClients,
verifyLimits
} from "@server/middlewares";
import HttpCode from "@server/types/HttpCode";
import { Router } from "express";
@@ -74,6 +75,7 @@ authenticated.get(
authenticated.post(
"/org/:orgId",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateOrg),
logActionAudit(ActionsEnum.updateOrg),
org.updateOrg
@@ -90,6 +92,7 @@ authenticated.delete(
authenticated.put(
"/org/:orgId/site",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createSite),
logActionAudit(ActionsEnum.createSite),
site.createSite
@@ -126,6 +129,7 @@ authenticated.get(
authenticated.post(
"/site/:siteId",
verifyApiKeySiteAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateSite),
logActionAudit(ActionsEnum.updateSite),
site.updateSite
@@ -146,8 +150,9 @@ authenticated.get(
);
// Site Resource endpoints
authenticated.put(
"/org/:orgId/private-resource",
"/org/:orgId/site-resource",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
logActionAudit(ActionsEnum.createSiteResource),
siteResource.createSiteResource
@@ -178,6 +183,7 @@ authenticated.get(
authenticated.post(
"/site-resource/:siteResourceId",
verifyApiKeySiteResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
logActionAudit(ActionsEnum.updateSiteResource),
siteResource.updateSiteResource
@@ -216,6 +222,7 @@ authenticated.post(
"/site-resource/:siteResourceId/roles",
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
siteResource.setSiteResourceRoles
@@ -225,6 +232,7 @@ authenticated.post(
"/site-resource/:siteResourceId/users",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.setSiteResourceUsers
@@ -234,6 +242,7 @@ authenticated.post(
"/site-resource/:siteResourceId/roles/add",
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
siteResource.addRoleToSiteResource
@@ -243,6 +252,7 @@ authenticated.post(
"/site-resource/:siteResourceId/roles/remove",
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
siteResource.removeRoleFromSiteResource
@@ -252,6 +262,7 @@ authenticated.post(
"/site-resource/:siteResourceId/users/add",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.addUserToSiteResource
@@ -261,6 +272,7 @@ authenticated.post(
"/site-resource/:siteResourceId/users/remove",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.removeUserFromSiteResource
@@ -270,6 +282,7 @@ authenticated.post(
"/site-resource/:siteResourceId/clients",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.setSiteResourceClients
@@ -279,6 +292,7 @@ authenticated.post(
"/site-resource/:siteResourceId/clients/add",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.addClientToSiteResource
@@ -288,6 +302,7 @@ authenticated.post(
"/site-resource/:siteResourceId/clients/remove",
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
siteResource.removeClientFromSiteResource
@@ -296,6 +311,7 @@ authenticated.post(
authenticated.put(
"/org/:orgId/resource",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResource),
logActionAudit(ActionsEnum.createResource),
resource.createResource
@@ -304,6 +320,7 @@ authenticated.put(
authenticated.put(
"/org/:orgId/site/:siteId/resource",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResource),
logActionAudit(ActionsEnum.createResource),
resource.createResource
@@ -340,6 +357,7 @@ authenticated.get(
authenticated.post(
"/org/:orgId/create-invite",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.inviteUser),
logActionAudit(ActionsEnum.inviteUser),
user.inviteUser
@@ -377,6 +395,7 @@ authenticated.get(
authenticated.post(
"/resource/:resourceId",
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateResource),
logActionAudit(ActionsEnum.updateResource),
resource.updateResource
@@ -393,6 +412,7 @@ authenticated.delete(
authenticated.put(
"/resource/:resourceId/target",
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createTarget),
logActionAudit(ActionsEnum.createTarget),
target.createTarget
@@ -408,6 +428,7 @@ authenticated.get(
authenticated.put(
"/resource/:resourceId/rule",
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResourceRule),
logActionAudit(ActionsEnum.createResourceRule),
resource.createResourceRule
@@ -423,6 +444,7 @@ authenticated.get(
authenticated.post(
"/resource/:resourceId/rule/:ruleId",
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateResourceRule),
logActionAudit(ActionsEnum.updateResourceRule),
resource.updateResourceRule
@@ -446,6 +468,7 @@ authenticated.get(
authenticated.post(
"/target/:targetId",
verifyApiKeyTargetAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateTarget),
logActionAudit(ActionsEnum.updateTarget),
target.updateTarget
@@ -462,6 +485,7 @@ authenticated.delete(
authenticated.put(
"/org/:orgId/role",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createRole),
logActionAudit(ActionsEnum.createRole),
role.createRole
@@ -470,6 +494,7 @@ authenticated.put(
authenticated.post(
"/role/:roleId",
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateRole),
logActionAudit(ActionsEnum.updateRole),
role.updateRole
@@ -501,6 +526,7 @@ authenticated.post(
"/role/:roleId/add/:userId",
verifyApiKeyRoleAccess,
verifyApiKeyUserAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.addUserRole),
logActionAudit(ActionsEnum.addUserRole),
user.addUserRole
@@ -510,6 +536,7 @@ authenticated.post(
"/resource/:resourceId/roles",
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
resource.setResourceRoles
@@ -519,6 +546,7 @@ authenticated.post(
"/resource/:resourceId/users",
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
resource.setResourceUsers
@@ -528,6 +556,7 @@ authenticated.post(
"/resource/:resourceId/roles/add",
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
resource.addRoleToResource
@@ -537,6 +566,7 @@ authenticated.post(
"/resource/:resourceId/roles/remove",
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
logActionAudit(ActionsEnum.setResourceRoles),
resource.removeRoleFromResource
@@ -546,6 +576,7 @@ authenticated.post(
"/resource/:resourceId/users/add",
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
resource.addUserToResource
@@ -555,6 +586,7 @@ authenticated.post(
"/resource/:resourceId/users/remove",
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
resource.removeUserFromResource
@@ -563,6 +595,7 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/password`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePassword),
logActionAudit(ActionsEnum.setResourcePassword),
resource.setResourcePassword
@@ -571,6 +604,7 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/pincode`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePincode),
logActionAudit(ActionsEnum.setResourcePincode),
resource.setResourcePincode
@@ -579,6 +613,7 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/header-auth`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth),
logActionAudit(ActionsEnum.setResourceHeaderAuth),
resource.setResourceHeaderAuth
@@ -587,6 +622,7 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/whitelist`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
logActionAudit(ActionsEnum.setResourceWhitelist),
resource.setResourceWhitelist
@@ -595,6 +631,7 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/whitelist/add`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
resource.addEmailToResourceWhitelist
);
@@ -602,6 +639,7 @@ authenticated.post(
authenticated.post(
`/resource/:resourceId/whitelist/remove`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
resource.removeEmailFromResourceWhitelist
);
@@ -616,6 +654,7 @@ authenticated.get(
authenticated.post(
`/resource/:resourceId/access-token`,
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.generateAccessToken),
logActionAudit(ActionsEnum.generateAccessToken),
accessToken.generateAccessToken
@@ -653,6 +692,7 @@ authenticated.get(
authenticated.post(
"/user/:userId/2fa",
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateUser),
logActionAudit(ActionsEnum.updateUser),
user.updateUser2FA
@@ -675,6 +715,7 @@ authenticated.get(
authenticated.put(
"/org/:orgId/user",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createOrgUser),
logActionAudit(ActionsEnum.createOrgUser),
user.createOrgUser
@@ -684,6 +725,7 @@ authenticated.post(
"/org/:orgId/user/:userId",
verifyApiKeyOrgAccess,
verifyApiKeyUserAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateOrgUser),
logActionAudit(ActionsEnum.updateOrgUser),
user.updateOrgUser
@@ -714,6 +756,7 @@ authenticated.get(
authenticated.post(
`/org/:orgId/api-key/:apiKeyId/actions`,
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setApiKeyActions),
logActionAudit(ActionsEnum.setApiKeyActions),
apiKeys.setApiKeyActions
@@ -729,6 +772,7 @@ authenticated.get(
authenticated.put(
`/org/:orgId/api-key`,
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createApiKey),
logActionAudit(ActionsEnum.createApiKey),
apiKeys.createOrgApiKey
@@ -745,6 +789,7 @@ authenticated.delete(
authenticated.put(
"/idp/oidc",
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createIdp),
logActionAudit(ActionsEnum.createIdp),
idp.createOidcIdp
@@ -753,6 +798,7 @@ authenticated.put(
authenticated.post(
"/idp/:idpId/oidc",
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateIdp),
logActionAudit(ActionsEnum.updateIdp),
idp.updateOidcIdp
@@ -776,6 +822,7 @@ authenticated.get(
authenticated.put(
"/idp/:idpId/org/:orgId",
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createIdpOrg),
logActionAudit(ActionsEnum.createIdpOrg),
idp.createIdpOrgPolicy
@@ -784,6 +831,7 @@ authenticated.put(
authenticated.post(
"/idp/:idpId/org/:orgId",
verifyApiKeyIsRoot,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateIdpOrg),
logActionAudit(ActionsEnum.updateIdpOrg),
idp.updateIdpOrgPolicy
@@ -835,6 +883,7 @@ authenticated.get(
authenticated.put(
"/org/:orgId/client",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createClient),
logActionAudit(ActionsEnum.createClient),
client.createClient
@@ -861,6 +910,7 @@ authenticated.delete(
authenticated.post(
"/client/:clientId/archive",
verifyApiKeyClientAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.archiveClient),
logActionAudit(ActionsEnum.archiveClient),
client.archiveClient
@@ -869,6 +919,7 @@ authenticated.post(
authenticated.post(
"/client/:clientId/unarchive",
verifyApiKeyClientAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.unarchiveClient),
logActionAudit(ActionsEnum.unarchiveClient),
client.unarchiveClient
@@ -877,6 +928,7 @@ authenticated.post(
authenticated.post(
"/client/:clientId/block",
verifyApiKeyClientAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.blockClient),
logActionAudit(ActionsEnum.blockClient),
client.blockClient
@@ -885,6 +937,7 @@ authenticated.post(
authenticated.post(
"/client/:clientId/unblock",
verifyApiKeyClientAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.unblockClient),
logActionAudit(ActionsEnum.unblockClient),
client.unblockClient
@@ -893,6 +946,7 @@ authenticated.post(
authenticated.post(
"/client/:clientId",
verifyApiKeyClientAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateClient),
logActionAudit(ActionsEnum.updateClient),
client.updateClient
@@ -901,6 +955,7 @@ authenticated.post(
authenticated.put(
"/org/:orgId/blueprint",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.applyBlueprint),
logActionAudit(ActionsEnum.applyBlueprint),
blueprints.applyJSONBlueprint

View File

@@ -1,17 +1,13 @@
import { db, ExitNode, exitNodeOrgs, newts, Transaction } from "@server/db";
import { db, ExitNode, newts, Transaction } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db";
import { targetHealthCheck } from "@server/db";
import { eq, and, sql, inArray, ne } from "drizzle-orm";
import { exitNodes, Newt, sites } from "@server/db";
import { eq } from "drizzle-orm";
import { addPeer, deletePeer } from "../gerbil/peers";
import logger from "@server/logger";
import config from "@server/lib/config";
import {
findNextAvailableCidr,
getNextAvailableClientSubnet
} from "@server/lib/ip";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
import {
selectBestExitNode,
verifyExitNodeOrgAccess
@@ -30,8 +26,6 @@ export type ExitNodePingResult = {
wasPreviouslyConnected: boolean;
};
const numTimesLimitExceededForId: Record<string, number> = {};
export const handleNewtRegisterMessage: MessageHandler = async (context) => {
const { message, client, sendToClient } = context;
const newt = client as Newt;
@@ -96,42 +90,6 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
fetchContainers(newt.newtId);
}
const rejectSiteUptime = await usageService.checkLimitSet(
oldSite.orgId,
false,
FeatureId.SITE_UPTIME
);
const rejectEgressDataMb = await usageService.checkLimitSet(
oldSite.orgId,
false,
FeatureId.EGRESS_DATA_MB
);
// Do we need to check the users and domains daily limits here?
// const rejectUsers = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.USERS);
// const rejectDomains = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.DOMAINS);
// if (rejectEgressDataMb || rejectSiteUptime || rejectUsers || rejectDomains) {
if (rejectEgressDataMb || rejectSiteUptime) {
logger.info(
`Usage limits exceeded for org ${oldSite.orgId}. Rejecting newt registration.`
);
// PREVENT FURTHER REGISTRATION ATTEMPTS SO WE DON'T SPAM
// Increment the limit exceeded count for this site
numTimesLimitExceededForId[newt.newtId] =
(numTimesLimitExceededForId[newt.newtId] || 0) + 1;
if (numTimesLimitExceededForId[newt.newtId] > 15) {
logger.debug(
`Newt ${newt.newtId} has exceeded usage limits 15 times. Terminating...`
);
}
return;
}
let siteSubnet = oldSite.subnet;
let exitNodeIdToQuery = oldSite.exitNodeId;
if (exitNodeId && (oldSite.exitNodeId !== exitNodeId || !oldSite.subnet)) {

View File

@@ -271,7 +271,7 @@ export async function createOrg(
// make sure we have the stripe customer
const customerId = await createCustomer(orgId, req.user?.email);
if (customerId) {
await usageService.updateDaily(
await usageService.updateCount(
orgId,
FeatureId.USERS,
1,

View File

@@ -10,10 +10,10 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { cache } from "@server/lib/cache";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { getOrgTierData } from "#dynamic/lib/billing";
const updateOrgParamsSchema = z.strictObject({
orgId: z.string()
@@ -88,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 { tier } = await getOrgTierData(orgId);
if (
build == "saas" &&
tier != TierId.STANDARD &&
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 = 3;
} 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

View File

@@ -14,6 +14,7 @@ import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToke
import config from "@server/lib/config";
import stoi from "@server/lib/stoi";
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
import { normalizePostAuthPath } from "@server/lib/normalizePostAuthPath";
const authWithAccessTokenBodySchema = z.strictObject({
accessToken: z.string(),
@@ -164,10 +165,16 @@ export async function authWithAccessToken(
requestIp: req.ip
});
let redirectUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
const postAuthPath = normalizePostAuthPath(resource.postAuthPath);
if (postAuthPath) {
redirectUrl = redirectUrl + postAuthPath;
}
return response<AuthWithAccessTokenResponse>(res, {
data: {
session: token,
redirectUrl: `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
redirectUrl
},
success: true,
error: false,

View File

@@ -36,7 +36,8 @@ const createHttpResourceSchema = z
http: z.boolean(),
protocol: z.enum(["tcp", "udp"]),
domainId: z.string(),
stickySession: z.boolean().optional()
stickySession: z.boolean().optional(),
postAuthPath: z.string().nullable().optional()
})
.refine(
(data) => {
@@ -188,7 +189,7 @@ async function createHttpResource(
);
}
const { name, domainId } = parsedBody.data;
const { name, domainId, postAuthPath } = parsedBody.data;
const subdomain = parsedBody.data.subdomain;
const stickySession = parsedBody.data.stickySession;
@@ -255,7 +256,8 @@ async function createHttpResource(
http: true,
protocol: "tcp",
ssl: true,
stickySession: stickySession
stickySession: stickySession,
postAuthPath: postAuthPath
})
.returning();

View File

@@ -35,6 +35,7 @@ export type GetResourceAuthInfoResponse = {
whitelist: boolean;
skipToIdpId: number | null;
orgId: string;
postAuthPath: string | null;
};
export async function getResourceAuthInfo(
@@ -147,7 +148,8 @@ export async function getResourceAuthInfo(
url,
whitelist: resource.emailWhitelistEnabled,
skipToIdpId: resource.skipToIdpId,
orgId: resource.orgId
orgId: resource.orgId,
postAuthPath: resource.postAuthPath ?? null
},
success: true,
error: false,

View File

@@ -24,6 +24,7 @@ import { createCertificate } from "#dynamic/routers/certificates/createCertifica
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const updateResourceParamsSchema = z.strictObject({
resourceId: z.string().transform(Number).pipe(z.int().positive())
@@ -54,7 +55,8 @@ const updateHttpResourceBodySchema = z
maintenanceModeType: z.enum(["forced", "automatic"]).optional(),
maintenanceTitle: z.string().max(255).nullable().optional(),
maintenanceMessage: z.string().max(2000).nullable().optional(),
maintenanceEstimatedTime: z.string().max(100).nullable().optional()
maintenanceEstimatedTime: z.string().max(100).nullable().optional(),
postAuthPath: z.string().nullable().optional()
})
.refine((data) => Object.keys(data).length > 0, {
error: "At least one field must be provided for update"
@@ -341,7 +343,7 @@ async function updateHttpResource(
headers = null;
}
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
const isLicensed = await isLicensedOrSubscribed(resource.orgId, tierMatrix.maintencePage);
if (!isLicensed) {
updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined;

View File

@@ -12,6 +12,7 @@ import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const createRoleParamsSchema = z.strictObject({
orgId: z.string()
@@ -100,7 +101,7 @@ export async function createRole(
);
}
const isLicensed = await isLicensedOrSubscribed(orgId);
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
if (!isLicensed) {
roleData.requireDeviceApproval = undefined;
}

View File

@@ -10,6 +10,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { OpenAPITags, registry } from "@server/openApi";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const updateRoleParamsSchema = z.strictObject({
roleId: z.string().transform(Number).pipe(z.int().positive())
@@ -110,7 +111,7 @@ export async function updateRole(
);
}
const isLicensed = await isLicensedOrSubscribed(orgId);
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
if (!isLicensed) {
updateData.requireDeviceApproval = undefined;
}

View File

@@ -18,6 +18,8 @@ import { isValidIP } from "@server/lib/validators";
import { isIpInCidr } from "@server/lib/ip";
import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes";
import { build } from "@server/build";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
const createSiteParamsSchema = z.strictObject({
orgId: z.string()
@@ -126,6 +128,35 @@ export async function createSite(
);
}
if (build == "saas") {
const usage = await usageService.getUsage(orgId, FeatureId.SITES);
if (!usage) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"No usage data found for this organization"
)
);
}
const rejectSites = await usageService.checkLimitSet(
orgId,
FeatureId.SITES,
{
...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1
} // We need to add one to know if we are violating the limit
);
if (rejectSites) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Site limit exceeded. Please upgrade your plan."
)
);
}
}
let updatedAddress = null;
if (address) {
if (!org.subnet) {
@@ -256,10 +287,22 @@ export async function createSite(
const niceId = await getUniqueSiteName(orgId);
let newSite: Site;
let newSite: Site | undefined;
let numSites: Site[] | undefined;
await db.transaction(async (trx) => {
if (type == "wireguard" || type == "newt") {
if (type == "newt") {
[newSite] = await trx
.insert(sites)
.values({
orgId,
name,
niceId,
address: updatedAddress || null,
type,
dockerSocketEnabled: true
})
.returning();
} else if (type == "wireguard") {
// we are creating a site with an exit node (tunneled)
if (!subnet) {
return next(
@@ -311,11 +354,9 @@ export async function createSite(
exitNodeId,
name,
niceId,
address: updatedAddress || null,
subnet,
type,
dockerSocketEnabled: type == "newt",
...(pubKey && type == "wireguard" && { pubKey })
pubKey: pubKey || null
})
.returning();
} else if (type == "local") {
@@ -402,13 +443,35 @@ export async function createSite(
});
}
return response<CreateSiteResponse>(res, {
data: newSite,
success: true,
error: false,
message: "Site created successfully",
status: HttpCode.CREATED
});
numSites = await trx
.select()
.from(sites)
.where(eq(sites.orgId, orgId));
});
if (numSites) {
await usageService.updateCount(
orgId,
FeatureId.SITES,
numSites.length
);
}
if (!newSite) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to create site"
)
);
}
return response<CreateSiteResponse>(res, {
data: newSite,
success: true,
error: false,
message: "Site created successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, siteResources } from "@server/db";
import { db, Site, siteResources } from "@server/db";
import { newts, newtSessions, sites } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
@@ -12,6 +12,8 @@ import { fromError } from "zod-validation-error";
import { sendToClient } from "#dynamic/routers/ws";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
const deleteSiteSchema = z.strictObject({
siteId: z.string().transform(Number).pipe(z.int().positive())
@@ -62,6 +64,7 @@ export async function deleteSite(
}
let deletedNewtId: string | null = null;
let numSites: Site[] | undefined;
await db.transaction(async (trx) => {
if (site.type == "wireguard") {
@@ -99,8 +102,20 @@ export async function deleteSite(
}
await trx.delete(sites).where(eq(sites.siteId, siteId));
numSites = await trx
.select()
.from(sites)
.where(eq(sites.orgId, site.orgId));
});
if (numSites) {
await usageService.updateCount(
site.orgId,
FeatureId.SITES,
numSites.length
);
}
// Send termination message outside of transaction to prevent blocking
if (deletedNewtId) {
const payload = {

View File

@@ -13,6 +13,7 @@ import { verifySession } from "@server/auth/sessions/verifySession";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { build } from "@server/build";
const acceptInviteBodySchema = z.strictObject({
token: z.string(),
@@ -92,6 +93,38 @@ export async function acceptInvite(
);
}
if (build == "saas") {
const usage = await usageService.getUsage(
existingInvite.orgId,
FeatureId.USERS
);
if (!usage) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"No usage data found for this organization"
)
);
}
const rejectUsers = await usageService.checkLimitSet(
existingInvite.orgId,
FeatureId.USERS,
{
...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1
} // We need to add one to know if we are violating the limit
);
if (rejectUsers) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Can not accept because this org's user limit is exceeded. Please contact your administrator to upgrade their plan."
)
);
}
}
let roleId: number;
let totalUsers: UserOrg[] | undefined;
// get the role to make sure it exists
@@ -125,17 +158,21 @@ export async function acceptInvite(
.delete(userInvites)
.where(eq(userInvites.inviteId, inviteId));
await calculateUserClientsForOrgs(existingUser[0].userId, trx);
// Get the total number of users in the org now
totalUsers = await db
totalUsers = await trx
.select()
.from(userOrgs)
.where(eq(userOrgs.orgId, existingInvite.orgId));
await calculateUserClientsForOrgs(existingUser[0].userId, trx);
logger.debug(
`User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}. Total users in org: ${totalUsers.length}`
);
});
if (totalUsers) {
await usageService.updateDaily(
await usageService.updateCount(
existingInvite.orgId,
FeatureId.USERS,
totalUsers.length

View File

@@ -13,20 +13,16 @@ import { generateId } from "@server/auth/sessions/app";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
import { build } from "@server/build";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
});
const bodySchema = z.strictObject({
email: z
.string()
.email()
.toLowerCase()
.optional(),
email: z.string().email().toLowerCase().optional(),
username: z.string().nonempty().toLowerCase(),
name: z.string().optional(),
type: z.enum(["internal", "oidc"]).optional(),
@@ -95,7 +91,7 @@ export async function createOrgUser(
}
const rejectUsers = await usageService.checkLimitSet(
orgId,
false,
FeatureId.USERS,
{
...usage,
@@ -132,8 +128,10 @@ export async function createOrgUser(
);
} else if (type === "oidc") {
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
const subscribed = await isSubscribed(
orgId,
tierMatrix.orgOidc
);
if (!subscribed) {
return next(
createHttpError(
@@ -256,7 +254,7 @@ export async function createOrgUser(
});
if (orgUsers) {
await usageService.updateDaily(
await usageService.updateCount(
orgId,
FeatureId.USERS,
orgUsers.length

View File

@@ -133,7 +133,6 @@ export async function inviteUser(
}
const rejectUsers = await usageService.checkLimitSet(
orgId,
false,
FeatureId.USERS,
{
...usage,

View File

@@ -140,7 +140,7 @@ export async function removeUserOrg(
});
if (userCount) {
await usageService.updateDaily(
await usageService.updateCount(
orgId,
FeatureId.USERS,
userCount.length