mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-13 17:21:48 +02:00
Merge branch 'dev' into refactor/show-if-client-needs-update
This commit is contained in:
@@ -4,19 +4,26 @@ import { eq } from "drizzle-orm";
|
|||||||
|
|
||||||
type SetServerAdminArgs = {
|
type SetServerAdminArgs = {
|
||||||
email: string;
|
email: string;
|
||||||
|
remove: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = {
|
export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = {
|
||||||
command: "set-server-admin",
|
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) => {
|
builder: (yargs) => {
|
||||||
return yargs.option("email", {
|
return yargs
|
||||||
type: "string",
|
.option("email", {
|
||||||
demandOption: true,
|
type: "string",
|
||||||
describe: "User email address"
|
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 {
|
try {
|
||||||
const email = argv.email.trim().toLowerCase();
|
const email = argv.email.trim().toLowerCase();
|
||||||
|
|
||||||
@@ -31,6 +38,33 @@ export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = {
|
|||||||
process.exit(1);
|
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) {
|
if (user.serverAdmin) {
|
||||||
console.log(`User '${email}' is already a server admin`);
|
console.log(`User '${email}' is already a server admin`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
+26
-13
@@ -214,6 +214,7 @@
|
|||||||
"resourceErrorDelte": "Error deleting resource",
|
"resourceErrorDelte": "Error deleting resource",
|
||||||
"resourcePoliciesBannerTitle": "Re-use Authentication and Access Rules",
|
"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.",
|
"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",
|
"resourcePoliciesTitle": "Manage Public Resource Policies",
|
||||||
"resourcePoliciesAttachedResourcesColumnTitle": "Resources",
|
"resourcePoliciesAttachedResourcesColumnTitle": "Resources",
|
||||||
"resourcePoliciesAttachedResources": "{count} resource(s)",
|
"resourcePoliciesAttachedResources": "{count} resource(s)",
|
||||||
@@ -280,7 +281,7 @@
|
|||||||
"back": "Back",
|
"back": "Back",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"resourceConfig": "Configuration Snippets",
|
"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",
|
"resourceAddEntrypoints": "Traefik: Add Entrypoints",
|
||||||
"resourceExposePorts": "Gerbil: Expose Ports in Docker Compose",
|
"resourceExposePorts": "Gerbil: Expose Ports in Docker Compose",
|
||||||
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
|
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
|
||||||
@@ -727,7 +728,7 @@
|
|||||||
"targetSubmit": "Add Target",
|
"targetSubmit": "Add Target",
|
||||||
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to the backend.",
|
"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.",
|
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
||||||
"targetsSubmit": "Save Targets",
|
"targetsSubmit": "Save Settings",
|
||||||
"addTarget": "Add Target",
|
"addTarget": "Add Target",
|
||||||
"proxyMultiSiteRoundRobinNodeHelp": "Round robin routing will not work between sites that are not connected to the same node, but failover will work.",
|
"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",
|
"targetErrorInvalidIp": "Invalid IP address",
|
||||||
@@ -982,7 +983,9 @@
|
|||||||
"resourcePolicySharedDescription": "This resource uses a shared policy.",
|
"resourcePolicySharedDescription": "This resource uses a shared policy.",
|
||||||
"sharedPolicy": "Shared Policy",
|
"sharedPolicy": "Shared Policy",
|
||||||
"sharedPolicyNoneDescription": "This resource has its own 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 authentication and access rules controls 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>.",
|
"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",
|
"resourceUsersRoles": "Access Controls",
|
||||||
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
||||||
@@ -1008,7 +1011,14 @@
|
|||||||
"resourceVisibilityTitle": "Visibility",
|
"resourceVisibilityTitle": "Visibility",
|
||||||
"resourceVisibilityTitleDescription": "Completely enable or disable resource visibility",
|
"resourceVisibilityTitleDescription": "Completely enable or disable resource visibility",
|
||||||
"resourceGeneral": "General Settings",
|
"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",
|
"resourceEnable": "Enable Resource",
|
||||||
"resourceTransfer": "Transfer Resource",
|
"resourceTransfer": "Transfer Resource",
|
||||||
"resourceTransferDescription": "Transfer this resource to a different site",
|
"resourceTransferDescription": "Transfer this resource to a different site",
|
||||||
@@ -1734,10 +1744,10 @@
|
|||||||
"enableDockerSocket": "Enable Docker Blueprint",
|
"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>.",
|
"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",
|
"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",
|
"siteAutoUpdate": "Site Auto-Update",
|
||||||
"siteAutoUpdateLabel": "Enable 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}",
|
"siteAutoUpdateOrgDefault": "Organization default: {state}",
|
||||||
"siteAutoUpdateOverriding": "Overriding organization setting",
|
"siteAutoUpdateOverriding": "Overriding organization setting",
|
||||||
"siteAutoUpdateResetToOrg": "Reset to Organization Default",
|
"siteAutoUpdateResetToOrg": "Reset to Organization Default",
|
||||||
@@ -1835,9 +1845,9 @@
|
|||||||
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
|
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
"saveAllSettings": "Save All Settings",
|
"saveAllSettings": "Save All Settings",
|
||||||
"saveResourceTargets": "Save Targets",
|
"saveResourceTargets": "Save Settings",
|
||||||
"saveResourceHttp": "Save Proxy Settings",
|
"saveResourceHttp": "Save Settings",
|
||||||
"saveProxyProtocol": "Save Proxy protocol settings",
|
"saveProxyProtocol": "Save Settings",
|
||||||
"settingsUpdated": "Settings updated",
|
"settingsUpdated": "Settings updated",
|
||||||
"settingsUpdatedDescription": "Settings updated successfully",
|
"settingsUpdatedDescription": "Settings updated successfully",
|
||||||
"settingsErrorUpdate": "Failed to update settings",
|
"settingsErrorUpdate": "Failed to update settings",
|
||||||
@@ -2964,9 +2974,10 @@
|
|||||||
"enableProxyProtocol": "Enable Proxy Protocol",
|
"enableProxyProtocol": "Enable Proxy Protocol",
|
||||||
"proxyProtocolInfo": "Preserve client IP addresses for TCP backends",
|
"proxyProtocolInfo": "Preserve client IP addresses for TCP backends",
|
||||||
"proxyProtocolVersion": "Proxy Protocol Version",
|
"proxyProtocolVersion": "Proxy Protocol Version",
|
||||||
"version1": " Version 1 (Recommended)",
|
"version1": "Version 1 (Recommended)",
|
||||||
"version2": "Version 2",
|
"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",
|
"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.",
|
"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...",
|
"restarting": "Restarting...",
|
||||||
@@ -3172,6 +3183,8 @@
|
|||||||
"warning:": "Warning:",
|
"warning:": "Warning:",
|
||||||
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
|
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
|
||||||
"pageTitle": "Page Title",
|
"pageTitle": "Page Title",
|
||||||
|
"maintenancePageContentSubsection": "Page Content",
|
||||||
|
"maintenancePageContentSubsectionDescription": "Customize the content displayed on the maintenance page",
|
||||||
"pageTitleDescription": "The main heading displayed on the maintenance page",
|
"pageTitleDescription": "The main heading displayed on the maintenance page",
|
||||||
"maintenancePageMessage": "Maintenance Message",
|
"maintenancePageMessage": "Maintenance Message",
|
||||||
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
|
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
|
||||||
@@ -3531,14 +3544,14 @@
|
|||||||
"sshConnecting": "Connecting…",
|
"sshConnecting": "Connecting…",
|
||||||
"sshInitializing": "Initializing…",
|
"sshInitializing": "Initializing…",
|
||||||
"sshSignInTitle": "Sign in to SSH",
|
"sshSignInTitle": "Sign in to SSH",
|
||||||
"sshSignInDescription": "Enter your SSH credentials",
|
"sshSignInDescription": "Enter your SSH credentials to connect",
|
||||||
"sshPasswordTab": "Password",
|
"sshPasswordTab": "Password",
|
||||||
"sshPrivateKeyTab": "Private Key",
|
"sshPrivateKeyTab": "Private Key",
|
||||||
"sshPrivateKeyField": "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.",
|
"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",
|
"sshLearnMore": "Learn more",
|
||||||
"sshPrivateKeyFile": "Private Key File",
|
"sshPrivateKeyFile": "Private Key File",
|
||||||
"sshAuthenticate": "Authenticate",
|
"sshAuthenticate": "Connect",
|
||||||
"sshTerminate": "Terminate",
|
"sshTerminate": "Terminate",
|
||||||
"sshPoweredBy": "Powered by",
|
"sshPoweredBy": "Powered by",
|
||||||
"sshErrorNoTarget": "No target specified",
|
"sshErrorNoTarget": "No target specified",
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ function createDb() {
|
|||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
export default db;
|
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<
|
export type Transaction = Parameters<
|
||||||
Parameters<(typeof db)["transaction"]>[0]
|
Parameters<(typeof db)["transaction"]>[0]
|
||||||
>[0];
|
>[0];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
|||||||
import { readConfigFile } from "@server/lib/readConfigFile";
|
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||||
import { withReplicas } from "drizzle-orm/pg-core";
|
import { withReplicas } from "drizzle-orm/pg-core";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { db as mainDb, primaryDb as mainPrimaryDb } from "./driver";
|
import { db as mainDb } from "./driver";
|
||||||
import { createPool } from "./poolConfig";
|
import { createPool } from "./poolConfig";
|
||||||
|
|
||||||
function createLogsDb() {
|
function createLogsDb() {
|
||||||
@@ -63,8 +63,7 @@ function createLogsDb() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const maxReplicaConnections =
|
const maxReplicaConnections = poolConfig?.max_replica_connections || 20;
|
||||||
poolConfig?.max_replica_connections || 20;
|
|
||||||
for (const conn of replicaConnections) {
|
for (const conn of replicaConnections) {
|
||||||
const replicaPool = createPool(
|
const replicaPool = createPool(
|
||||||
conn.connection_string,
|
conn.connection_string,
|
||||||
@@ -91,4 +90,4 @@ function createLogsDb() {
|
|||||||
|
|
||||||
export const logsDb = createLogsDb();
|
export const logsDb = createLogsDb();
|
||||||
export default logsDb;
|
export default logsDb;
|
||||||
export const primaryLogsDb = logsDb.$primary;
|
export const primaryLogsDb = logsDb.$primary;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Pool, PoolConfig } from "pg";
|
import { Pool, PoolConfig } from "pg";
|
||||||
import logger from "@server/logger";
|
|
||||||
|
|
||||||
export function createPoolConfig(
|
export function createPoolConfig(
|
||||||
connectionString: string,
|
connectionString: string,
|
||||||
@@ -27,7 +26,7 @@ export function attachPoolErrorHandlers(pool: Pool, label: string): void {
|
|||||||
pool.on("error", (err) => {
|
pool.on("error", (err) => {
|
||||||
// This catches errors on idle clients in the pool. Without this
|
// This catches errors on idle clients in the pool. Without this
|
||||||
// handler an unexpected disconnect would crash the process.
|
// handler an unexpected disconnect would crash the process.
|
||||||
logger.error(
|
console.error(
|
||||||
`Unexpected error on idle ${label} database client: ${err.message}`
|
`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
|
// Set a statement timeout on every new connection so a single slow
|
||||||
// query can't block the pool forever
|
// query can't block the pool forever
|
||||||
client.query("SET statement_timeout = '30s'").catch((err: Error) => {
|
client.query("SET statement_timeout = '30s'").catch((err: Error) => {
|
||||||
logger.warn(
|
console.warn(
|
||||||
`Failed to set statement_timeout on ${label} client: ${err.message}`
|
`Failed to set statement_timeout on ${label} client: ${err.message}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -60,4 +59,4 @@ export function createPool(
|
|||||||
);
|
);
|
||||||
attachPoolErrorHandlers(pool, label);
|
attachPoolErrorHandlers(pool, label);
|
||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,12 +147,10 @@ export const resources = pgTable("resources", {
|
|||||||
}),
|
}),
|
||||||
ssl: boolean("ssl").notNull().default(false),
|
ssl: boolean("ssl").notNull().default(false),
|
||||||
blockAccess: boolean("blockAccess").notNull().default(false),
|
blockAccess: boolean("blockAccess").notNull().default(false),
|
||||||
sso: boolean("sso").notNull().default(true),
|
|
||||||
proxyPort: integer("proxyPort"),
|
proxyPort: integer("proxyPort"),
|
||||||
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
|
sso: boolean("sso"),
|
||||||
.notNull()
|
emailWhitelistEnabled: boolean("emailWhitelistEnabled"),
|
||||||
.default(false),
|
applyRules: boolean("applyRules"),
|
||||||
applyRules: boolean("applyRules").notNull().default(false),
|
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
stickySession: boolean("stickySession").notNull().default(false),
|
stickySession: boolean("stickySession").notNull().default(false),
|
||||||
tlsServerName: varchar("tlsServerName"),
|
tlsServerName: varchar("tlsServerName"),
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ export type ResourceWithAuth = {
|
|||||||
password: ResourcePassword | ResourcePolicyPassword | null;
|
password: ResourcePassword | ResourcePolicyPassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null;
|
||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
applyRules: boolean;
|
applyRules: boolean | null;
|
||||||
sso: boolean;
|
sso: boolean | null;
|
||||||
emailWhitelistEnabled: boolean;
|
emailWhitelistEnabled: boolean | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -165,14 +165,12 @@ export const resources = sqliteTable("resources", {
|
|||||||
blockAccess: integer("blockAccess", { mode: "boolean" })
|
blockAccess: integer("blockAccess", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
sso: integer("sso", { mode: "boolean" }).notNull().default(true),
|
|
||||||
proxyPort: integer("proxyPort"),
|
proxyPort: integer("proxyPort"),
|
||||||
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
|
sso: integer("sso", { mode: "boolean" }),
|
||||||
.notNull()
|
emailWhitelistEnabled: integer("emailWhitelistEnabled", {
|
||||||
.default(false),
|
mode: "boolean"
|
||||||
applyRules: integer("applyRules", { mode: "boolean" })
|
}),
|
||||||
.notNull()
|
applyRules: integer("applyRules", { mode: "boolean" }),
|
||||||
.default(false),
|
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
stickySession: integer("stickySession", { mode: "boolean" })
|
stickySession: integer("stickySession", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|||||||
@@ -157,7 +157,9 @@ function getOpenApiDocumentation() {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z
|
||||||
|
.record(z.string(), z.any())
|
||||||
|
.nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export enum TierFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||||
[TierFeature.Labels]: ["tier2", "tier3", "enterprise"],
|
[TierFeature.Labels]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
|
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
|
||||||
@@ -71,16 +71,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
|||||||
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"],
|
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"],
|
||||||
[TierFeature.AdvancedPublicResources]: [
|
[TierFeature.AdvancedPublicResources]: ["tier3", "enterprise"],
|
||||||
"tier1",
|
[TierFeature.AdvancedPrivateResources]: ["tier3", "enterprise"]
|
||||||
"tier2",
|
|
||||||
"tier3",
|
|
||||||
"enterprise"
|
|
||||||
],
|
|
||||||
[TierFeature.AdvancedPrivateResources]: [
|
|
||||||
"tier1",
|
|
||||||
"tier2",
|
|
||||||
"tier3",
|
|
||||||
"enterprise"
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -415,7 +415,11 @@ export async function updatePrivateResources(
|
|||||||
} else {
|
} else {
|
||||||
let aliasAddress: string | null = null;
|
let aliasAddress: string | null = null;
|
||||||
let releaseAliasLock: (() => Promise<void>) | 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(
|
const { value, release } = await getNextAvailableAliasAddress(
|
||||||
orgId,
|
orgId,
|
||||||
trx
|
trx
|
||||||
|
|||||||
@@ -1467,17 +1467,6 @@ async function syncWhitelistUsers(
|
|||||||
.where(eq(resourceWhitelist.resourceId, resourceId));
|
.where(eq(resourceWhitelist.resourceId, resourceId));
|
||||||
|
|
||||||
for (const email of whitelistUsers) {
|
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(
|
const existingWhitelistEntry = existingWhitelist.find(
|
||||||
(w) => w.email === email
|
(w) => w.email === email
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
import { portRangeStringSchema } from "@server/lib/ip";
|
import { portRangeStringSchema } from "@server/lib/ip";
|
||||||
import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema";
|
import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema";
|
||||||
import { isValidRegionId } from "@server/db/regions";
|
import { isValidRegionId } from "@server/db/regions";
|
||||||
import { wildcardSubdomainSchema } from "@server/lib/schemas";
|
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({
|
export const SiteSchema = z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
@@ -117,6 +132,9 @@ export const RuleSchema = z
|
|||||||
.refine(
|
.refine(
|
||||||
(rule) => {
|
(rule) => {
|
||||||
if (rule.match === "country") {
|
if (rule.match === "country") {
|
||||||
|
if (!hasMaxmindCountryDb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Check if it's a valid 2-letter country code or "ALL"
|
// Check if it's a valid 2-letter country code or "ALL"
|
||||||
return /^[A-Z]{2}$/.test(rule.value) || rule.value === "ALL";
|
return /^[A-Z]{2}$/.test(rule.value) || rule.value === "ALL";
|
||||||
}
|
}
|
||||||
@@ -125,12 +143,15 @@ export const RuleSchema = z
|
|||||||
{
|
{
|
||||||
path: ["value"],
|
path: ["value"],
|
||||||
message:
|
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(
|
.refine(
|
||||||
(rule) => {
|
(rule) => {
|
||||||
if (rule.match === "asn") {
|
if (rule.match === "asn") {
|
||||||
|
if (!hasMaxmindCountryDb || !hasMaxmindAsnDb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Check if it's either AS<number> format or "ALL"
|
// Check if it's either AS<number> format or "ALL"
|
||||||
const asNumberPattern = /^AS\d+$/i;
|
const asNumberPattern = /^AS\d+$/i;
|
||||||
return asNumberPattern.test(rule.value) || rule.value === "ALL";
|
return asNumberPattern.test(rule.value) || rule.value === "ALL";
|
||||||
@@ -140,7 +161,7 @@ export const RuleSchema = z
|
|||||||
{
|
{
|
||||||
path: ["value"],
|
path: ["value"],
|
||||||
message:
|
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(
|
.refine(
|
||||||
|
|||||||
+14
-5
@@ -504,7 +504,7 @@ export function generateRemoteSubnets(
|
|||||||
const parseResult = cidrSchema.safeParse(sr.destination);
|
const parseResult = cidrSchema.safeParse(sr.destination);
|
||||||
return parseResult.success;
|
return parseResult.success;
|
||||||
}
|
}
|
||||||
if (sr.mode === "host") {
|
if (sr.mode === "host" || sr.mode === "ssh") {
|
||||||
// check if its a valid IP using zod
|
// check if its a valid IP using zod
|
||||||
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
||||||
const parseResult = ipSchema.safeParse(sr.destination);
|
const parseResult = ipSchema.safeParse(sr.destination);
|
||||||
@@ -514,7 +514,7 @@ export function generateRemoteSubnets(
|
|||||||
})
|
})
|
||||||
.map((sr) => {
|
.map((sr) => {
|
||||||
if (sr.mode === "cidr") return sr.destination;
|
if (sr.mode === "cidr") return sr.destination;
|
||||||
if (sr.mode === "host") {
|
if (sr.mode === "host" || sr.mode === "ssh") {
|
||||||
return `${sr.destination}/32`;
|
return `${sr.destination}/32`;
|
||||||
}
|
}
|
||||||
return ""; // This should never be reached due to filtering, but satisfies TypeScript
|
return ""; // This should never be reached due to filtering, but satisfies TypeScript
|
||||||
@@ -531,7 +531,7 @@ export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
|||||||
.filter(
|
.filter(
|
||||||
(sr) =>
|
(sr) =>
|
||||||
sr.aliasAddress &&
|
sr.aliasAddress &&
|
||||||
((sr.alias && sr.mode == "host") ||
|
((sr.alias && (sr.mode == "host" || sr.mode == "ssh")) ||
|
||||||
(sr.fullDomain && sr.mode == "http"))
|
(sr.fullDomain && sr.mode == "http"))
|
||||||
)
|
)
|
||||||
.map((sr) => ({
|
.map((sr) => ({
|
||||||
@@ -577,6 +577,10 @@ export function generateSubnetProxyTargets(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!siteResource.destination) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
|
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
|
||||||
const portRange = [
|
const portRange = [
|
||||||
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
|
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
|
||||||
@@ -584,7 +588,7 @@ export function generateSubnetProxyTargets(
|
|||||||
];
|
];
|
||||||
const disableIcmp = siteResource.disableIcmp ?? false;
|
const disableIcmp = siteResource.disableIcmp ?? false;
|
||||||
|
|
||||||
if (siteResource.mode == "host") {
|
if (siteResource.mode == "host" || siteResource.mode == "ssh") {
|
||||||
let destination = siteResource.destination;
|
let destination = siteResource.destination;
|
||||||
// check if this is a valid ip
|
// check if this is a valid ip
|
||||||
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
||||||
@@ -665,6 +669,11 @@ export async function generateSubnetProxyTargetV2(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!siteResource.destination) {
|
||||||
|
// ssh can have no destination
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const targets: SubnetProxyTargetV2[] = [];
|
const targets: SubnetProxyTargetV2[] = [];
|
||||||
|
|
||||||
const portRange = [
|
const portRange = [
|
||||||
@@ -673,7 +682,7 @@ export async function generateSubnetProxyTargetV2(
|
|||||||
];
|
];
|
||||||
const disableIcmp = siteResource.disableIcmp ?? false;
|
const disableIcmp = siteResource.disableIcmp ?? false;
|
||||||
|
|
||||||
if (siteResource.mode == "host") {
|
if (siteResource.mode == "host" || siteResource.mode == "ssh") {
|
||||||
let destination = siteResource.destination;
|
let destination = siteResource.destination;
|
||||||
// check if this is a valid ip
|
// check if this is a valid ip
|
||||||
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ class TelemetryClient {
|
|||||||
let numPrivResourceHosts = 0;
|
let numPrivResourceHosts = 0;
|
||||||
let numPrivResourceCidr = 0;
|
let numPrivResourceCidr = 0;
|
||||||
let numPrivResourceHttp = 0;
|
let numPrivResourceHttp = 0;
|
||||||
|
let numPrivResourceSsh = 0;
|
||||||
for (const res of allPrivateResources) {
|
for (const res of allPrivateResources) {
|
||||||
if (res.mode === "host") {
|
if (res.mode === "host") {
|
||||||
numPrivResourceHosts += 1;
|
numPrivResourceHosts += 1;
|
||||||
@@ -188,6 +189,8 @@ class TelemetryClient {
|
|||||||
numPrivResourceCidr += 1;
|
numPrivResourceCidr += 1;
|
||||||
} else if (res.mode === "http") {
|
} else if (res.mode === "http") {
|
||||||
numPrivResourceHttp += 1;
|
numPrivResourceHttp += 1;
|
||||||
|
} else if (res.mode === "ssh") {
|
||||||
|
numPrivResourceSsh += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.alias) {
|
if (res.alias) {
|
||||||
@@ -207,6 +210,7 @@ class TelemetryClient {
|
|||||||
numPrivateResourceHosts: numPrivResourceHosts,
|
numPrivateResourceHosts: numPrivResourceHosts,
|
||||||
numPrivateResourceCidr: numPrivResourceCidr,
|
numPrivateResourceCidr: numPrivResourceCidr,
|
||||||
numPrivateResourceHttp: numPrivResourceHttp,
|
numPrivateResourceHttp: numPrivResourceHttp,
|
||||||
|
numPrivateResourceSsh: numPrivResourceSsh,
|
||||||
numAlertRules: numAlertRules.count,
|
numAlertRules: numAlertRules.count,
|
||||||
numUserDevices: userDevicesCount.count,
|
numUserDevices: userDevicesCount.count,
|
||||||
numMachineClients: machineClients.count,
|
numMachineClients: machineClients.count,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { db, Resource } from "@server/db";
|
import { db, Resource } from "@server/db";
|
||||||
import { resources, userOrgs, userResources, roleResources } from "@server/db";
|
import { resources, userOrgs } from "@server/db";
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import {
|
||||||
|
getRoleResourceAccess,
|
||||||
|
getUserResourceAccess
|
||||||
|
} from "@server/db/queries/verifySessionQueries";
|
||||||
|
|
||||||
export async function verifyResourceAccess(
|
export async function verifyResourceAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -116,37 +120,22 @@ export async function verifyResourceAccess(
|
|||||||
|
|
||||||
const roleResourceAccess =
|
const roleResourceAccess =
|
||||||
(req.userOrgRoleIds?.length ?? 0) > 0
|
(req.userOrgRoleIds?.length ?? 0) > 0
|
||||||
? await db
|
? await getRoleResourceAccess(
|
||||||
.select()
|
resource.resourceId,
|
||||||
.from(roleResources)
|
req.userOrgRoleIds!
|
||||||
.where(
|
)
|
||||||
and(
|
: null;
|
||||||
eq(roleResources.resourceId, resource.resourceId),
|
|
||||||
inArray(
|
|
||||||
roleResources.roleId,
|
|
||||||
req.userOrgRoleIds!
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (roleResourceAccess.length > 0) {
|
if (roleResourceAccess) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const userResourceAccess = await db
|
const userResourceAccess = await getUserResourceAccess(
|
||||||
.select()
|
userId,
|
||||||
.from(userResources)
|
resource.resourceId
|
||||||
.where(
|
);
|
||||||
and(
|
|
||||||
eq(userResources.userId, userId),
|
|
||||||
eq(userResources.resourceId, resource.resourceId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (userResourceAccess.length > 0) {
|
if (userResourceAccess) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
@@ -112,4 +112,4 @@ export async function deleteAlertRule(
|
|||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
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 { and, eq } from "drizzle-orm";
|
||||||
import { decrypt } from "@server/lib/crypto";
|
import { decrypt } from "@server/lib/crypto";
|
||||||
import config from "@server/lib/config";
|
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
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -55,7 +58,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
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) {
|
if (!parsedParams.success) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -112,4 +114,4 @@ export async function exportConnectionAuditLogs(
|
|||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,14 @@
|
|||||||
* This file is not licensed under the AGPLv3.
|
* 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 { registry } from "@server/openApi";
|
||||||
import { NextFunction } from "express";
|
import { NextFunction } from "express";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
@@ -150,21 +157,30 @@ export function queryAccess(data: Q) {
|
|||||||
.orderBy(desc(accessAuditLog.timestamp), desc(accessAuditLog.id));
|
.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
|
const resourceIds = logs
|
||||||
.map(log => log.resourceId)
|
.map((log) => log.resourceId)
|
||||||
.filter((id): id is number => id !== null && id !== undefined);
|
.filter((id): id is number => id !== null && id !== undefined);
|
||||||
|
|
||||||
const siteResourceIds = logs
|
const siteResourceIds = logs
|
||||||
.filter(log => log.resourceId == null && log.siteResourceId != null)
|
.filter((log) => log.resourceId == null && log.siteResourceId != null)
|
||||||
.map(log => log.siteResourceId)
|
.map((log) => log.siteResourceId)
|
||||||
.filter((id): id is number => id !== null && id !== undefined);
|
.filter((id): id is number => id !== null && id !== undefined);
|
||||||
|
|
||||||
if (resourceIds.length === 0 && siteResourceIds.length === 0) {
|
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) {
|
if (resourceIds.length > 0) {
|
||||||
const resourceDetails = await primaryDb
|
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) {
|
if (siteResourceIds.length > 0) {
|
||||||
const siteResourceDetails = await primaryDb
|
const siteResourceDetails = await primaryDb
|
||||||
@@ -194,12 +213,15 @@ async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryAc
|
|||||||
.where(inArray(siteResources.siteResourceId, siteResourceIds));
|
.where(inArray(siteResources.siteResourceId, siteResourceIds));
|
||||||
|
|
||||||
for (const r of siteResourceDetails) {
|
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
|
// Enrich logs with resource details
|
||||||
return logs.map(log => {
|
return logs.map((log) => {
|
||||||
if (log.resourceId != null) {
|
if (log.resourceId != null) {
|
||||||
const details = resourceMap.get(log.resourceId);
|
const details = resourceMap.get(log.resourceId);
|
||||||
return {
|
return {
|
||||||
@@ -273,11 +295,11 @@ async function queryUniqueFilterAttributes(
|
|||||||
|
|
||||||
// Fetch resource names from main database for the unique resource IDs
|
// Fetch resource names from main database for the unique resource IDs
|
||||||
const resourceIds = uniqueResources
|
const resourceIds = uniqueResources
|
||||||
.map(row => row.id)
|
.map((row) => row.id)
|
||||||
.filter((id): id is number => id !== null);
|
.filter((id): id is number => id !== null);
|
||||||
|
|
||||||
const siteResourceIds = uniqueSiteResources
|
const siteResourceIds = uniqueSiteResources
|
||||||
.map(row => row.id)
|
.map((row) => row.id)
|
||||||
.filter((id): id is number => id !== null);
|
.filter((id): id is number => id !== null);
|
||||||
|
|
||||||
let resourcesWithNames: Array<{ id: number; name: string | null }> = [];
|
let resourcesWithNames: Array<{ id: number; name: string | null }> = [];
|
||||||
@@ -293,7 +315,7 @@ async function queryUniqueFilterAttributes(
|
|||||||
|
|
||||||
resourcesWithNames = [
|
resourcesWithNames = [
|
||||||
...resourcesWithNames,
|
...resourcesWithNames,
|
||||||
...resourceDetails.map(r => ({
|
...resourceDetails.map((r) => ({
|
||||||
id: r.resourceId,
|
id: r.resourceId,
|
||||||
name: r.name
|
name: r.name
|
||||||
}))
|
}))
|
||||||
@@ -311,7 +333,7 @@ async function queryUniqueFilterAttributes(
|
|||||||
|
|
||||||
resourcesWithNames = [
|
resourcesWithNames = [
|
||||||
...resourcesWithNames,
|
...resourcesWithNames,
|
||||||
...siteResourceDetails.map(r => ({
|
...siteResourceDetails.map((r) => ({
|
||||||
id: r.siteResourceId,
|
id: r.siteResourceId,
|
||||||
name: r.name
|
name: r.name
|
||||||
}))
|
}))
|
||||||
@@ -344,7 +366,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import createHttpError from "http-errors";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { sessions, sessionTransferToken } from "@server/db";
|
import { db, safeRead, sessions, sessionTransferToken } from "@server/db";
|
||||||
import { db } from "@server/db";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { response } from "@server/lib/response";
|
import { response } from "@server/lib/response";
|
||||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
@@ -57,15 +56,19 @@ export async function transferSession(
|
|||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
|
|
||||||
const [existing] = await db
|
const result = await safeRead((db) =>
|
||||||
.select()
|
db
|
||||||
.from(sessionTransferToken)
|
.select()
|
||||||
.where(eq(sessionTransferToken.token, tokenRaw))
|
.from(sessionTransferToken)
|
||||||
.innerJoin(
|
.where(eq(sessionTransferToken.token, tokenRaw))
|
||||||
sessions,
|
.innerJoin(
|
||||||
eq(sessions.sessionId, sessionTransferToken.sessionId)
|
sessions,
|
||||||
)
|
eq(sessions.sessionId, sessionTransferToken.sessionId)
|
||||||
.limit(1);
|
)
|
||||||
|
.limit(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [existing] = result;
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const getOrgSchema = z.strictObject({
|
|||||||
// content: {
|
// content: {
|
||||||
// "application/json": {
|
// "application/json": {
|
||||||
// schema: z.object({
|
// schema: z.object({
|
||||||
// data: z.unknown().nullable(),
|
// data: z.record(z.string(), z.any()).nullable(),
|
||||||
// success: z.boolean(),
|
// success: z.boolean(),
|
||||||
// error: z.boolean(),
|
// error: z.boolean(),
|
||||||
// message: z.string(),
|
// message: z.string(),
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
|||||||
const paramsSchema = z.strictObject({});
|
const paramsSchema = z.strictObject({});
|
||||||
|
|
||||||
const querySchema = 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
|
// orgId: build === "saas" ? z.string() : z.string().optional() // Required for saas, optional otherwise
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ const paramsSchema = z
|
|||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
path: "/org/{orgId}/event-streaming-destination/{destinationId}",
|
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],
|
tags: [OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
params: paramsSchema
|
params: paramsSchema
|
||||||
@@ -44,7 +45,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
@@ -115,4 +116,4 @@ export async function deleteEventStreamingDestination(
|
|||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -219,9 +219,9 @@ export type ResourceWithAuth = {
|
|||||||
password: ResourcePassword | ResourcePolicyPassword | null;
|
password: ResourcePassword | ResourcePolicyPassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null;
|
||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
applyRules: boolean;
|
applyRules: boolean | null;
|
||||||
sso: boolean;
|
sso: boolean | null;
|
||||||
emailWhitelistEnabled: boolean;
|
emailWhitelistEnabled: boolean | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
@@ -127,7 +127,8 @@ export async function createOrgOidcIdp(
|
|||||||
|
|
||||||
let { autoProvision } = parsedBody.data;
|
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(
|
const subscribed = await isSubscribed(
|
||||||
orgId,
|
orgId,
|
||||||
tierMatrix.deviceApprovals
|
tierMatrix.deviceApprovals
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
@@ -164,7 +164,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
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 { registry } from "@server/openApi";
|
||||||
import { NextFunction } from "express";
|
import { NextFunction } from "express";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
@@ -127,16 +134,16 @@ export function queryRequest(data: Q) {
|
|||||||
return logsDb
|
return logsDb
|
||||||
.select({
|
.select({
|
||||||
id: requestAuditLog.id,
|
id: requestAuditLog.id,
|
||||||
timestamp: requestAuditLog.timestamp,
|
timestamp: requestAuditLog.timestamp,
|
||||||
orgId: requestAuditLog.orgId,
|
orgId: requestAuditLog.orgId,
|
||||||
action: requestAuditLog.action,
|
action: requestAuditLog.action,
|
||||||
reason: requestAuditLog.reason,
|
reason: requestAuditLog.reason,
|
||||||
actorType: requestAuditLog.actorType,
|
actorType: requestAuditLog.actorType,
|
||||||
actor: requestAuditLog.actor,
|
actor: requestAuditLog.actor,
|
||||||
actorId: requestAuditLog.actorId,
|
actorId: requestAuditLog.actorId,
|
||||||
resourceId: requestAuditLog.resourceId,
|
resourceId: requestAuditLog.resourceId,
|
||||||
siteResourceId: requestAuditLog.siteResourceId,
|
siteResourceId: requestAuditLog.siteResourceId,
|
||||||
ip: requestAuditLog.ip,
|
ip: requestAuditLog.ip,
|
||||||
location: requestAuditLog.location,
|
location: requestAuditLog.location,
|
||||||
userAgent: requestAuditLog.userAgent,
|
userAgent: requestAuditLog.userAgent,
|
||||||
metadata: requestAuditLog.metadata,
|
metadata: requestAuditLog.metadata,
|
||||||
@@ -154,21 +161,30 @@ export function queryRequest(data: Q) {
|
|||||||
.orderBy(desc(requestAuditLog.timestamp));
|
.orderBy(desc(requestAuditLog.timestamp));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryRequest>>) {
|
async function enrichWithResourceDetails(
|
||||||
|
logs: Awaited<ReturnType<typeof queryRequest>>
|
||||||
|
) {
|
||||||
const resourceIds = logs
|
const resourceIds = logs
|
||||||
.map(log => log.resourceId)
|
.map((log) => log.resourceId)
|
||||||
.filter((id): id is number => id !== null && id !== undefined);
|
.filter((id): id is number => id !== null && id !== undefined);
|
||||||
|
|
||||||
const siteResourceIds = logs
|
const siteResourceIds = logs
|
||||||
.filter(log => log.resourceId == null && log.siteResourceId != null)
|
.filter((log) => log.resourceId == null && log.siteResourceId != null)
|
||||||
.map(log => log.siteResourceId)
|
.map((log) => log.siteResourceId)
|
||||||
.filter((id): id is number => id !== null && id !== undefined);
|
.filter((id): id is number => id !== null && id !== undefined);
|
||||||
|
|
||||||
if (resourceIds.length === 0 && siteResourceIds.length === 0) {
|
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) {
|
if (resourceIds.length > 0) {
|
||||||
const resourceDetails = await primaryDb
|
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) {
|
if (siteResourceIds.length > 0) {
|
||||||
const siteResourceDetails = await primaryDb
|
const siteResourceDetails = await primaryDb
|
||||||
@@ -198,12 +217,15 @@ async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryRe
|
|||||||
.where(inArray(siteResources.siteResourceId, siteResourceIds));
|
.where(inArray(siteResources.siteResourceId, siteResourceIds));
|
||||||
|
|
||||||
for (const r of siteResourceDetails) {
|
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
|
// Enrich logs with resource details
|
||||||
return logs.map(log => {
|
return logs.map((log) => {
|
||||||
if (log.resourceId != null) {
|
if (log.resourceId != null) {
|
||||||
const details = resourceMap.get(log.resourceId);
|
const details = resourceMap.get(log.resourceId);
|
||||||
return {
|
return {
|
||||||
@@ -247,7 +269,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
@@ -333,11 +355,11 @@ async function queryUniqueFilterAttributes(
|
|||||||
|
|
||||||
// Fetch resource names from main database for the unique resource IDs
|
// Fetch resource names from main database for the unique resource IDs
|
||||||
const resourceIds = uniqueResources
|
const resourceIds = uniqueResources
|
||||||
.map(row => row.id)
|
.map((row) => row.id)
|
||||||
.filter((id): id is number => id !== null);
|
.filter((id): id is number => id !== null);
|
||||||
|
|
||||||
const siteResourceIds = uniqueSiteResources
|
const siteResourceIds = uniqueSiteResources
|
||||||
.map(row => row.id)
|
.map((row) => row.id)
|
||||||
.filter((id): id is number => id !== null);
|
.filter((id): id is number => id !== null);
|
||||||
|
|
||||||
let resourcesWithNames: Array<{ id: number; name: string | null }> = [];
|
let resourcesWithNames: Array<{ id: number; name: string | null }> = [];
|
||||||
@@ -353,7 +375,7 @@ async function queryUniqueFilterAttributes(
|
|||||||
|
|
||||||
resourcesWithNames = [
|
resourcesWithNames = [
|
||||||
...resourcesWithNames,
|
...resourcesWithNames,
|
||||||
...resourceDetails.map(r => ({
|
...resourceDetails.map((r) => ({
|
||||||
id: r.resourceId,
|
id: r.resourceId,
|
||||||
name: r.name
|
name: r.name
|
||||||
}))
|
}))
|
||||||
@@ -371,7 +393,7 @@ async function queryUniqueFilterAttributes(
|
|||||||
|
|
||||||
resourcesWithNames = [
|
resourcesWithNames = [
|
||||||
...resourcesWithNames,
|
...resourcesWithNames,
|
||||||
...siteResourceDetails.map(r => ({
|
...siteResourceDetails.map((r) => ({
|
||||||
id: r.siteResourceId,
|
id: r.siteResourceId,
|
||||||
name: r.name
|
name: r.name
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import {
|
import { users, userOrgs, orgs, idpOrg, idp, idpOidcConfig } from "@server/db";
|
||||||
users,
|
|
||||||
userOrgs,
|
|
||||||
orgs,
|
|
||||||
idpOrg,
|
|
||||||
idp,
|
|
||||||
idpOidcConfig
|
|
||||||
} from "@server/db";
|
|
||||||
import { eq, or, sql, and, isNotNull, inArray } from "drizzle-orm";
|
import { eq, or, sql, and, isNotNull, inArray } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -57,7 +50,7 @@ export type LookupUserResponse = {
|
|||||||
// content: {
|
// content: {
|
||||||
// "application/json": {
|
// "application/json": {
|
||||||
// schema: z.object({
|
// schema: z.object({
|
||||||
// data: z.unknown().nullable(),
|
// data: z.record(z.string(), z.any()).nullable(),
|
||||||
// success: z.boolean(),
|
// success: z.boolean(),
|
||||||
// error: z.boolean(),
|
// error: z.boolean(),
|
||||||
// message: z.string(),
|
// message: z.string(),
|
||||||
@@ -169,46 +162,54 @@ export async function lookupUser(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Deduplicate orgs (user might have multiple memberships in same org)
|
// 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) {
|
for (const membership of userOrgMemberships) {
|
||||||
if (!uniqueOrgs.has(membership.orgId)) {
|
if (!uniqueOrgs.has(membership.orgId)) {
|
||||||
uniqueOrgs.set(membership.orgId, membership);
|
uniqueOrgs.set(membership.orgId, membership);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgsData = Array.from(uniqueOrgs.values()).map((membership) => {
|
const orgsData = Array.from(uniqueOrgs.values()).map(
|
||||||
// Get IdPs for this org where the user (with the exact identifier) is authenticated via that IdP
|
(membership) => {
|
||||||
// Only show IdPs where the user's idpId matches
|
// Get IdPs for this org where the user (with the exact identifier) is authenticated via that IdP
|
||||||
// Internal users don't have an idpId, so they won't see any IdPs
|
// Only show IdPs where the user's idpId matches
|
||||||
const orgIdpsList = orgIdps
|
// Internal users don't have an idpId, so they won't see any IdPs
|
||||||
.filter((idp) => {
|
const orgIdpsList = orgIdps
|
||||||
if (idp.orgId !== membership.orgId) {
|
.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;
|
return false;
|
||||||
}
|
})
|
||||||
// Only show IdPs where the user (with exact identifier) is authenticated via that IdP
|
.map((idp) => ({
|
||||||
// This means user.idpId must match idp.idpId
|
idpId: idp.idpId,
|
||||||
if (user.idpId !== null && user.idpId === idp.idpId) {
|
name: idp.idpName,
|
||||||
return true;
|
variant: idp.variant
|
||||||
}
|
}));
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.map((idp) => ({
|
|
||||||
idpId: idp.idpId,
|
|
||||||
name: idp.idpName,
|
|
||||||
variant: idp.variant
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Check if user has internal auth for this org
|
// Check if user has internal auth for this org
|
||||||
// User has internal auth if they have an internal account type
|
// User has internal auth if they have an internal account type
|
||||||
const orgHasInternalAuth = hasInternalAuth;
|
const orgHasInternalAuth = hasInternalAuth;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
orgId: membership.orgId,
|
orgId: membership.orgId,
|
||||||
orgName: membership.orgName,
|
orgName: membership.orgName,
|
||||||
idps: orgIdpsList,
|
idps: orgIdpsList,
|
||||||
hasInternalAuth: orgHasInternalAuth
|
hasInternalAuth: orgHasInternalAuth
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
accounts.push({
|
accounts.push({
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
|
|||||||
@@ -145,9 +145,9 @@ export async function verifyResourceSession(
|
|||||||
| ResourcePolicyHeaderAuth
|
| ResourcePolicyHeaderAuth
|
||||||
| null;
|
| null;
|
||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
applyRules: boolean;
|
applyRules: boolean | null;
|
||||||
sso: boolean;
|
sso: boolean | null;
|
||||||
emailWhitelistEnabled: boolean;
|
emailWhitelistEnabled: boolean | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
}
|
}
|
||||||
| undefined = localCache.get(resourceCacheKey);
|
| undefined = localCache.get(resourceCacheKey);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
@@ -94,7 +94,11 @@ export async function blockClient(
|
|||||||
|
|
||||||
// Send terminate signal if there's an associated OLM and it's connected
|
// Send terminate signal if there's an associated OLM and it's connected
|
||||||
if (client.olmId && client.online) {
|
if (client.olmId && client.online) {
|
||||||
await sendTerminateClient(client.clientId, OlmErrorCodes.TERMINATED_BLOCKED, client.olmId);
|
await sendTerminateClient(
|
||||||
|
client.clientId,
|
||||||
|
OlmErrorCodes.TERMINATED_BLOCKED,
|
||||||
|
client.olmId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
@@ -287,7 +287,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
@@ -340,18 +340,18 @@ export async function getClient(
|
|||||||
// Build fingerprint data if available
|
// Build fingerprint data if available
|
||||||
const fingerprintData = client.currentFingerprint
|
const fingerprintData = client.currentFingerprint
|
||||||
? {
|
? {
|
||||||
username: client.currentFingerprint.username || null,
|
username: client.currentFingerprint.username || null,
|
||||||
hostname: client.currentFingerprint.hostname || null,
|
hostname: client.currentFingerprint.hostname || null,
|
||||||
platform: client.currentFingerprint.platform || null,
|
platform: client.currentFingerprint.platform || null,
|
||||||
osVersion: client.currentFingerprint.osVersion || null,
|
osVersion: client.currentFingerprint.osVersion || null,
|
||||||
kernelVersion:
|
kernelVersion:
|
||||||
client.currentFingerprint.kernelVersion || null,
|
client.currentFingerprint.kernelVersion || null,
|
||||||
arch: client.currentFingerprint.arch || null,
|
arch: client.currentFingerprint.arch || null,
|
||||||
deviceModel: client.currentFingerprint.deviceModel || null,
|
deviceModel: client.currentFingerprint.deviceModel || null,
|
||||||
serialNumber: client.currentFingerprint.serialNumber || null,
|
serialNumber: client.currentFingerprint.serialNumber || null,
|
||||||
firstSeen: client.currentFingerprint.firstSeen || null,
|
firstSeen: client.currentFingerprint.firstSeen || null,
|
||||||
lastSeen: client.currentFingerprint.lastSeen || null
|
lastSeen: client.currentFingerprint.lastSeen || null
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Build posture data if available (platform-specific)
|
// Build posture data if available (platform-specific)
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import semver from "semver";
|
|||||||
|
|
||||||
const NEWT_V2_TARGETS_VERSION = ">=1.10.3";
|
const NEWT_V2_TARGETS_VERSION = ">=1.10.3";
|
||||||
|
|
||||||
export async function convertTargetsIfNessicary(
|
export async function convertTargetsIfNecessary(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[]
|
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[]
|
||||||
) {
|
) {
|
||||||
@@ -47,7 +47,7 @@ export async function addTargets(
|
|||||||
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
|
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
|
||||||
version?: string | null
|
version?: string | null
|
||||||
) {
|
) {
|
||||||
targets = await convertTargetsIfNessicary(newtId, targets);
|
targets = await convertTargetsIfNecessary(newtId, targets);
|
||||||
|
|
||||||
await sendToClient(
|
await sendToClient(
|
||||||
newtId,
|
newtId,
|
||||||
@@ -64,7 +64,7 @@ export async function removeTargets(
|
|||||||
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
|
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
|
||||||
version?: string | null
|
version?: string | null
|
||||||
) {
|
) {
|
||||||
targets = await convertTargetsIfNessicary(newtId, targets);
|
targets = await convertTargetsIfNecessary(newtId, targets);
|
||||||
|
|
||||||
await sendToClient(
|
await sendToClient(
|
||||||
newtId,
|
newtId,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, Org } from "@server/db";
|
import { db, Org, primaryDb } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -635,9 +635,7 @@ export async function validateOidcCallback(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
db.transaction(async (trx) => {
|
calculateUserClientsForOrgs(userId!, primaryDb).catch((err) => {
|
||||||
await calculateUserClientsForOrgs(userId!, trx);
|
|
||||||
}).catch((err) => {
|
|
||||||
logger.error(
|
logger.error(
|
||||||
"Error calculating user clients after syncing orgs and roles for OIDC user",
|
"Error calculating user clients after syncing orgs and roles for OIDC user",
|
||||||
{ error: err }
|
{ error: err }
|
||||||
|
|||||||
@@ -152,34 +152,64 @@ export async function buildClientConfigurationForNewtClient(
|
|||||||
|
|
||||||
const targetsToSend: SubnetProxyTargetV2[] = [];
|
const targetsToSend: SubnetProxyTargetV2[] = [];
|
||||||
|
|
||||||
for (const resource of allSiteResources) {
|
if (allSiteResources.length === 0) {
|
||||||
// Get clients associated with this specific resource
|
return {
|
||||||
const resourceClients = await db
|
peers: validPeers,
|
||||||
.select({
|
targets: targetsToSend
|
||||||
clientId: clients.clientId,
|
};
|
||||||
pubKey: clients.pubKey,
|
}
|
||||||
subnet: clients.subnet
|
|
||||||
})
|
|
||||||
.from(clients)
|
|
||||||
.innerJoin(
|
|
||||||
clientSiteResourcesAssociationsCache,
|
|
||||||
eq(
|
|
||||||
clients.clientId,
|
|
||||||
clientSiteResourcesAssociationsCache.clientId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
eq(
|
|
||||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
|
||||||
resource.siteResourceId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const resourceTargets = await generateSubnetProxyTargetV2(
|
// Batch fetch all client associations for every site resource in one query
|
||||||
resource,
|
// to avoid an N+1 lookup that would issue thousands of queries when a site
|
||||||
resourceClients
|
// 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) {
|
if (resourceTargets) {
|
||||||
targetsToSend.push(...resourceTargets);
|
targetsToSend.push(...resourceTargets);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,13 +56,18 @@ async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
|||||||
return staleReleaseInfo;
|
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(
|
releases = releases.filter(
|
||||||
(r: any) =>
|
(r: any) =>
|
||||||
!r.draft &&
|
!r.draft &&
|
||||||
!r.prerelease &&
|
!r.prerelease &&
|
||||||
!r.tag_name.includes("rc") &&
|
!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.
|
// 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 { eq } from "drizzle-orm";
|
||||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
|
import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
|
||||||
import { convertTargetsIfNessicary } from "../client/targets";
|
import { convertTargetsIfNecessary } from "../client/targets";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
exitNode
|
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 {
|
return {
|
||||||
message: {
|
message: {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export type CreateOlmResponse = {
|
|||||||
// content: {
|
// content: {
|
||||||
// "application/json": {
|
// "application/json": {
|
||||||
// schema: z.object({
|
// schema: z.object({
|
||||||
// data: z.unknown().nullable(),
|
// data: z.record(z.string(), z.any()).nullable(),
|
||||||
// success: z.boolean(),
|
// success: z.boolean(),
|
||||||
// error: z.boolean(),
|
// error: z.boolean(),
|
||||||
// message: z.string(),
|
// message: z.string(),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const paramsSchema = z
|
|||||||
// content: {
|
// content: {
|
||||||
// "application/json": {
|
// "application/json": {
|
||||||
// schema: z.object({
|
// schema: z.object({
|
||||||
// data: z.unknown().nullable(),
|
// data: z.record(z.string(), z.any()).nullable(),
|
||||||
// success: z.boolean(),
|
// success: z.boolean(),
|
||||||
// error: z.boolean(),
|
// error: z.boolean(),
|
||||||
// message: z.string(),
|
// message: z.string(),
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const querySchema = z.object({
|
|||||||
// content: {
|
// content: {
|
||||||
// "application/json": {
|
// "application/json": {
|
||||||
// schema: z.object({
|
// schema: z.object({
|
||||||
// data: z.unknown().nullable(),
|
// data: z.record(z.string(), z.any()).nullable(),
|
||||||
// success: z.boolean(),
|
// success: z.boolean(),
|
||||||
// error: z.boolean(),
|
// error: z.boolean(),
|
||||||
// message: z.string(),
|
// message: z.string(),
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const paramsSchema = z
|
|||||||
// content: {
|
// content: {
|
||||||
// "application/json": {
|
// "application/json": {
|
||||||
// schema: z.object({
|
// schema: z.object({
|
||||||
// data: z.unknown().nullable(),
|
// data: z.record(z.string(), z.any()).nullable(),
|
||||||
// success: z.boolean(),
|
// success: z.boolean(),
|
||||||
// error: z.boolean(),
|
// error: z.boolean(),
|
||||||
// message: z.string(),
|
// message: z.string(),
|
||||||
|
|||||||
@@ -49,10 +49,7 @@ async function queryUser(orgId: string, userId: string) {
|
|||||||
.from(userOrgRoles)
|
.from(userOrgRoles)
|
||||||
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
|
||||||
eq(userOrgRoles.userId, userId),
|
|
||||||
eq(userOrgRoles.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAdmin = roleRows.some((r) => r.isAdmin);
|
const isAdmin = roleRows.some((r) => r.isAdmin);
|
||||||
@@ -89,7 +86,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const listOrgsSchema = z.object({
|
|||||||
// content: {
|
// content: {
|
||||||
// "application/json": {
|
// "application/json": {
|
||||||
// schema: z.object({
|
// schema: z.object({
|
||||||
// data: z.unknown().nullable(),
|
// data: z.record(z.string(), z.any()).nullable(),
|
||||||
// success: z.boolean(),
|
// success: z.boolean(),
|
||||||
// error: z.boolean(),
|
// error: z.boolean(),
|
||||||
// message: z.string(),
|
// message: z.string(),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ const addRoleToResourceParamsSchema = z
|
|||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/resource/{resourceId}/roles/add",
|
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],
|
tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
|
||||||
request: {
|
request: {
|
||||||
params: addRoleToResourceParamsSchema,
|
params: addRoleToResourceParamsSchema,
|
||||||
@@ -46,7 +47,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ const addUserToResourceParamsSchema = z
|
|||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/resource/{resourceId}/users/add",
|
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],
|
tags: [OpenAPITags.PublicResource, OpenAPITags.User],
|
||||||
request: {
|
request: {
|
||||||
params: addUserToResourceParamsSchema,
|
params: addUserToResourceParamsSchema,
|
||||||
@@ -46,7 +47,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { db, resources } from "@server/db";
|
import { db, resourcePolicies, resources } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import logger from "@server/logger";
|
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<
|
export type GetResourceResponse = Omit<
|
||||||
NonNullable<Awaited<ReturnType<typeof query>>>,
|
NonNullable<Awaited<ReturnType<typeof query>>>,
|
||||||
"headers"
|
"headers"
|
||||||
@@ -66,7 +75,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
@@ -94,7 +103,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
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, {
|
return response<GetResourceResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
...resource,
|
...returnData,
|
||||||
headers: resource.headers
|
headers: returnData.headers
|
||||||
? JSON.parse(resource.headers)
|
? JSON.parse(returnData.headers)
|
||||||
: resource.headers
|
: returnData.headers
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -203,9 +203,9 @@ export async function getUserResources(
|
|||||||
fullDomain: string | null;
|
fullDomain: string | null;
|
||||||
ssl: boolean;
|
ssl: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
sso: boolean;
|
sso: boolean | null;
|
||||||
mode: string;
|
mode: string;
|
||||||
emailWhitelistEnabled: boolean;
|
emailWhitelistEnabled: boolean | null;
|
||||||
policyEmailWhitelistEnabled: boolean | null;
|
policyEmailWhitelistEnabled: boolean | null;
|
||||||
}> = [];
|
}> = [];
|
||||||
if (uniqueResourceIds.length > 0) {
|
if (uniqueResourceIds.length > 0) {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ registry.registerPath({
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
data: z.unknown().nullable(),
|
data: z.record(z.string(), z.any()).nullable(),
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
error: z.boolean(),
|
error: z.boolean(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user