Compare commits

...

63 Commits

Author SHA1 Message Date
Owen Schwartz 6e271028f3 Merge pull request #3245 from fosrl/dev
Bugfixes
2026-06-11 16:17:41 -07:00
Owen 820f66e58f Properly hide things with disable enterprise flag 2026-06-11 16:10:29 -07:00
Owen b0fdc10e06 Properly hide things with disable enterprise flag 2026-06-11 16:01:32 -07:00
miloschwartz b82b41ed26 fix migration 2026-06-11 15:02:29 -07:00
miloschwartz 3e977ba00d make paid alert position more consistent on resource 2026-06-11 12:38:08 -07:00
Owen Schwartz a724b07846 Merge pull request #3244 from fosrl/dev
fix paywalling
2026-06-11 12:27:49 -07:00
Owen 5f0bc71bcd Merge branch 'main' into dev 2026-06-11 12:26:31 -07:00
miloschwartz aea7827c1a fix paywalling 2026-06-11 12:26:01 -07:00
Owen Schwartz d865c4c55b Merge pull request #3242 from fosrl/dev
Use ssh like mode host
2026-06-11 11:29:45 -07:00
Owen 5baf0c3c09 Use ssh like mode host 2026-06-11 11:11:50 -07:00
Owen Schwartz cfe33eb974 Merge pull request #3241 from fosrl/dev
dev
2026-06-10 21:47:44 -07:00
Owen 71273e1b1c Try to fix large query problem 2026-06-10 21:41:34 -07:00
Owen 02f6e2a8c3 Add ; fix lint 2026-06-10 20:56:26 -07:00
Owen Schwartz 3cc244a1d3 Merge pull request #3240 from fosrl/dev
Fix small bugs with paid features, ui, docs
2026-06-10 20:49:59 -07:00
Owen 1d9c4dd9e2 Fix padding 2026-06-10 20:46:53 -07:00
Owen b9dd0c8e43 Add advantech install link 2026-06-10 20:46:43 -07:00
Owen cd052976eb Properly paywall the edit policy screen 2026-06-10 20:38:59 -07:00
Owen cc498f0e33 Properly paywall ui for labels 2026-06-10 20:32:07 -07:00
Owen 1a942937e6 Remove precheck on websocket for now 2026-06-10 20:24:41 -07:00
Owen d81d1a6b7f Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-06-10 20:24:22 -07:00
Owen f64d04e827 Add loading back to create resource 2026-06-10 18:23:01 -07:00
miloschwartz 540aee3fe2 update docs links 2026-06-10 17:52:42 -07:00
Owen Schwartz 10542d7282 Merge pull request #3239 from fosrl/dev
1.19.0
2026-06-10 16:50:32 -07:00
Owen b1d52ad1a3 Update tiers 2026-06-10 16:27:25 -07:00
Owen ce2fbef805 Filter only newt sites in the browser gateway 2026-06-10 16:16:42 -07:00
Owen e312b31e02 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-06-10 15:50:52 -07:00
Owen bc156c715d 24 hour requirement for updates 2026-06-10 15:50:43 -07:00
miloschwartz 9a4c1f23c6 support remove server admin 2026-06-10 15:10:58 -07:00
miloschwartz 6921447fab fix typo 2026-06-10 11:55:20 -07:00
Owen d47449b082 Add notes about inline policy to api endpoints 2026-06-10 10:24:31 -07:00
Owen 665806dfe8 Add some documentation; pull the override values 2026-06-10 10:03:16 -07:00
Owen e248571268 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-06-09 22:02:24 -07:00
miloschwartz fcf03854ff fix tag input wrapping 2026-06-09 22:01:13 -07:00
Owen dd1fba4e45 Also clear the roles and users 2026-06-09 21:59:30 -07:00
miloschwartz a1ab8d8f35 standardize client titles 2026-06-09 21:47:15 -07:00
miloschwartz c789e967db standardize client titles 2026-06-09 21:36:54 -07:00
Owen d870b9ff49 Drop the not null on resource columns 2026-06-09 21:36:27 -07:00
miloschwartz 9c09019ddb add protocol filter 2026-06-09 21:33:56 -07:00
Owen 9d88683fc5 Reset resource info when on inline policy 2026-06-09 21:28:25 -07:00
miloschwartz dd2c9f2a02 check resource policy in verifyResourceAccess middleware 2026-06-09 17:52:31 -07:00
miloschwartz bdb38db5bc fix form responsiveness 2026-06-09 16:52:18 -07:00
Owen 96a54fc9cc Fix import issue in migrations 2026-06-09 16:51:55 -07:00
Owen 3a485f74f1 Move session migration out of the loop 2026-06-09 16:16:14 -07:00
Owen 92b0340324 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-06-09 16:10:35 -07:00
miloschwartz 9257ac01c7 add learn more links to auto update 2026-06-09 16:08:07 -07:00
Owen 4d1d0d9fcb Add warning if we cant reach the vnc server 2026-06-09 16:02:52 -07:00
Owen f186e7e99e Dont allow asn or country without having maxmind 2026-06-09 16:02:52 -07:00
miloschwartz 1aa6e3511f dont show policy on tcp/udp resources 2026-06-09 15:56:24 -07:00
miloschwartz fb6f5b3953 form layout improvements 2026-06-09 15:42:28 -07:00
Owen c85a7f6ac5 Migrate unkown openapi response from string to {} 2026-06-09 15:35:08 -07:00
Owen dd54be523f Dont need to check user exists for the whitelist 2026-06-09 15:26:35 -07:00
Owen d57f064d4c Fix spelling 2026-06-09 15:26:35 -07:00
miloschwartz 34799b7de2 move maintenance page config to tab 2026-06-09 15:07:55 -07:00
miloschwartz 20a66bba6f fix resource context updating problem 2026-06-09 14:49:57 -07:00
miloschwartz cdb43d9658 dont set whitelist until click set button in dialog 2026-06-09 14:36:50 -07:00
miloschwartz 6581ccafa3 fix toggle on pin or passcode not working on policy form 2026-06-09 14:34:58 -07:00
miloschwartz a3a45b4239 add safe read 2026-06-09 14:09:36 -07:00
Owen d6634b6e8a Add types 2026-06-09 12:16:00 -07:00
Owen 1089cfbacc Update query to be more efficient 2026-06-09 11:54:46 -07:00
Owen 1907a3c93b Link to primary org only when you can see billing 2026-06-09 10:33:42 -07:00
Owen Schwartz 7fa1180d10 Merge pull request #3221 from fosrl/dev
1.19.0-rc.1
2026-06-04 15:45:27 -07:00
Owen Schwartz 8b50f1fb65 Merge pull request #3218 from fosrl/dev
Fix installer
2026-06-04 11:21:59 -07:00
Owen Schwartz 527d4cc777 Merge pull request #3215 from fosrl/dev
1.19.0-rc.0
2026-06-04 10:34:20 -07:00
203 changed files with 3152 additions and 2387 deletions
+2 -1
View File
@@ -34,4 +34,5 @@ build.ts
tsconfig.json
Dockerfile*
drizzle.config.ts
allowedDevOrigins.json
allowedDevOrigins.json
scratch/
+41 -7
View File
@@ -4,19 +4,26 @@ import { eq } from "drizzle-orm";
type SetServerAdminArgs = {
email: string;
remove: boolean;
};
export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = {
command: "set-server-admin",
describe: "Mark any user as a server admin by email address",
describe: "Add or remove server admin by email address",
builder: (yargs) => {
return yargs.option("email", {
type: "string",
demandOption: true,
describe: "User email address"
});
return yargs
.option("email", {
type: "string",
demandOption: true,
describe: "User email address"
})
.option("remove", {
type: "boolean",
default: false,
describe: "Remove server admin status from the user"
});
},
handler: async (argv: { email: string }) => {
handler: async (argv: SetServerAdminArgs) => {
try {
const email = argv.email.trim().toLowerCase();
@@ -31,6 +38,33 @@ export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = {
process.exit(1);
}
if (argv.remove) {
if (!user.serverAdmin) {
console.log(`User '${email}' is not a server admin`);
process.exit(0);
}
const serverAdmins = await db
.select()
.from(users)
.where(eq(users.serverAdmin, true));
if (serverAdmins.length <= 1) {
console.error(
"Cannot remove server admin: at least one server admin must exist"
);
process.exit(1);
}
await db
.update(users)
.set({ serverAdmin: false })
.where(eq(users.userId, user.userId));
console.log(`Server admin status removed from user '${email}'`);
process.exit(0);
}
if (user.serverAdmin) {
console.log(`User '${email}' is already a server admin`);
process.exit(0);
+26 -13
View File
@@ -214,6 +214,7 @@
"resourceErrorDelte": "Error deleting resource",
"resourcePoliciesBannerTitle": "Re-use Authentication and Access Rules",
"resourcePoliciesBannerDescription": "Shared resource policies let you define authentication methods and access rules once, then attach them to multiple public resources. When you update a policy, every linked resource inherits the change automatically.",
"resourcePoliciesBannerButtonText": "Learn More",
"resourcePoliciesTitle": "Manage Public Resource Policies",
"resourcePoliciesAttachedResourcesColumnTitle": "Resources",
"resourcePoliciesAttachedResources": "{count} resource(s)",
@@ -280,7 +281,7 @@
"back": "Back",
"cancel": "Cancel",
"resourceConfig": "Configuration Snippets",
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource.",
"resourceAddEntrypoints": "Traefik: Add Entrypoints",
"resourceExposePorts": "Gerbil: Expose Ports in Docker Compose",
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
@@ -727,7 +728,7 @@
"targetSubmit": "Add Target",
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to the backend.",
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
"targetsSubmit": "Save Targets",
"targetsSubmit": "Save Settings",
"addTarget": "Add Target",
"proxyMultiSiteRoundRobinNodeHelp": "Round robin routing will not work between sites that are not connected to the same node, but failover will work.",
"targetErrorInvalidIp": "Invalid IP address",
@@ -982,7 +983,9 @@
"resourcePolicySharedDescription": "This resource uses a shared policy.",
"sharedPolicy": "Shared Policy",
"sharedPolicyNoneDescription": "This resource has its own policy.",
"resourceSharedPolicyAuthenticationNotice": "This resource is using a shared policy. Some authentication settings can be edited on this resource. To change the underlying policy, you must edit to <policyLink>{policyName}</policyLink>.",
"resourceSharedPolicyOwnDescription": "This resource has its own authentication and access rules controls.",
"resourceSharedPolicyInheritedDescription": "This resource inherits from <policyLink>{policyName}</policyLink>.",
"resourceSharedPolicyAuthenticationNotice": "This resource is using a shared policy. Some authentication settings can be edited on this resource to add to the policy. To change the underlying policy, you must edit to <policyLink>{policyName}</policyLink>.",
"resourceSharedPolicyRulesNotice": "This resource is using a shared policy. Some access rules can be edited on this resource. To change the underlying policy, you must edit <policyLink>{policyName}</policyLink>.",
"resourceUsersRoles": "Access Controls",
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
@@ -1008,7 +1011,14 @@
"resourceVisibilityTitle": "Visibility",
"resourceVisibilityTitleDescription": "Completely enable or disable resource visibility",
"resourceGeneral": "General Settings",
"resourceGeneralDescription": "Configure the general settings for this resource",
"resourceGeneralDescription": "Configure name, address, and access policy for this resource.",
"resourceGeneralDetailsSubsection": "Resource Details",
"resourceGeneralDetailsSubsectionDescription": "Set the display name, identifier, and publicly accessible domain for this resource.",
"resourceGeneralDetailsSubsectionPortDescription": "Set the display name, identifier, and public port for this resource.",
"resourceGeneralPublicAddressSubsection": "Public Address",
"resourceGeneralPublicAddressSubsectionDescription": "Configure how users reach this resource.",
"resourceGeneralAuthenticationAccessSubsection": "Authentication & Access",
"resourceGeneralAuthenticationAccessSubsectionDescription": "Choose whether this resource uses its own policy or inherits from a shared policy.",
"resourceEnable": "Enable Resource",
"resourceTransfer": "Transfer Resource",
"resourceTransferDescription": "Transfer this resource to a different site",
@@ -1734,10 +1744,10 @@
"enableDockerSocket": "Enable Docker Blueprint",
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to the site connector. Read about how this works in <docsLink>the documentation</docsLink>.",
"newtAutoUpdate": "Enable Site Auto-Update",
"newtAutoUpdateDescription": "When enabled, site connectors will automatically update to the latest version when a new release is available.",
"newtAutoUpdateDescription": "When enabled, site connectors will automatically download the latest version and restart themselves. This can be overridden on a per-site basis.",
"siteAutoUpdate": "Site Auto-Update",
"siteAutoUpdateLabel": "Enable Auto-Update",
"siteAutoUpdateDescription": "Control whether this site's connector automatically downloads the latest version.",
"siteAutoUpdateDescription": "When enabled, this site's connector will automatically download the latest version and restart itself.",
"siteAutoUpdateOrgDefault": "Organization default: {state}",
"siteAutoUpdateOverriding": "Overriding organization setting",
"siteAutoUpdateResetToOrg": "Reset to Organization Default",
@@ -1835,9 +1845,9 @@
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
"documentation": "Documentation",
"saveAllSettings": "Save All Settings",
"saveResourceTargets": "Save Targets",
"saveResourceHttp": "Save Proxy Settings",
"saveProxyProtocol": "Save Proxy protocol settings",
"saveResourceTargets": "Save Settings",
"saveResourceHttp": "Save Settings",
"saveProxyProtocol": "Save Settings",
"settingsUpdated": "Settings updated",
"settingsUpdatedDescription": "Settings updated successfully",
"settingsErrorUpdate": "Failed to update settings",
@@ -2963,9 +2973,10 @@
"enableProxyProtocol": "Enable Proxy Protocol",
"proxyProtocolInfo": "Preserve client IP addresses for TCP backends",
"proxyProtocolVersion": "Proxy Protocol Version",
"version1": " Version 1 (Recommended)",
"version1": "Version 1 (Recommended)",
"version2": "Version 2",
"versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible. Make sure servers transport is added to dynamic config.",
"version1Description": "Text-based and widely supported. Make sure servers transport is added to dynamic config.",
"version2Description": "Binary and more efficient but less compatible. Make sure servers transport is added to dynamic config.",
"warning": "Warning",
"proxyProtocolWarning": "The backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections so only enable this if you know what you're doing. Make sure to configure your backend to trust Proxy Protocol headers from Traefik.",
"restarting": "Restarting...",
@@ -3171,6 +3182,8 @@
"warning:": "Warning:",
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
"pageTitle": "Page Title",
"maintenancePageContentSubsection": "Page Content",
"maintenancePageContentSubsectionDescription": "Customize the content displayed on the maintenance page",
"pageTitleDescription": "The main heading displayed on the maintenance page",
"maintenancePageMessage": "Maintenance Message",
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
@@ -3530,14 +3543,14 @@
"sshConnecting": "Connecting…",
"sshInitializing": "Initializing…",
"sshSignInTitle": "Sign in to SSH",
"sshSignInDescription": "Enter your SSH credentials",
"sshSignInDescription": "Enter your SSH credentials to connect",
"sshPasswordTab": "Password",
"sshPrivateKeyTab": "Private Key",
"sshPrivateKeyField": "Private Key",
"sshPrivateKeyDisclaimer": "Your private key is not stored or visible to Pangolin. Alternatively, you can use short-lived certificates for seamless authentication using your existing Pangolin identity.",
"sshLearnMore": "Learn more",
"sshPrivateKeyFile": "Private Key File",
"sshAuthenticate": "Authenticate",
"sshAuthenticate": "Connect",
"sshTerminate": "Terminate",
"sshPoweredBy": "Powered by",
"sshErrorNoTarget": "No target specified",
+1 -1
View File
@@ -87,7 +87,7 @@ function createDb() {
export const db = createDb();
export default db;
export const primaryDb = db.$primary as typeof db; // is this typeof a problem - techincally they are different types
export const primaryDb = db.$primary as typeof db; // is this typeof a problem - technically they are different types
export type Transaction = Parameters<
Parameters<(typeof db)["transaction"]>[0]
>[0];
+3 -4
View File
@@ -2,7 +2,7 @@ import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
import { readConfigFile } from "@server/lib/readConfigFile";
import { withReplicas } from "drizzle-orm/pg-core";
import { build } from "@server/build";
import { db as mainDb, primaryDb as mainPrimaryDb } from "./driver";
import { db as mainDb } from "./driver";
import { createPool } from "./poolConfig";
function createLogsDb() {
@@ -63,8 +63,7 @@ function createLogsDb() {
})
);
} else {
const maxReplicaConnections =
poolConfig?.max_replica_connections || 20;
const maxReplicaConnections = poolConfig?.max_replica_connections || 20;
for (const conn of replicaConnections) {
const replicaPool = createPool(
conn.connection_string,
@@ -91,4 +90,4 @@ function createLogsDb() {
export const logsDb = createLogsDb();
export default logsDb;
export const primaryLogsDb = logsDb.$primary;
export const primaryLogsDb = logsDb.$primary;
+3 -4
View File
@@ -1,5 +1,4 @@
import { Pool, PoolConfig } from "pg";
import logger from "@server/logger";
export function createPoolConfig(
connectionString: string,
@@ -27,7 +26,7 @@ export function attachPoolErrorHandlers(pool: Pool, label: string): void {
pool.on("error", (err) => {
// This catches errors on idle clients in the pool. Without this
// handler an unexpected disconnect would crash the process.
logger.error(
console.error(
`Unexpected error on idle ${label} database client: ${err.message}`
);
});
@@ -36,7 +35,7 @@ export function attachPoolErrorHandlers(pool: Pool, label: string): void {
// Set a statement timeout on every new connection so a single slow
// query can't block the pool forever
client.query("SET statement_timeout = '30s'").catch((err: Error) => {
logger.warn(
console.warn(
`Failed to set statement_timeout on ${label} client: ${err.message}`
);
});
@@ -60,4 +59,4 @@ export function createPool(
);
attachPoolErrorHandlers(pool, label);
return pool;
}
}
+3 -5
View File
@@ -147,12 +147,10 @@ export const resources = pgTable("resources", {
}),
ssl: boolean("ssl").notNull().default(false),
blockAccess: boolean("blockAccess").notNull().default(false),
sso: boolean("sso").notNull().default(true),
proxyPort: integer("proxyPort"),
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
.notNull()
.default(false),
applyRules: boolean("applyRules").notNull().default(false),
sso: boolean("sso"),
emailWhitelistEnabled: boolean("emailWhitelistEnabled"),
applyRules: boolean("applyRules"),
enabled: boolean("enabled").notNull().default(true),
stickySession: boolean("stickySession").notNull().default(false),
tlsServerName: varchar("tlsServerName"),
+3 -3
View File
@@ -45,9 +45,9 @@ export type ResourceWithAuth = {
password: ResourcePassword | ResourcePolicyPassword | null;
headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null;
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
applyRules: boolean;
sso: boolean;
emailWhitelistEnabled: boolean;
applyRules: boolean | null;
sso: boolean | null;
emailWhitelistEnabled: boolean | null;
org: Org;
};
+5 -7
View File
@@ -165,14 +165,12 @@ export const resources = sqliteTable("resources", {
blockAccess: integer("blockAccess", { mode: "boolean" })
.notNull()
.default(false),
sso: integer("sso", { mode: "boolean" }).notNull().default(true),
proxyPort: integer("proxyPort"),
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
.notNull()
.default(false),
applyRules: integer("applyRules", { mode: "boolean" })
.notNull()
.default(false),
sso: integer("sso", { mode: "boolean" }),
emailWhitelistEnabled: integer("emailWhitelistEnabled", {
mode: "boolean"
}),
applyRules: integer("applyRules", { mode: "boolean" }),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
stickySession: integer("stickySession", { mode: "boolean" })
.notNull()
+3 -1
View File
@@ -157,7 +157,9 @@ function getOpenApiDocumentation() {
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z
.record(z.string(), z.any())
.nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+3 -13
View File
@@ -31,7 +31,7 @@ export enum TierFeature {
}
export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.Labels]: ["tier2", "tier3", "enterprise"],
[TierFeature.Labels]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
@@ -71,16 +71,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"],
[TierFeature.AdvancedPublicResources]: [
"tier1",
"tier2",
"tier3",
"enterprise"
],
[TierFeature.AdvancedPrivateResources]: [
"tier1",
"tier2",
"tier3",
"enterprise"
]
[TierFeature.AdvancedPublicResources]: ["tier3", "enterprise"],
[TierFeature.AdvancedPrivateResources]: ["tier3", "enterprise"]
};
+5 -1
View File
@@ -415,7 +415,11 @@ export async function updatePrivateResources(
} else {
let aliasAddress: string | null = null;
let releaseAliasLock: (() => Promise<void>) | null = null;
if (resourceData.mode === "host" || resourceData.mode === "http") {
if (
resourceData.mode === "host" ||
resourceData.mode === "http" ||
resourceData.mode === "ssh"
) {
const { value, release } = await getNextAvailableAliasAddress(
orgId,
trx
-11
View File
@@ -1467,17 +1467,6 @@ async function syncWhitelistUsers(
.where(eq(resourceWhitelist.resourceId, resourceId));
for (const email of whitelistUsers) {
const [user] = await trx
.select()
.from(users)
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
.where(and(eq(users.email, email), eq(userOrgs.orgId, orgId)))
.limit(1);
if (!user) {
throw new Error(`User not found: ${email} in org ${orgId}`);
}
const existingWhitelistEntry = existingWhitelist.find(
(w) => w.email === email
);
+23 -2
View File
@@ -1,8 +1,23 @@
import { z } from "zod";
import { existsSync } from "node:fs";
import { portRangeStringSchema } from "@server/lib/ip";
import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema";
import { isValidRegionId } from "@server/db/regions";
import { wildcardSubdomainSchema } from "@server/lib/schemas";
import config from "@server/lib/config";
const maxmindDbPath = config.getRawConfig().server.maxmind_db_path;
const maxmindAsnPath = config.getRawConfig().server.maxmind_asn_path;
const hasMaxmindCountryDb =
typeof maxmindDbPath === "string" &&
maxmindDbPath.length > 0 &&
existsSync(maxmindDbPath);
const hasMaxmindAsnDb =
typeof maxmindAsnPath === "string" &&
maxmindAsnPath.length > 0 &&
existsSync(maxmindAsnPath);
export const SiteSchema = z.object({
name: z.string().min(1).max(100),
@@ -117,6 +132,9 @@ export const RuleSchema = z
.refine(
(rule) => {
if (rule.match === "country") {
if (!hasMaxmindCountryDb) {
return false;
}
// Check if it's a valid 2-letter country code or "ALL"
return /^[A-Z]{2}$/.test(rule.value) || rule.value === "ALL";
}
@@ -125,12 +143,15 @@ export const RuleSchema = z
{
path: ["value"],
message:
"Value must be a 2-letter country code or 'ALL' when match is 'country'"
"Country rules require a valid existing server.maxmind_db_path and value must be a 2-letter country code or 'ALL'"
}
)
.refine(
(rule) => {
if (rule.match === "asn") {
if (!hasMaxmindCountryDb || !hasMaxmindAsnDb) {
return false;
}
// Check if it's either AS<number> format or "ALL"
const asNumberPattern = /^AS\d+$/i;
return asNumberPattern.test(rule.value) || rule.value === "ALL";
@@ -140,7 +161,7 @@ export const RuleSchema = z
{
path: ["value"],
message:
"Value must be 'AS<number>' format or 'ALL' when match is 'asn'"
"ASN rules require valid existing server.maxmind_db_path and server.maxmind_asn_path, and value must be 'AS<number>' format or 'ALL'"
}
)
.refine(
+14 -5
View File
@@ -504,7 +504,7 @@ export function generateRemoteSubnets(
const parseResult = cidrSchema.safeParse(sr.destination);
return parseResult.success;
}
if (sr.mode === "host") {
if (sr.mode === "host" || sr.mode === "ssh") {
// check if its a valid IP using zod
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
const parseResult = ipSchema.safeParse(sr.destination);
@@ -514,7 +514,7 @@ export function generateRemoteSubnets(
})
.map((sr) => {
if (sr.mode === "cidr") return sr.destination;
if (sr.mode === "host") {
if (sr.mode === "host" || sr.mode === "ssh") {
return `${sr.destination}/32`;
}
return ""; // This should never be reached due to filtering, but satisfies TypeScript
@@ -531,7 +531,7 @@ export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
.filter(
(sr) =>
sr.aliasAddress &&
((sr.alias && sr.mode == "host") ||
((sr.alias && (sr.mode == "host" || sr.mode == "ssh")) ||
(sr.fullDomain && sr.mode == "http"))
)
.map((sr) => ({
@@ -577,6 +577,10 @@ export function generateSubnetProxyTargets(
continue;
}
if (!siteResource.destination) {
continue;
}
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
const portRange = [
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
@@ -584,7 +588,7 @@ export function generateSubnetProxyTargets(
];
const disableIcmp = siteResource.disableIcmp ?? false;
if (siteResource.mode == "host") {
if (siteResource.mode == "host" || siteResource.mode == "ssh") {
let destination = siteResource.destination;
// check if this is a valid ip
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
@@ -665,6 +669,11 @@ export async function generateSubnetProxyTargetV2(
return;
}
if (!siteResource.destination) {
// ssh can have no destination
return;
}
const targets: SubnetProxyTargetV2[] = [];
const portRange = [
@@ -673,7 +682,7 @@ export async function generateSubnetProxyTargetV2(
];
const disableIcmp = siteResource.disableIcmp ?? false;
if (siteResource.mode == "host") {
if (siteResource.mode == "host" || siteResource.mode == "ssh") {
let destination = siteResource.destination;
// check if this is a valid ip
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
+4
View File
@@ -181,6 +181,7 @@ class TelemetryClient {
let numPrivResourceHosts = 0;
let numPrivResourceCidr = 0;
let numPrivResourceHttp = 0;
let numPrivResourceSsh = 0;
for (const res of allPrivateResources) {
if (res.mode === "host") {
numPrivResourceHosts += 1;
@@ -188,6 +189,8 @@ class TelemetryClient {
numPrivResourceCidr += 1;
} else if (res.mode === "http") {
numPrivResourceHttp += 1;
} else if (res.mode === "ssh") {
numPrivResourceSsh += 1;
}
if (res.alias) {
@@ -207,6 +210,7 @@ class TelemetryClient {
numPrivateResourceHosts: numPrivResourceHosts,
numPrivateResourceCidr: numPrivResourceCidr,
numPrivateResourceHttp: numPrivResourceHttp,
numPrivateResourceSsh: numPrivResourceSsh,
numAlertRules: numAlertRules.count,
numUserDevices: userDevicesCount.count,
numMachineClients: machineClients.count,
+17 -28
View File
@@ -1,11 +1,15 @@
import { Request, Response, NextFunction } from "express";
import { db, Resource } from "@server/db";
import { resources, userOrgs, userResources, roleResources } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import { resources, userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import {
getRoleResourceAccess,
getUserResourceAccess
} from "@server/db/queries/verifySessionQueries";
export async function verifyResourceAccess(
req: Request,
@@ -116,37 +120,22 @@ export async function verifyResourceAccess(
const roleResourceAccess =
(req.userOrgRoleIds?.length ?? 0) > 0
? await db
.select()
.from(roleResources)
.where(
and(
eq(roleResources.resourceId, resource.resourceId),
inArray(
roleResources.roleId,
req.userOrgRoleIds!
)
)
)
.limit(1)
: [];
? await getRoleResourceAccess(
resource.resourceId,
req.userOrgRoleIds!
)
: null;
if (roleResourceAccess.length > 0) {
if (roleResourceAccess) {
return next();
}
const userResourceAccess = await db
.select()
.from(userResources)
.where(
and(
eq(userResources.userId, userId),
eq(userResources.resourceId, resource.resourceId)
)
)
.limit(1);
const userResourceAccess = await getUserResourceAccess(
userId,
resource.resourceId
);
if (userResourceAccess.length > 0) {
if (userResourceAccess) {
return next();
}
@@ -208,7 +208,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -44,7 +44,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -112,4 +112,4 @@ export async function deleteAlertRule(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
}
@@ -32,7 +32,10 @@ import { OpenAPITags, registry } from "@server/openApi";
import { and, eq } from "drizzle-orm";
import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { GetAlertRuleResponse, WebhookAlertConfig } from "@server/routers/alertRule/types";
import {
GetAlertRuleResponse,
WebhookAlertConfig
} from "@server/routers/alertRule/types";
const paramsSchema = z
.object({
@@ -55,7 +58,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -101,7 +101,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -44,7 +44,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -44,7 +44,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -44,7 +44,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -72,7 +72,9 @@ export async function exportConnectionAuditLogs(
);
}
const parsedParams = queryConnectionAuditLogsParams.safeParse(req.params);
const parsedParams = queryConnectionAuditLogsParams.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
@@ -112,4 +114,4 @@ export async function exportConnectionAuditLogs(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
}
@@ -11,7 +11,14 @@
* This file is not licensed under the AGPLv3.
*/
import { accessAuditLog, logsDb, resources, siteResources, db, primaryDb } from "@server/db";
import {
accessAuditLog,
logsDb,
resources,
siteResources,
db,
primaryDb
} from "@server/db";
import { registry } from "@server/openApi";
import { NextFunction } from "express";
import { Request, Response } from "express";
@@ -150,21 +157,30 @@ export function queryAccess(data: Q) {
.orderBy(desc(accessAuditLog.timestamp), desc(accessAuditLog.id));
}
async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryAccess>>) {
async function enrichWithResourceDetails(
logs: Awaited<ReturnType<typeof queryAccess>>
) {
const resourceIds = logs
.map(log => log.resourceId)
.map((log) => log.resourceId)
.filter((id): id is number => id !== null && id !== undefined);
const siteResourceIds = logs
.filter(log => log.resourceId == null && log.siteResourceId != null)
.map(log => log.siteResourceId)
.filter((log) => log.resourceId == null && log.siteResourceId != null)
.map((log) => log.siteResourceId)
.filter((id): id is number => id !== null && id !== undefined);
if (resourceIds.length === 0 && siteResourceIds.length === 0) {
return logs.map(log => ({ ...log, resourceName: null, resourceNiceId: null }));
return logs.map((log) => ({
...log,
resourceName: null,
resourceNiceId: null
}));
}
const resourceMap = new Map<number, { name: string | null; niceId: string | null }>();
const resourceMap = new Map<
number,
{ name: string | null; niceId: string | null }
>();
if (resourceIds.length > 0) {
const resourceDetails = await primaryDb
@@ -181,7 +197,10 @@ async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryAc
}
}
const siteResourceMap = new Map<number, { name: string | null; niceId: string | null }>();
const siteResourceMap = new Map<
number,
{ name: string | null; niceId: string | null }
>();
if (siteResourceIds.length > 0) {
const siteResourceDetails = await primaryDb
@@ -194,12 +213,15 @@ async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryAc
.where(inArray(siteResources.siteResourceId, siteResourceIds));
for (const r of siteResourceDetails) {
siteResourceMap.set(r.siteResourceId, { name: r.name, niceId: r.niceId });
siteResourceMap.set(r.siteResourceId, {
name: r.name,
niceId: r.niceId
});
}
}
// Enrich logs with resource details
return logs.map(log => {
return logs.map((log) => {
if (log.resourceId != null) {
const details = resourceMap.get(log.resourceId);
return {
@@ -273,11 +295,11 @@ async function queryUniqueFilterAttributes(
// Fetch resource names from main database for the unique resource IDs
const resourceIds = uniqueResources
.map(row => row.id)
.map((row) => row.id)
.filter((id): id is number => id !== null);
const siteResourceIds = uniqueSiteResources
.map(row => row.id)
.map((row) => row.id)
.filter((id): id is number => id !== null);
let resourcesWithNames: Array<{ id: number; name: string | null }> = [];
@@ -293,7 +315,7 @@ async function queryUniqueFilterAttributes(
resourcesWithNames = [
...resourcesWithNames,
...resourceDetails.map(r => ({
...resourceDetails.map((r) => ({
id: r.resourceId,
name: r.name
}))
@@ -311,7 +333,7 @@ async function queryUniqueFilterAttributes(
resourcesWithNames = [
...resourcesWithNames,
...siteResourceDetails.map(r => ({
...siteResourceDetails.map((r) => ({
id: r.siteResourceId,
name: r.name
}))
@@ -344,7 +366,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -171,7 +171,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -459,7 +459,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+14 -11
View File
@@ -17,8 +17,7 @@ import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { sessions, sessionTransferToken } from "@server/db";
import { db } from "@server/db";
import { db, safeRead, sessions, sessionTransferToken } from "@server/db";
import { eq } from "drizzle-orm";
import { response } from "@server/lib/response";
import { encodeHexLowerCase } from "@oslojs/encoding";
@@ -57,15 +56,19 @@ export async function transferSession(
sha256(new TextEncoder().encode(token))
);
const [existing] = await db
.select()
.from(sessionTransferToken)
.where(eq(sessionTransferToken.token, tokenRaw))
.innerJoin(
sessions,
eq(sessions.sessionId, sessionTransferToken.sessionId)
)
.limit(1);
const result = await safeRead((db) =>
db
.select()
.from(sessionTransferToken)
.where(eq(sessionTransferToken.token, tokenRaw))
.innerJoin(
sessions,
eq(sessions.sessionId, sessionTransferToken.sessionId)
)
.limit(1)
);
const [existing] = result;
if (!existing) {
return next(
@@ -45,7 +45,7 @@ const getOrgSchema = z.strictObject({
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// data: z.record(z.string(), z.any()).nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
@@ -121,7 +121,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -46,7 +46,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -29,7 +29,7 @@ import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z.strictObject({});
const querySchema = z.strictObject({
subdomain: z.string(),
subdomain: z.string()
// orgId: build === "saas" ? z.string() : z.string().optional() // Required for saas, optional otherwise
});
@@ -48,7 +48,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -33,7 +33,8 @@ const paramsSchema = z
registry.registerPath({
method: "delete",
path: "/org/{orgId}/event-streaming-destination/{destinationId}",
description: "Delete an event streaming destination for a specific organization.",
description:
"Delete an event streaming destination for a specific organization.",
tags: [OpenAPITags.Org],
request: {
params: paramsSchema
@@ -44,7 +45,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -115,4 +116,4 @@ export async function deleteEventStreamingDestination(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
}
@@ -47,7 +47,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -74,7 +74,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+3 -3
View File
@@ -219,9 +219,9 @@ export type ResourceWithAuth = {
password: ResourcePassword | ResourcePolicyPassword | null;
headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null;
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
applyRules: boolean;
sso: boolean;
emailWhitelistEnabled: boolean;
applyRules: boolean | null;
sso: boolean | null;
emailWhitelistEnabled: boolean | null;
org: Org;
};
@@ -69,7 +69,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -127,7 +127,8 @@ export async function createOrgOidcIdp(
let { autoProvision } = parsedBody.data;
if (build == "saas") { // this is not paywalled with a ee license because this whole endpoint is restricted
if (build == "saas") {
// this is not paywalled with a ee license because this whole endpoint is restricted
const subscribed = await isSubscribed(
orgId,
tierMatrix.deviceApprovals
@@ -44,7 +44,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -62,7 +62,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -78,7 +78,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -44,7 +44,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -45,7 +45,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -28,7 +28,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -61,7 +61,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -135,7 +135,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -164,7 +164,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -28,7 +28,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -42,7 +42,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -35,7 +35,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -162,7 +162,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -1,4 +1,11 @@
import { logsDb, requestAuditLog, resources, siteResources, db, primaryDb } from "@server/db";
import {
logsDb,
requestAuditLog,
resources,
siteResources,
db,
primaryDb
} from "@server/db";
import { registry } from "@server/openApi";
import { NextFunction } from "express";
import { Request, Response } from "express";
@@ -127,16 +134,16 @@ export function queryRequest(data: Q) {
return logsDb
.select({
id: requestAuditLog.id,
timestamp: requestAuditLog.timestamp,
orgId: requestAuditLog.orgId,
action: requestAuditLog.action,
reason: requestAuditLog.reason,
actorType: requestAuditLog.actorType,
actor: requestAuditLog.actor,
actorId: requestAuditLog.actorId,
resourceId: requestAuditLog.resourceId,
siteResourceId: requestAuditLog.siteResourceId,
ip: requestAuditLog.ip,
timestamp: requestAuditLog.timestamp,
orgId: requestAuditLog.orgId,
action: requestAuditLog.action,
reason: requestAuditLog.reason,
actorType: requestAuditLog.actorType,
actor: requestAuditLog.actor,
actorId: requestAuditLog.actorId,
resourceId: requestAuditLog.resourceId,
siteResourceId: requestAuditLog.siteResourceId,
ip: requestAuditLog.ip,
location: requestAuditLog.location,
userAgent: requestAuditLog.userAgent,
metadata: requestAuditLog.metadata,
@@ -154,21 +161,30 @@ export function queryRequest(data: Q) {
.orderBy(desc(requestAuditLog.timestamp));
}
async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryRequest>>) {
async function enrichWithResourceDetails(
logs: Awaited<ReturnType<typeof queryRequest>>
) {
const resourceIds = logs
.map(log => log.resourceId)
.map((log) => log.resourceId)
.filter((id): id is number => id !== null && id !== undefined);
const siteResourceIds = logs
.filter(log => log.resourceId == null && log.siteResourceId != null)
.map(log => log.siteResourceId)
.filter((log) => log.resourceId == null && log.siteResourceId != null)
.map((log) => log.siteResourceId)
.filter((id): id is number => id !== null && id !== undefined);
if (resourceIds.length === 0 && siteResourceIds.length === 0) {
return logs.map(log => ({ ...log, resourceName: null, resourceNiceId: null }));
return logs.map((log) => ({
...log,
resourceName: null,
resourceNiceId: null
}));
}
const resourceMap = new Map<number, { name: string | null; niceId: string | null }>();
const resourceMap = new Map<
number,
{ name: string | null; niceId: string | null }
>();
if (resourceIds.length > 0) {
const resourceDetails = await primaryDb
@@ -185,7 +201,10 @@ async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryRe
}
}
const siteResourceMap = new Map<number, { name: string | null; niceId: string | null }>();
const siteResourceMap = new Map<
number,
{ name: string | null; niceId: string | null }
>();
if (siteResourceIds.length > 0) {
const siteResourceDetails = await primaryDb
@@ -198,12 +217,15 @@ async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryRe
.where(inArray(siteResources.siteResourceId, siteResourceIds));
for (const r of siteResourceDetails) {
siteResourceMap.set(r.siteResourceId, { name: r.name, niceId: r.niceId });
siteResourceMap.set(r.siteResourceId, {
name: r.name,
niceId: r.niceId
});
}
}
// Enrich logs with resource details
return logs.map(log => {
return logs.map((log) => {
if (log.resourceId != null) {
const details = resourceMap.get(log.resourceId);
return {
@@ -247,7 +269,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -333,11 +355,11 @@ async function queryUniqueFilterAttributes(
// Fetch resource names from main database for the unique resource IDs
const resourceIds = uniqueResources
.map(row => row.id)
.map((row) => row.id)
.filter((id): id is number => id !== null);
const siteResourceIds = uniqueSiteResources
.map(row => row.id)
.map((row) => row.id)
.filter((id): id is number => id !== null);
let resourcesWithNames: Array<{ id: number; name: string | null }> = [];
@@ -353,7 +375,7 @@ async function queryUniqueFilterAttributes(
resourcesWithNames = [
...resourcesWithNames,
...resourceDetails.map(r => ({
...resourceDetails.map((r) => ({
id: r.resourceId,
name: r.name
}))
@@ -371,7 +393,7 @@ async function queryUniqueFilterAttributes(
resourcesWithNames = [
...resourcesWithNames,
...siteResourceDetails.map(r => ({
...siteResourceDetails.map((r) => ({
id: r.siteResourceId,
name: r.name
}))
+41 -40
View File
@@ -1,14 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import {
users,
userOrgs,
orgs,
idpOrg,
idp,
idpOidcConfig
} from "@server/db";
import { users, userOrgs, orgs, idpOrg, idp, idpOidcConfig } from "@server/db";
import { eq, or, sql, and, isNotNull, inArray } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -57,7 +50,7 @@ export type LookupUserResponse = {
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// data: z.record(z.string(), z.any()).nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
@@ -169,46 +162,54 @@ export async function lookupUser(
);
// Deduplicate orgs (user might have multiple memberships in same org)
const uniqueOrgs = new Map<string, typeof userOrgMemberships[0]>();
const uniqueOrgs = new Map<
string,
(typeof userOrgMemberships)[0]
>();
for (const membership of userOrgMemberships) {
if (!uniqueOrgs.has(membership.orgId)) {
uniqueOrgs.set(membership.orgId, membership);
}
}
const orgsData = Array.from(uniqueOrgs.values()).map((membership) => {
// Get IdPs for this org where the user (with the exact identifier) is authenticated via that IdP
// Only show IdPs where the user's idpId matches
// Internal users don't have an idpId, so they won't see any IdPs
const orgIdpsList = orgIdps
.filter((idp) => {
if (idp.orgId !== membership.orgId) {
const orgsData = Array.from(uniqueOrgs.values()).map(
(membership) => {
// Get IdPs for this org where the user (with the exact identifier) is authenticated via that IdP
// Only show IdPs where the user's idpId matches
// Internal users don't have an idpId, so they won't see any IdPs
const orgIdpsList = orgIdps
.filter((idp) => {
if (idp.orgId !== membership.orgId) {
return false;
}
// Only show IdPs where the user (with exact identifier) is authenticated via that IdP
// This means user.idpId must match idp.idpId
if (
user.idpId !== null &&
user.idpId === idp.idpId
) {
return true;
}
return false;
}
// Only show IdPs where the user (with exact identifier) is authenticated via that IdP
// This means user.idpId must match idp.idpId
if (user.idpId !== null && user.idpId === idp.idpId) {
return true;
}
return false;
})
.map((idp) => ({
idpId: idp.idpId,
name: idp.idpName,
variant: idp.variant
}));
})
.map((idp) => ({
idpId: idp.idpId,
name: idp.idpName,
variant: idp.variant
}));
// Check if user has internal auth for this org
// User has internal auth if they have an internal account type
const orgHasInternalAuth = hasInternalAuth;
// Check if user has internal auth for this org
// User has internal auth if they have an internal account type
const orgHasInternalAuth = hasInternalAuth;
return {
orgId: membership.orgId,
orgName: membership.orgName,
idps: orgIdpsList,
hasInternalAuth: orgHasInternalAuth
};
});
return {
orgId: membership.orgId,
orgName: membership.orgName,
idps: orgIdpsList,
hasInternalAuth: orgHasInternalAuth
};
}
);
accounts.push({
userId: user.userId,
+3 -3
View File
@@ -145,9 +145,9 @@ export async function verifyResourceSession(
| ResourcePolicyHeaderAuth
| null;
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
applyRules: boolean;
sso: boolean;
emailWhitelistEnabled: boolean;
applyRules: boolean | null;
sso: boolean | null;
emailWhitelistEnabled: boolean | null;
org: Org;
}
| undefined = localCache.get(resourceCacheKey);
@@ -37,7 +37,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -60,7 +60,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -62,7 +62,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -80,7 +80,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -28,7 +28,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+6 -2
View File
@@ -30,7 +30,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -94,7 +94,11 @@ export async function blockClient(
// Send terminate signal if there's an associated OLM and it's connected
if (client.olmId && client.online) {
await sendTerminateClient(client.clientId, OlmErrorCodes.TERMINATED_BLOCKED, client.olmId);
await sendTerminateClient(
client.clientId,
OlmErrorCodes.TERMINATED_BLOCKED,
client.olmId
);
}
});
+1 -1
View File
@@ -65,7 +65,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -66,7 +66,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -31,7 +31,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+14 -14
View File
@@ -259,7 +259,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -287,7 +287,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -340,18 +340,18 @@ export async function getClient(
// Build fingerprint data if available
const fingerprintData = client.currentFingerprint
? {
username: client.currentFingerprint.username || null,
hostname: client.currentFingerprint.hostname || null,
platform: client.currentFingerprint.platform || null,
osVersion: client.currentFingerprint.osVersion || null,
kernelVersion:
client.currentFingerprint.kernelVersion || null,
arch: client.currentFingerprint.arch || null,
deviceModel: client.currentFingerprint.deviceModel || null,
serialNumber: client.currentFingerprint.serialNumber || null,
firstSeen: client.currentFingerprint.firstSeen || null,
lastSeen: client.currentFingerprint.lastSeen || null
}
username: client.currentFingerprint.username || null,
hostname: client.currentFingerprint.hostname || null,
platform: client.currentFingerprint.platform || null,
osVersion: client.currentFingerprint.osVersion || null,
kernelVersion:
client.currentFingerprint.kernelVersion || null,
arch: client.currentFingerprint.arch || null,
deviceModel: client.currentFingerprint.deviceModel || null,
serialNumber: client.currentFingerprint.serialNumber || null,
firstSeen: client.currentFingerprint.firstSeen || null,
lastSeen: client.currentFingerprint.lastSeen || null
}
: null;
// Build posture data if available (platform-specific)
+1 -1
View File
@@ -218,7 +218,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -219,7 +219,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+3 -3
View File
@@ -13,7 +13,7 @@ import semver from "semver";
const NEWT_V2_TARGETS_VERSION = ">=1.10.3";
export async function convertTargetsIfNessicary(
export async function convertTargetsIfNecessary(
newtId: string,
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[]
) {
@@ -47,7 +47,7 @@ export async function addTargets(
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
version?: string | null
) {
targets = await convertTargetsIfNessicary(newtId, targets);
targets = await convertTargetsIfNecessary(newtId, targets);
await sendToClient(
newtId,
@@ -64,7 +64,7 @@ export async function removeTargets(
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
version?: string | null
) {
targets = await convertTargetsIfNessicary(newtId, targets);
targets = await convertTargetsIfNecessary(newtId, targets);
await sendToClient(
newtId,
+1 -1
View File
@@ -28,7 +28,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -28,7 +28,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -42,7 +42,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -43,7 +43,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -44,7 +44,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -31,7 +31,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -29,7 +29,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -44,7 +44,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+2 -4
View File
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, Org } from "@server/db";
import { db, Org, primaryDb } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -635,9 +635,7 @@ export async function validateOidcCallback(
}
});
db.transaction(async (trx) => {
await calculateUserClientsForOrgs(userId!, trx);
}).catch((err) => {
calculateUserClientsForOrgs(userId!, primaryDb).catch((err) => {
logger.error(
"Error calculating user clients after syncing orgs and roles for OIDC user",
{ error: err }
+55 -25
View File
@@ -152,34 +152,64 @@ export async function buildClientConfigurationForNewtClient(
const targetsToSend: SubnetProxyTargetV2[] = [];
for (const resource of allSiteResources) {
// Get clients associated with this specific resource
const resourceClients = await db
.select({
clientId: clients.clientId,
pubKey: clients.pubKey,
subnet: clients.subnet
})
.from(clients)
.innerJoin(
clientSiteResourcesAssociationsCache,
eq(
clients.clientId,
clientSiteResourcesAssociationsCache.clientId
)
)
.where(
eq(
clientSiteResourcesAssociationsCache.siteResourceId,
resource.siteResourceId
)
);
if (allSiteResources.length === 0) {
return {
peers: validPeers,
targets: targetsToSend
};
}
const resourceTargets = await generateSubnetProxyTargetV2(
resource,
resourceClients
// Batch fetch all client associations for every site resource in one query
// to avoid an N+1 lookup that would issue thousands of queries when a site
// has many resources.
const siteResourceIds = allSiteResources.map((r) => r.siteResourceId);
const resourceClientRows = await db
.select({
siteResourceId: clientSiteResourcesAssociationsCache.siteResourceId,
clientId: clients.clientId,
pubKey: clients.pubKey,
subnet: clients.subnet
})
.from(clients)
.innerJoin(
clientSiteResourcesAssociationsCache,
eq(clients.clientId, clientSiteResourcesAssociationsCache.clientId)
)
.where(
inArray(
clientSiteResourcesAssociationsCache.siteResourceId,
siteResourceIds
)
);
const clientsByResourceId = new Map<
number,
{ clientId: number; pubKey: string | null; subnet: string | null }[]
>();
for (const row of resourceClientRows) {
let list = clientsByResourceId.get(row.siteResourceId);
if (!list) {
list = [];
clientsByResourceId.set(row.siteResourceId, list);
}
list.push({
clientId: row.clientId,
pubKey: row.pubKey,
subnet: row.subnet
});
}
const resourceTargetsArr = await Promise.all(
allSiteResources.map((resource) =>
generateSubnetProxyTargetV2(
resource,
clientsByResourceId.get(resource.siteResourceId) ?? []
)
)
);
for (const resourceTargets of resourceTargetsArr) {
if (resourceTargets) {
targetsToSend.push(...resourceTargets);
}
+7 -2
View File
@@ -56,13 +56,18 @@ async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
return staleReleaseInfo;
}
// Drop drafts, pre-releases, and anything with "rc" in the tag name.
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
// Drop drafts, pre-releases, anything with "rc" in the tag name,
// and releases published less than 1 day ago.
releases = releases.filter(
(r: any) =>
!r.draft &&
!r.prerelease &&
!r.tag_name.includes("rc") &&
!r.tag_name.includes("v")
!r.tag_name.includes("v") &&
r.published_at &&
new Date(r.published_at) <= oneDayAgo
);
// Sort descending by semver to find the true latest stable release.
@@ -6,7 +6,7 @@ import { db, ExitNode, exitNodes, Newt, sites } from "@server/db";
import { eq } from "drizzle-orm";
import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
import { convertTargetsIfNessicary } from "../client/targets";
import { convertTargetsIfNecessary } from "../client/targets";
import { canCompress } from "@server/lib/clientVersionChecks";
import config from "@server/lib/config";
@@ -113,7 +113,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
exitNode
);
const targetsToSend = await convertTargetsIfNessicary(newt.newtId, targets); // for backward compatibility with old newt versions that don't support the new target format
const targetsToSend = await convertTargetsIfNecessary(newt.newtId, targets); // for backward compatibility with old newt versions that don't support the new target format
return {
message: {
+1 -1
View File
@@ -49,7 +49,7 @@ export type CreateOlmResponse = {
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// data: z.record(z.string(), z.any()).nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
+1 -1
View File
@@ -34,7 +34,7 @@ const paramsSchema = z
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// data: z.record(z.string(), z.any()).nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
+1 -1
View File
@@ -36,7 +36,7 @@ const querySchema = z.object({
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// data: z.record(z.string(), z.any()).nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
+1 -1
View File
@@ -47,7 +47,7 @@ const paramsSchema = z
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// data: z.record(z.string(), z.any()).nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
+2 -5
View File
@@ -49,10 +49,7 @@ async function queryUser(orgId: string, userId: string) {
.from(userOrgRoles)
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
);
const isAdmin = roleRows.some((r) => r.isAdmin);
@@ -89,7 +86,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -80,7 +80,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -30,7 +30,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -43,7 +43,7 @@ const listOrgsSchema = z.object({
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// data: z.record(z.string(), z.any()).nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
+1 -1
View File
@@ -27,7 +27,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -68,7 +68,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -46,7 +46,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+3 -2
View File
@@ -28,7 +28,8 @@ const addRoleToResourceParamsSchema = z
registry.registerPath({
method: "post",
path: "/resource/{resourceId}/roles/add",
description: "Add a single role to a resource.",
description:
"Add a single role to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the role is added to the inline policy instead of directly to the resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
request: {
params: addRoleToResourceParamsSchema,
@@ -46,7 +47,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+3 -2
View File
@@ -28,7 +28,8 @@ const addUserToResourceParamsSchema = z
registry.registerPath({
method: "post",
path: "/resource/{resourceId}/users/add",
description: "Add a single user to a resource.",
description:
"Add a single user to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the user is added to the inline policy instead of directly to the resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.User],
request: {
params: addUserToResourceParamsSchema,
@@ -46,7 +47,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -168,7 +168,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -49,7 +49,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -37,7 +37,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -29,7 +29,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+35 -7
View File
@@ -1,4 +1,4 @@
import { db, resources } from "@server/db";
import { db, resourcePolicies, resources } from "@server/db";
import response from "@server/lib/response";
import stoi from "@server/lib/stoi";
import logger from "@server/logger";
@@ -41,6 +41,15 @@ async function query(resourceId?: number, niceId?: string, orgId?: string) {
}
}
async function queryInlinePolicy(resourcePolicyId: number) {
const [res] = await db
.select()
.from(resourcePolicies)
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
.limit(1);
return res;
}
export type GetResourceResponse = Omit<
NonNullable<Awaited<ReturnType<typeof query>>>,
"headers"
@@ -66,7 +75,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -94,7 +103,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
@@ -132,12 +141,31 @@ export async function getResource(
);
}
const isInlinePolicy =
resource.resourcePolicyId === null &&
resource.defaultResourcePolicyId !== null;
let returnData = resource;
if (isInlinePolicy) {
// get the policy
const policy = await queryInlinePolicy(
resource.defaultResourcePolicyId!
);
returnData = {
...returnData,
sso: policy?.sso || null,
emailWhitelistEnabled: policy?.emailWhitelistEnabled || null,
applyRules: policy?.applyRules || null,
skipToIdpId: policy?.idpId || null
};
}
return response<GetResourceResponse>(res, {
data: {
...resource,
headers: resource.headers
? JSON.parse(resource.headers)
: resource.headers
...returnData,
headers: returnData.headers
? JSON.parse(returnData.headers)
: returnData.headers
},
success: true,
error: false,
@@ -54,7 +54,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+2 -2
View File
@@ -203,9 +203,9 @@ export async function getUserResources(
fullDomain: string | null;
ssl: boolean;
enabled: boolean;
sso: boolean;
sso: boolean | null;
mode: string;
emailWhitelistEnabled: boolean;
emailWhitelistEnabled: boolean | null;
policyEmailWhitelistEnabled: boolean | null;
}> = [];
if (uniqueResourceIds.length > 0) {
@@ -45,7 +45,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -58,7 +58,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
+1 -1
View File
@@ -82,7 +82,7 @@ registry.registerPath({
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),

Some files were not shown because too many files have changed in this diff Show More