mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-16 21:29:52 +00:00
Merge branch 'dev' into resource-policies
This commit is contained in:
@@ -100,6 +100,7 @@ export type QueryConnectionAuditLogResponse = {
|
||||
orgId: string | null;
|
||||
siteId: number | null;
|
||||
clientId: number | null;
|
||||
clientEndpoint: string | null;
|
||||
userId: string | null;
|
||||
sourceAddr: string;
|
||||
destAddr: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, orgs, userOrgs, users } from "@server/db";
|
||||
import { db, orgs, userOrgs, users, primaryDb } from "@server/db";
|
||||
import { eq, and, inArray, not } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -218,13 +218,18 @@ export async function deleteMyAccount(
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.delete(users).where(eq(users.userId, userId));
|
||||
await calculateUserClientsForOrgs(userId, trx);
|
||||
// loop through the other orgs and decrement the count
|
||||
for (const userOrg of otherOrgsTheUserWasIn) {
|
||||
await usageService.add(userOrg.orgId, FeatureId.USERS, -1, trx);
|
||||
}
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after deleting account for user ${userId}: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await invalidateSession(session.sessionId);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { db, primaryDb } from "@server/db";
|
||||
import {
|
||||
roles,
|
||||
Client,
|
||||
@@ -92,7 +92,10 @@ export async function createClient(
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
|
||||
if (
|
||||
req.user &&
|
||||
(!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)
|
||||
) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
@@ -198,7 +201,10 @@ export async function createClient(
|
||||
|
||||
if (!randomExitNode) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, `No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`)
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -256,10 +262,18 @@ export async function createClient(
|
||||
clientId: newClient.clientId,
|
||||
dateCreated: moment().toISOString()
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromClient(newClient, trx);
|
||||
});
|
||||
|
||||
if (newClient) {
|
||||
rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after creating client: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return response<CreateClientResponse>(res, {
|
||||
data: newClient,
|
||||
success: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { db, primaryDb } from "@server/db";
|
||||
import {
|
||||
roles,
|
||||
Client,
|
||||
@@ -237,10 +237,18 @@ export async function createUserClient(
|
||||
userId,
|
||||
clientId: newClient.clientId
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromClient(newClient, trx);
|
||||
});
|
||||
|
||||
if (newClient) {
|
||||
rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after creating user client: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return response<CreateClientAndOlmResponse>(res, {
|
||||
data: newClient,
|
||||
success: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, olms } from "@server/db";
|
||||
import { db, olms, primaryDb, Client, Olm } from "@server/db";
|
||||
import { clients, clientSitesAssociationsCache } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
@@ -71,14 +71,17 @@ export async function deleteClient(
|
||||
);
|
||||
}
|
||||
|
||||
let deletedClient: Client | undefined;
|
||||
let olm: Olm | undefined;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
// Then delete the client itself
|
||||
const [deletedClient] = await trx
|
||||
[deletedClient] = await trx
|
||||
.delete(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.returning();
|
||||
|
||||
const [olm] = await trx
|
||||
[olm] = await trx
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(eq(olms.clientId, clientId))
|
||||
@@ -88,14 +91,29 @@ export async function deleteClient(
|
||||
if (!client.userId && client.olmId) {
|
||||
await trx.delete(olms).where(eq(olms.olmId, client.olmId));
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
||||
|
||||
if (olm) {
|
||||
await sendTerminateClient(deletedClient.clientId, OlmErrorCodes.TERMINATED_DELETED, olm.olmId); // the olmId needs to be provided because it cant look it up after deletion
|
||||
}
|
||||
});
|
||||
|
||||
if (deletedClient) {
|
||||
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after deleting client ${clientId}: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
if (olm) {
|
||||
sendTerminateClient(
|
||||
deletedClient.clientId,
|
||||
OlmErrorCodes.TERMINATED_DELETED,
|
||||
olm.olmId
|
||||
).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to send terminate message for client ${deletedClient?.clientId} after deleting client ${clientId}: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { db, olms } from "@server/db";
|
||||
import { db, olms, primaryDb } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { z } from "zod";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -81,16 +81,19 @@ export async function createUserOlm(
|
||||
|
||||
const secretHash = await hashPassword(secret);
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.insert(olms).values({
|
||||
olmId: olmId,
|
||||
userId,
|
||||
name,
|
||||
secretHash,
|
||||
dateCreated: moment().toISOString()
|
||||
});
|
||||
await db.insert(olms).values({
|
||||
olmId: olmId,
|
||||
userId,
|
||||
name,
|
||||
secretHash,
|
||||
dateCreated: moment().toISOString()
|
||||
});
|
||||
|
||||
await calculateUserClientsForOrgs(userId, trx);
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
console.error(
|
||||
"Error calculating user clients after creating olm:",
|
||||
e
|
||||
);
|
||||
});
|
||||
|
||||
return response<CreateOlmResponse>(res, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { Client, db } from "@server/db";
|
||||
import { Client, db, Olm, primaryDb } from "@server/db";
|
||||
import { olms, clients, clientSitesAssociationsCache } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -49,6 +49,7 @@ export async function deleteUserOlm(
|
||||
|
||||
const { olmId } = parsedParams.data;
|
||||
|
||||
let deletedClient: Client | undefined;
|
||||
// Delete associated clients and the OLM in a transaction
|
||||
await db.transaction(async (trx) => {
|
||||
// Find all clients associated with this OLM
|
||||
@@ -57,7 +58,6 @@ export async function deleteUserOlm(
|
||||
.from(clients)
|
||||
.where(eq(clients.olmId, olmId));
|
||||
|
||||
let deletedClient: Client | null = null;
|
||||
// Delete all associated clients
|
||||
if (associatedClients.length > 0) {
|
||||
[deletedClient] = await trx
|
||||
@@ -67,23 +67,28 @@ export async function deleteUserOlm(
|
||||
}
|
||||
|
||||
// Finally, delete the OLM itself
|
||||
const [olm] = await trx
|
||||
.delete(olms)
|
||||
.where(eq(olms.olmId, olmId))
|
||||
.returning();
|
||||
|
||||
if (deletedClient) {
|
||||
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
||||
if (olm) {
|
||||
await sendTerminateClient(
|
||||
deletedClient.clientId,
|
||||
OlmErrorCodes.TERMINATED_DELETED,
|
||||
olm.olmId
|
||||
); // the olmId needs to be provided because it cant look it up after deletion
|
||||
}
|
||||
}
|
||||
await trx.delete(olms).where(eq(olms.olmId, olmId)).returning();
|
||||
});
|
||||
|
||||
if (deletedClient) {
|
||||
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client-site associations after deleting OLM ${olmId}: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
sendTerminateClient(
|
||||
deletedClient.clientId,
|
||||
OlmErrorCodes.TERMINATED_DELETED,
|
||||
olmId
|
||||
).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to send terminate message for client ${deletedClient?.clientId} after deleting OLM ${olmId}: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
|
||||
@@ -22,14 +22,14 @@ import { canCompress } from "@server/lib/clientVersionChecks";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
logger.info("Handling register olm message!");
|
||||
logger.info("[handleOlmRegisterMessage] Handling register olm message");
|
||||
const { message, client: c, sendToClient } = context;
|
||||
const olm = c as Olm;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (!olm) {
|
||||
logger.warn("Olm not found");
|
||||
logger.warn("[handleOlmRegisterMessage] Olm not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,16 +46,19 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
} = message.data;
|
||||
|
||||
if (!olm.clientId) {
|
||||
logger.warn("Olm client ID not found");
|
||||
logger.warn("[handleOlmRegisterMessage] Olm client ID not found");
|
||||
sendOlmError(OlmErrorCodes.CLIENT_ID_NOT_FOUND, olm.olmId);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Handling fingerprint insertion for olm register...", {
|
||||
olmId: olm.olmId,
|
||||
fingerprint,
|
||||
postures
|
||||
});
|
||||
logger.debug(
|
||||
"[handleOlmRegisterMessage] Handling fingerprint insertion for olm register...",
|
||||
{
|
||||
olmId: olm.olmId,
|
||||
fingerprint,
|
||||
postures
|
||||
}
|
||||
);
|
||||
|
||||
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
|
||||
|
||||
@@ -85,14 +88,17 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
logger.warn("Client ID not found");
|
||||
logger.warn("[handleOlmRegisterMessage] Client not found", {
|
||||
clientId: olm.clientId
|
||||
});
|
||||
sendOlmError(OlmErrorCodes.CLIENT_NOT_FOUND, olm.olmId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.blocked) {
|
||||
logger.debug(
|
||||
`Client ${client.clientId} is blocked. Ignoring register.`
|
||||
`[handleOlmRegisterMessage] Client ${client.clientId} is blocked. Ignoring register.`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
sendOlmError(OlmErrorCodes.CLIENT_BLOCKED, olm.olmId);
|
||||
return;
|
||||
@@ -100,7 +106,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
|
||||
if (client.approvalState == "pending") {
|
||||
logger.debug(
|
||||
`Client ${client.clientId} approval is pending. Ignoring register.`
|
||||
`[handleOlmRegisterMessage] Client ${client.clientId} approval is pending. Ignoring register.`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
sendOlmError(OlmErrorCodes.CLIENT_PENDING, olm.olmId);
|
||||
return;
|
||||
@@ -128,14 +135,18 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
.limit(1);
|
||||
|
||||
if (!org) {
|
||||
logger.warn("Org not found");
|
||||
logger.warn("[handleOlmRegisterMessage] Org not found", {
|
||||
orgId: client.orgId
|
||||
});
|
||||
sendOlmError(OlmErrorCodes.ORG_NOT_FOUND, olm.olmId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (orgId) {
|
||||
if (!olm.userId) {
|
||||
logger.warn("Olm has no user ID");
|
||||
logger.warn("[handleOlmRegisterMessage] Olm has no user ID", {
|
||||
orgId: client.orgId
|
||||
});
|
||||
sendOlmError(OlmErrorCodes.USER_ID_NOT_FOUND, olm.olmId);
|
||||
return;
|
||||
}
|
||||
@@ -143,12 +154,18 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
const { session: userSession, user } =
|
||||
await validateSessionToken(userToken);
|
||||
if (!userSession || !user) {
|
||||
logger.warn("Invalid user session for olm register");
|
||||
logger.warn(
|
||||
"[handleOlmRegisterMessage] Invalid user session for olm register",
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
sendOlmError(OlmErrorCodes.INVALID_USER_SESSION, olm.olmId);
|
||||
return;
|
||||
}
|
||||
if (user.userId !== olm.userId) {
|
||||
logger.warn("User ID mismatch for olm register");
|
||||
logger.warn(
|
||||
"[handleOlmRegisterMessage] User ID mismatch for olm register",
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
sendOlmError(OlmErrorCodes.USER_ID_MISMATCH, olm.olmId);
|
||||
return;
|
||||
}
|
||||
@@ -163,11 +180,15 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
sessionId // this is the user token passed in the message
|
||||
});
|
||||
|
||||
logger.debug("Policy check result:", policyCheck);
|
||||
logger.debug("[handleOlmRegisterMessage] Policy check result", {
|
||||
orgId: client.orgId,
|
||||
policyCheck
|
||||
});
|
||||
|
||||
if (policyCheck?.error) {
|
||||
logger.error(
|
||||
`Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}`
|
||||
`[handleOlmRegisterMessage] Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
||||
return;
|
||||
@@ -175,7 +196,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
|
||||
if (policyCheck.policies?.passwordAge?.compliant === false) {
|
||||
logger.warn(
|
||||
`Olm user ${olm.userId} has non-compliant password age for org ${orgId}`
|
||||
`[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant password age for org ${orgId}`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
sendOlmError(
|
||||
OlmErrorCodes.ORG_ACCESS_POLICY_PASSWORD_EXPIRED,
|
||||
@@ -186,7 +208,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
policyCheck.policies?.maxSessionLength?.compliant === false
|
||||
) {
|
||||
logger.warn(
|
||||
`Olm user ${olm.userId} has non-compliant session length for org ${orgId}`
|
||||
`[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant session length for org ${orgId}`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
sendOlmError(
|
||||
OlmErrorCodes.ORG_ACCESS_POLICY_SESSION_EXPIRED,
|
||||
@@ -195,7 +218,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
} else if (policyCheck.policies?.requiredTwoFactor === false) {
|
||||
logger.warn(
|
||||
`Olm user ${olm.userId} does not have 2FA enabled for org ${orgId}`
|
||||
`[handleOlmRegisterMessage] Olm user ${olm.userId} does not have 2FA enabled for org ${orgId}`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
sendOlmError(
|
||||
OlmErrorCodes.ORG_ACCESS_POLICY_2FA_REQUIRED,
|
||||
@@ -204,7 +228,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
} else if (!policyCheck.allowed) {
|
||||
logger.warn(
|
||||
`Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`
|
||||
`[handleOlmRegisterMessage] Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
||||
return;
|
||||
@@ -226,29 +251,39 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
|
||||
|
||||
// Prepare an array to store site configurations
|
||||
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
|
||||
logger.debug(
|
||||
`[handleOlmRegisterMessage] Found ${sitesCount} sites for client ${client.clientId}`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
|
||||
let jitMode = false;
|
||||
if (sitesCount > 250 && build == "saas") {
|
||||
// THIS IS THE MAX ON THE BUSINESS TIER
|
||||
// we have too many sites
|
||||
// If we have too many sites we need to drop into fully JIT mode by not sending any of the sites
|
||||
logger.info("Too many sites (%d), dropping into JIT mode", sitesCount);
|
||||
logger.info(
|
||||
`[handleOlmRegisterMessage] Too many sites (${sitesCount}), dropping into JIT mode`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
jitMode = true;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
|
||||
`[handleOlmRegisterMessage] Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
|
||||
if (!publicKey) {
|
||||
logger.warn("Public key not provided");
|
||||
logger.warn("[handleOlmRegisterMessage] Public key not provided", {
|
||||
orgId: client.orgId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.pubKey !== publicKey || client.archived) {
|
||||
logger.info(
|
||||
"Public key mismatch. Updating public key and clearing session info..."
|
||||
"[handleOlmRegisterMessage] Public key mismatch. Updating public key and clearing session info...",
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
// Update the client's public key
|
||||
await db
|
||||
@@ -274,12 +309,13 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
||||
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
|
||||
logger.warn(
|
||||
`Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
|
||||
`[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`,
|
||||
{ orgId: client.orgId }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: its important that the client here is the old client and the public key is the new key
|
||||
// NOTE: its important that the client here is the old client and the public key is the new key
|
||||
const siteConfigurations = await buildSiteConfigurationForOlmClient(
|
||||
client,
|
||||
publicKey,
|
||||
|
||||
@@ -151,6 +151,8 @@ export async function getUserResources(
|
||||
destination: string;
|
||||
mode: string;
|
||||
scheme: string | null;
|
||||
ssl: boolean;
|
||||
fullDomain: string | null;
|
||||
enabled: boolean;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
@@ -164,6 +166,8 @@ export async function getUserResources(
|
||||
destination: siteResources.destination,
|
||||
mode: siteResources.mode,
|
||||
scheme: siteResources.scheme,
|
||||
ssl: siteResources.ssl,
|
||||
fullDomain: siteResources.fullDomain,
|
||||
enabled: siteResources.enabled,
|
||||
alias: siteResources.alias,
|
||||
aliasAddress: siteResources.aliasAddress
|
||||
@@ -251,6 +255,8 @@ export async function getUserResources(
|
||||
destination: siteResource.destination,
|
||||
mode: siteResource.mode,
|
||||
protocol: siteResource.scheme,
|
||||
ssl: siteResource.ssl,
|
||||
fullDomain: siteResource.fullDomain,
|
||||
enabled: siteResource.enabled,
|
||||
alias: siteResource.alias,
|
||||
aliasAddress: siteResource.aliasAddress,
|
||||
@@ -296,6 +302,8 @@ export type GetUserResourcesResponse = {
|
||||
destination: string;
|
||||
mode: string;
|
||||
protocol: string | null;
|
||||
ssl: boolean;
|
||||
fullDomain: string | null;
|
||||
enabled: boolean;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
clients,
|
||||
clientSiteResources,
|
||||
siteResources,
|
||||
apiKeyOrg
|
||||
apiKeyOrg,
|
||||
primaryDb
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -220,8 +221,12 @@ export async function batchAddClientToSiteResources(
|
||||
siteResourceId: siteResource.siteResourceId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromClient(client, trx);
|
||||
rebuildClientAssociationsFromClient(client, primaryDb).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after batch adding site resources for client ${clientId}: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
SiteResource,
|
||||
siteResources,
|
||||
sites,
|
||||
userSiteResources
|
||||
userSiteResources,
|
||||
primaryDb
|
||||
} from "@server/db";
|
||||
import { getUniqueSiteResourceName } from "@server/db/names";
|
||||
import {
|
||||
@@ -74,16 +75,14 @@ const createSiteResourceSchema = z
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.mode === "host") {
|
||||
if (data.mode == "host") {
|
||||
// Check if it's a valid IP address using zod (v4 or v6)
|
||||
const isValidIP = z
|
||||
// .union([z.ipv4(), z.ipv6()])
|
||||
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||
.safeParse(data.destination).success;
|
||||
// Check if it's a valid IP address using zod (v4 or v6)
|
||||
const isValidIP = z
|
||||
// .union([z.ipv4(), z.ipv6()])
|
||||
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||
.safeParse(data.destination).success;
|
||||
|
||||
if (isValidIP) {
|
||||
return true;
|
||||
}
|
||||
if (isValidIP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a valid domain (hostname pattern, TLD not required)
|
||||
@@ -96,17 +95,12 @@ const createSiteResourceSchema = z
|
||||
data.alias.trim() !== "";
|
||||
|
||||
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Destination must be a valid IPV4 address or valid domain AND alias is required"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.mode === "cidr") {
|
||||
} else if (data.mode === "http") {
|
||||
// we have to have a domainId defined
|
||||
if (!data.domainId) {
|
||||
return false;
|
||||
}
|
||||
} else if (data.mode === "cidr") {
|
||||
// Check if it's a valid CIDR (v4 or v6)
|
||||
const isValidCIDR = z
|
||||
.union([z.cidrv4(), z.cidrv6()])
|
||||
@@ -116,7 +110,8 @@ const createSiteResourceSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||
message:
|
||||
"Destination must be a valid IPV4 address or valid domain AND alias is required"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
@@ -525,12 +520,10 @@ export async function createSiteResource(
|
||||
// own transaction so it always executes on the primary — avoiding any
|
||||
// replica-lag issues while still allowing the HTTP response to return
|
||||
// early.
|
||||
db.transaction(async (trx) => {
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
newSiteResource!,
|
||||
trx
|
||||
);
|
||||
}).catch((err) => {
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
newSiteResource!,
|
||||
primaryDb
|
||||
).catch((err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, newts, sites } from "@server/db";
|
||||
import { db, newts, primaryDb, sites } from "@server/db";
|
||||
import { siteResources } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -73,12 +73,10 @@ export async function deleteSiteResource(
|
||||
// own transaction so it always executes on the primary — avoiding any
|
||||
// replica-lag issues while still allowing the HTTP response to return
|
||||
// early.
|
||||
db.transaction(async (trx) => {
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
removedSiteResource,
|
||||
trx
|
||||
);
|
||||
}).catch((err) => {
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
removedSiteResource,
|
||||
primaryDb
|
||||
).catch((err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
|
||||
@@ -104,6 +104,17 @@ const updateSiteResourceSchema = z
|
||||
data.alias.trim() !== "";
|
||||
|
||||
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
||||
} else if (data.mode === "cidr" && data.destination) {
|
||||
// Check if it's a valid CIDR (v4 or v6)
|
||||
const isValidCIDR = z
|
||||
.union([z.cidrv4(), z.cidrv6()])
|
||||
.safeParse(data.destination).success;
|
||||
return isValidCIDR;
|
||||
} else if (data.mode === "http") {
|
||||
// we have to have a domainId defined
|
||||
if (!data.domainId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -112,21 +123,6 @@ const updateSiteResourceSchema = z
|
||||
"Destination must be a valid IP address or valid domain AND alias is required"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.mode === "cidr" && data.destination) {
|
||||
// Check if it's a valid CIDR (v4 or v6)
|
||||
const isValidCIDR = z
|
||||
.union([z.cidrv4(), z.cidrv6()])
|
||||
.safeParse(data.destination).success;
|
||||
return isValidCIDR;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.mode !== "http") return true;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, orgs } from "@server/db";
|
||||
import { roles, userInviteRoles, userInvites, userOrgs, users } from "@server/db";
|
||||
import { db, orgs, primaryDb } from "@server/db";
|
||||
import {
|
||||
roles,
|
||||
userInviteRoles,
|
||||
userInvites,
|
||||
userOrgs,
|
||||
users
|
||||
} from "@server/db";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -146,9 +152,7 @@ export async function acceptInvite(
|
||||
.from(userInviteRoles)
|
||||
.where(eq(userInviteRoles.inviteId, inviteId));
|
||||
|
||||
const inviteRoleIds = [
|
||||
...new Set(inviteRoleRows.map((r) => r.roleId))
|
||||
];
|
||||
const inviteRoleIds = [...new Set(inviteRoleRows.map((r) => r.roleId))];
|
||||
if (inviteRoleIds.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -193,13 +197,19 @@ export async function acceptInvite(
|
||||
.delete(userInvites)
|
||||
.where(eq(userInvites.inviteId, inviteId));
|
||||
|
||||
await calculateUserClientsForOrgs(existingUser[0].userId, trx);
|
||||
|
||||
logger.debug(
|
||||
`User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}`
|
||||
);
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(existingUser[0].userId, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after accepting invite for user ${existingUser[0].userId}: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return response<AcceptInviteResponse>(res, {
|
||||
data: { accepted: true, orgId: existingInvite.orgId },
|
||||
success: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { clients, db } from "@server/db";
|
||||
import { clients, db, primaryDb, Client } from "@server/db";
|
||||
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
@@ -88,11 +88,11 @@ export async function addUserRoleLegacy(
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUser.isOwner) {
|
||||
if (existingUser.isOwner && role.isAdmin !== true) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Cannot change the role of the owner of the organization"
|
||||
"The organization owner must retain an administrator role"
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -112,6 +112,8 @@ export async function addUserRoleLegacy(
|
||||
);
|
||||
}
|
||||
|
||||
let orgClientsToRebuild: Client[] = [];
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(userOrgRoles)
|
||||
@@ -138,11 +140,19 @@ export async function addUserRoleLegacy(
|
||||
)
|
||||
);
|
||||
|
||||
for (const orgClient of orgClients) {
|
||||
await rebuildClientAssociationsFromClient(orgClient, trx);
|
||||
}
|
||||
orgClientsToRebuild = orgClients;
|
||||
});
|
||||
|
||||
for (const orgClient of orgClientsToRebuild) {
|
||||
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: { ...existingUser, roleId },
|
||||
success: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { db, primaryDb } from "@server/db";
|
||||
import { users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
@@ -53,8 +53,12 @@ export async function adminRemoveUser(
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.delete(users).where(eq(users.userId, userId));
|
||||
});
|
||||
|
||||
await calculateUserClientsForOrgs(userId, trx);
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after removing user ${userId}: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -6,7 +6,7 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { db, orgs } from "@server/db";
|
||||
import { db, orgs, primaryDb } from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
@@ -34,8 +34,7 @@ const bodySchema = z
|
||||
roleId: z.number().int().positive().optional()
|
||||
})
|
||||
.refine(
|
||||
(d) =>
|
||||
(d.roleIds != null && d.roleIds.length > 0) || d.roleId != null,
|
||||
(d) => (d.roleIds != null && d.roleIds.length > 0) || d.roleId != null,
|
||||
{ message: "roleIds or roleId is required", path: ["roleIds"] }
|
||||
)
|
||||
.transform((data) => ({
|
||||
@@ -100,8 +99,14 @@ export async function createOrgUser(
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
const { username, email, name, type, idpId, roleIds: uniqueRoleIds } =
|
||||
parsedBody.data;
|
||||
const {
|
||||
username,
|
||||
email,
|
||||
name,
|
||||
type,
|
||||
idpId,
|
||||
roleIds: uniqueRoleIds
|
||||
} = parsedBody.data;
|
||||
|
||||
if (build == "saas") {
|
||||
const usage = await usageService.getUsage(orgId, FeatureId.USERS);
|
||||
@@ -232,6 +237,7 @@ export async function createOrgUser(
|
||||
);
|
||||
}
|
||||
|
||||
let userIdForClients: string | undefined;
|
||||
await db.transaction(async (trx) => {
|
||||
const [existingUser] = await trx
|
||||
.select()
|
||||
@@ -270,7 +276,7 @@ export async function createOrgUser(
|
||||
{
|
||||
orgId,
|
||||
userId: existingUser.userId,
|
||||
autoProvisioned: false,
|
||||
autoProvisioned: false
|
||||
},
|
||||
uniqueRoleIds,
|
||||
trx
|
||||
@@ -292,20 +298,30 @@ export async function createOrgUser(
|
||||
})
|
||||
.returning();
|
||||
|
||||
await assignUserToOrg(
|
||||
org,
|
||||
{
|
||||
orgId,
|
||||
userId: newUser.userId,
|
||||
autoProvisioned: false,
|
||||
},
|
||||
uniqueRoleIds,
|
||||
trx
|
||||
);
|
||||
await assignUserToOrg(
|
||||
org,
|
||||
{
|
||||
orgId,
|
||||
userId: newUser.userId,
|
||||
autoProvisioned: false
|
||||
},
|
||||
uniqueRoleIds,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
await calculateUserClientsForOrgs(userId, trx);
|
||||
userIdForClients = userId;
|
||||
});
|
||||
|
||||
if (userIdForClients) {
|
||||
calculateUserClientsForOrgs(userIdForClients, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after creating org user: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
|
||||
|
||||
@@ -58,7 +58,8 @@ export async function queryUser(orgId: string, userId: string) {
|
||||
roleIds: roleRows.map((r) => r.roleId),
|
||||
roles: roleRows.map((r) => ({
|
||||
roleId: r.roleId,
|
||||
name: r.roleName ?? ""
|
||||
name: r.roleName ?? "",
|
||||
isAdmin: r.isAdmin === true
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
siteResources,
|
||||
sites,
|
||||
UserOrg,
|
||||
userSiteResources
|
||||
userSiteResources,
|
||||
primaryDb
|
||||
} from "@server/db";
|
||||
import { userOrgs, userResources, users, userSites } from "@server/db";
|
||||
import { and, count, eq, exists, inArray } from "drizzle-orm";
|
||||
@@ -91,25 +92,12 @@ export async function removeUserOrg(
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await removeUserFromOrg(org, userId, trx);
|
||||
});
|
||||
|
||||
// if (build === "saas") {
|
||||
// const [rootUser] = await trx
|
||||
// .select()
|
||||
// .from(users)
|
||||
// .where(eq(users.userId, userId));
|
||||
//
|
||||
// const [leftInOrgs] = await trx
|
||||
// .select({ count: count() })
|
||||
// .from(userOrgs)
|
||||
// .where(eq(userOrgs.userId, userId));
|
||||
//
|
||||
// // if the user is not an internal user and does not belong to any org, delete the entire user
|
||||
// if (rootUser?.type !== UserType.Internal && !leftInOrgs.count) {
|
||||
// await trx.delete(users).where(eq(users.userId, userId));
|
||||
// }
|
||||
// }
|
||||
|
||||
await calculateUserClientsForOrgs(userId, trx);
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after removing user ${userId} from org ${orgId}: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
Reference in New Issue
Block a user