mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-27 17:49:04 +00:00
Compare commits
7 Commits
dependabot
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7506c0420d | ||
|
|
5572822c4a | ||
|
|
ea3f1c341b | ||
|
|
35dffe71cb | ||
|
|
5428bf4ed0 | ||
|
|
9a89579e08 | ||
|
|
2e628fe0e4 |
@@ -1,4 +1,4 @@
|
||||
FROM node:26-alpine
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -172,7 +172,9 @@ export async function applyBlueprint({
|
||||
} catch (err) {
|
||||
blueprintSucceeded = false;
|
||||
blueprintMessage = `Blueprint applied with errors: ${err}`;
|
||||
logger.error(blueprintMessage);
|
||||
logger.debug(
|
||||
`Org ${orgId} blueprint apply issues: ${blueprintMessage}`
|
||||
);
|
||||
error = err;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ type ClientRow = typeof clients.$inferSelect;
|
||||
function runQueuedClientAssociationRebuilds(
|
||||
userId: string,
|
||||
queuedClients: ClientRow[]
|
||||
): void {
|
||||
) {
|
||||
if (queuedClients.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -39,35 +39,29 @@ function runQueuedClientAssociationRebuilds(
|
||||
uniqueClientsById.set(client.clientId, client);
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
for (const client of uniqueClientsById.values()) {
|
||||
try {
|
||||
await rebuildClientAssociationsFromClient(client);
|
||||
} catch (error) {
|
||||
rebuildClientAssociationsFromClient(client).catch((error) => {
|
||||
logger.error(
|
||||
`Failed rebuilding associations for client ${client.clientId} (user ${userId}): ${String(error)}`
|
||||
`Error rebuilding client associations for client ${client.clientId} (user ${userId}): ${String(
|
||||
error
|
||||
)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Queued association rebuild completed for ${uniqueClientsById.size} client(s) (user ${userId})`
|
||||
);
|
||||
})();
|
||||
}
|
||||
|
||||
export async function calculateUserClientsForOrgs(
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
const trx = primaryDb;
|
||||
const queuedAssociationRebuilds: ClientRow[] = [];
|
||||
|
||||
const execute = async (transaction: Transaction | typeof db) => {
|
||||
const queuedAssociationRebuilds: ClientRow[] = [];
|
||||
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
||||
const adminRoleCache = new Map<
|
||||
string,
|
||||
typeof roles.$inferSelect | null
|
||||
>();
|
||||
const adminRoleCache = new Map<string, typeof roles.$inferSelect | null>();
|
||||
const exitNodesCache = new Map<
|
||||
string,
|
||||
Awaited<ReturnType<typeof listExitNodes>>
|
||||
@@ -80,8 +74,7 @@ export async function calculateUserClientsForOrgs(
|
||||
const roleClientAccessCache = new Map<string, boolean>();
|
||||
const userClientAccessCache = new Map<string, boolean>();
|
||||
|
||||
const getOrgOlmKey = (orgId: string, olmId: string) =>
|
||||
`${orgId}:${olmId}`;
|
||||
const getOrgOlmKey = (orgId: string, olmId: string) => `${orgId}:${olmId}`;
|
||||
const getRoleClientKey = (roleId: number, clientId: number) =>
|
||||
`${roleId}:${clientId}`;
|
||||
const getUserClientKey = (cachedUserId: string, clientId: number) =>
|
||||
@@ -92,7 +85,7 @@ export async function calculateUserClientsForOrgs(
|
||||
return orgCache.get(orgId) ?? null;
|
||||
}
|
||||
|
||||
const [org] = await transaction
|
||||
const [org] = await trx
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
@@ -106,7 +99,7 @@ export async function calculateUserClientsForOrgs(
|
||||
return adminRoleCache.get(orgId) ?? null;
|
||||
}
|
||||
|
||||
const [adminRole] = await transaction
|
||||
const [adminRole] = await trx
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||
@@ -147,7 +140,7 @@ export async function calculateUserClientsForOrgs(
|
||||
return existingClientCache.get(key) ?? null;
|
||||
}
|
||||
|
||||
const [existingClient] = await transaction
|
||||
const [existingClient] = await trx
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(
|
||||
@@ -164,16 +157,13 @@ export async function calculateUserClientsForOrgs(
|
||||
return existingClient ?? null;
|
||||
};
|
||||
|
||||
const hasRoleClientAccess = async (
|
||||
roleId: number,
|
||||
clientId: number
|
||||
) => {
|
||||
const hasRoleClientAccess = async (roleId: number, clientId: number) => {
|
||||
const key = getRoleClientKey(roleId, clientId);
|
||||
if (roleClientAccessCache.has(key)) {
|
||||
return roleClientAccessCache.get(key)!;
|
||||
}
|
||||
|
||||
const [existingRoleClient] = await transaction
|
||||
const [existingRoleClient] = await trx
|
||||
.select()
|
||||
.from(roleClients)
|
||||
.where(
|
||||
@@ -199,7 +189,7 @@ export async function calculateUserClientsForOrgs(
|
||||
return userClientAccessCache.get(key)!;
|
||||
}
|
||||
|
||||
const [existingUserClient] = await transaction
|
||||
const [existingUserClient] = await trx
|
||||
.select()
|
||||
.from(userClients)
|
||||
.where(
|
||||
@@ -217,7 +207,7 @@ export async function calculateUserClientsForOrgs(
|
||||
};
|
||||
|
||||
// Get all OLMs for this user
|
||||
const userOlms = await transaction
|
||||
const userOlms = await trx
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(eq(olms.userId, userId));
|
||||
@@ -226,7 +216,7 @@ export async function calculateUserClientsForOrgs(
|
||||
// No OLMs for this user, but we should still clean up any orphaned clients
|
||||
await cleanupOrphanedClients(
|
||||
userId,
|
||||
transaction,
|
||||
trx,
|
||||
[],
|
||||
queuedAssociationRebuilds
|
||||
);
|
||||
@@ -234,7 +224,7 @@ export async function calculateUserClientsForOrgs(
|
||||
}
|
||||
|
||||
// Get all user orgs with all roles (for org list and role-based logic)
|
||||
const userOrgRoleRows = await transaction
|
||||
const userOrgRoleRows = await trx
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.innerJoin(
|
||||
@@ -250,10 +240,7 @@ export async function calculateUserClientsForOrgs(
|
||||
const userOrgIds = [
|
||||
...new Set(userOrgRoleRows.map((r) => r.userOrgs.orgId))
|
||||
];
|
||||
const orgIdToRoleRows = new Map<
|
||||
string,
|
||||
(typeof userOrgRoleRows)[0][]
|
||||
>();
|
||||
const orgIdToRoleRows = new Map<string, (typeof userOrgRoleRows)[0][]>();
|
||||
for (const r of userOrgRoleRows) {
|
||||
const list = orgIdToRoleRows.get(r.userOrgs.orgId) ?? [];
|
||||
list.push(r);
|
||||
@@ -300,10 +287,7 @@ export async function calculateUserClientsForOrgs(
|
||||
}
|
||||
|
||||
// Check if a client already exists for this OLM+user+org combination
|
||||
const existingClient = await getExistingClient(
|
||||
orgId,
|
||||
olm.olmId
|
||||
);
|
||||
const existingClient = await getExistingClient(orgId, olm.olmId);
|
||||
|
||||
if (existingClient) {
|
||||
// Ensure admin role has access to the client
|
||||
@@ -313,7 +297,7 @@ export async function calculateUserClientsForOrgs(
|
||||
);
|
||||
|
||||
if (!hasRoleAccess) {
|
||||
await transaction.insert(roleClients).values({
|
||||
await trx.insert(roleClients).values({
|
||||
roleId: adminRole.roleId,
|
||||
clientId: existingClient.clientId
|
||||
});
|
||||
@@ -336,7 +320,7 @@ export async function calculateUserClientsForOrgs(
|
||||
);
|
||||
|
||||
if (!hasUserAccess) {
|
||||
await transaction.insert(userClients).values({
|
||||
await trx.insert(userClients).values({
|
||||
userId,
|
||||
clientId: existingClient.clientId
|
||||
});
|
||||
@@ -366,13 +350,11 @@ export async function calculateUserClientsForOrgs(
|
||||
}
|
||||
|
||||
const randomExitNode =
|
||||
exitNodesList[
|
||||
Math.floor(Math.random() * exitNodesList.length)
|
||||
];
|
||||
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
|
||||
|
||||
// Get next available subnet
|
||||
const { value: newSubnet, release: releaseSubnetLock } =
|
||||
await getNextAvailableClientSubnet(orgId, transaction);
|
||||
await getNextAvailableClientSubnet(orgId, trx);
|
||||
|
||||
const subnet = newSubnet.split("/")[0];
|
||||
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
|
||||
@@ -398,19 +380,16 @@ export async function calculateUserClientsForOrgs(
|
||||
};
|
||||
|
||||
// Create the client
|
||||
const [newClient] = await transaction
|
||||
const [newClient] = await trx
|
||||
.insert(clients)
|
||||
.values(newClientData)
|
||||
.returning();
|
||||
await releaseSubnetLock();
|
||||
existingClientCache.set(
|
||||
getOrgOlmKey(orgId, olm.olmId),
|
||||
newClient
|
||||
);
|
||||
existingClientCache.set(getOrgOlmKey(orgId, olm.olmId), newClient);
|
||||
|
||||
// create approval request
|
||||
if (requireApproval) {
|
||||
await transaction
|
||||
await trx
|
||||
.insert(approvals)
|
||||
.values({
|
||||
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||
@@ -425,7 +404,7 @@ export async function calculateUserClientsForOrgs(
|
||||
queuedAssociationRebuilds.push(newClient);
|
||||
|
||||
// Grant admin role access to the client
|
||||
await transaction.insert(roleClients).values({
|
||||
await trx.insert(roleClients).values({
|
||||
roleId: adminRole.roleId,
|
||||
clientId: newClient.clientId
|
||||
});
|
||||
@@ -435,7 +414,7 @@ export async function calculateUserClientsForOrgs(
|
||||
);
|
||||
|
||||
// Grant user access to the client
|
||||
await transaction.insert(userClients).values({
|
||||
await trx.insert(userClients).values({
|
||||
userId,
|
||||
clientId: newClient.clientId
|
||||
});
|
||||
@@ -453,11 +432,10 @@ export async function calculateUserClientsForOrgs(
|
||||
// Clean up clients in orgs the user is no longer in
|
||||
await cleanupOrphanedClients(
|
||||
userId,
|
||||
transaction,
|
||||
trx,
|
||||
userOrgIds,
|
||||
queuedAssociationRebuilds
|
||||
);
|
||||
};
|
||||
|
||||
runQueuedClientAssociationRebuilds(userId, queuedAssociationRebuilds);
|
||||
}
|
||||
@@ -496,7 +474,7 @@ async function cleanupOrphanedClients(
|
||||
)
|
||||
.returning();
|
||||
|
||||
// Queue deleted clients for post-transaction association cleanup.
|
||||
// Queue deleted clients for post-trx association cleanup.
|
||||
for (const deletedClient of deletedClients) {
|
||||
queuedAssociationRebuilds.push(deletedClient);
|
||||
|
||||
|
||||
@@ -197,15 +197,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
policyCheck
|
||||
});
|
||||
|
||||
if (policyCheck?.error) {
|
||||
logger.error(
|
||||
`[handleOlmRegisterMessage] Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}`,
|
||||
{ orgId: client.orgId, clientId: client.clientId }
|
||||
);
|
||||
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (policyCheck.policies?.passwordAge?.compliant === false) {
|
||||
logger.warn(
|
||||
`[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant password age for org ${orgId}`,
|
||||
@@ -238,7 +229,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
olm.olmId
|
||||
);
|
||||
return;
|
||||
} else if (!policyCheck.allowed) {
|
||||
} else if (!policyCheck.allowed || policyCheck.error) {
|
||||
logger.warn(
|
||||
`[handleOlmRegisterMessage] Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`,
|
||||
{ orgId: client.orgId, clientId: client.clientId }
|
||||
|
||||
@@ -76,6 +76,15 @@ export async function setResourcePolicyHeaderAuth(
|
||||
const { resourcePolicyId } = parsedParams.data;
|
||||
const { headerAuth } = parsedBody.data;
|
||||
|
||||
const headerAuthHash =
|
||||
headerAuth !== null
|
||||
? await hashPassword(
|
||||
Buffer.from(
|
||||
`${headerAuth.user}:${headerAuth.password}`
|
||||
).toString("base64")
|
||||
)
|
||||
: null;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(resourcePolicyHeaderAuth)
|
||||
@@ -86,13 +95,7 @@ export async function setResourcePolicyHeaderAuth(
|
||||
)
|
||||
);
|
||||
|
||||
if (headerAuth !== null) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(
|
||||
`${headerAuth.user}:${headerAuth.password}`
|
||||
).toString("base64")
|
||||
);
|
||||
|
||||
if (headerAuth !== null && headerAuthHash !== null) {
|
||||
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||
resourcePolicyId,
|
||||
headerAuthHash,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, resourcePolicyRules, resourcePolicies } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq, notInArray } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const ruleSchema = z.strictObject({
|
||||
ruleId: z.int().positive().optional(),
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({
|
||||
type: "string",
|
||||
enum: ["ACCEPT", "DROP", "PASS"],
|
||||
@@ -121,17 +122,74 @@ export async function setResourcePolicyRules(
|
||||
.set({ applyRules })
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||
|
||||
const incomingRuleIds = rules
|
||||
.map((r) => r.ruleId)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
|
||||
// Delete rules that are no longer in the incoming list
|
||||
if (incomingRuleIds.length > 0) {
|
||||
await trx
|
||||
.delete(resourcePolicyRules)
|
||||
.where(
|
||||
eq(resourcePolicyRules.resourcePolicyId, resourcePolicyId)
|
||||
and(
|
||||
eq(
|
||||
resourcePolicyRules.resourcePolicyId,
|
||||
resourcePolicyId
|
||||
),
|
||||
notInArray(
|
||||
resourcePolicyRules.ruleId,
|
||||
incomingRuleIds
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
await trx
|
||||
.delete(resourcePolicyRules)
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicyRules.resourcePolicyId,
|
||||
resourcePolicyId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (rules.length > 0) {
|
||||
// Update existing rules (those with a ruleId)
|
||||
const existingRules = rules.filter(
|
||||
(r): r is typeof r & { ruleId: number } =>
|
||||
r.ruleId !== undefined
|
||||
);
|
||||
for (const rule of existingRules) {
|
||||
await trx
|
||||
.update(resourcePolicyRules)
|
||||
.set({
|
||||
action: rule.action,
|
||||
match: rule.match,
|
||||
value: rule.value,
|
||||
priority: rule.priority,
|
||||
enabled: rule.enabled
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(resourcePolicyRules.ruleId, rule.ruleId),
|
||||
eq(
|
||||
resourcePolicyRules.resourcePolicyId,
|
||||
resourcePolicyId
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Insert new rules (those without a ruleId)
|
||||
const newRules = rules.filter((r) => r.ruleId === undefined);
|
||||
if (newRules.length > 0) {
|
||||
await trx.insert(resourcePolicyRules).values(
|
||||
rules.map((rule) => ({
|
||||
newRules.map((rule) => ({
|
||||
resourcePolicyId,
|
||||
...rule
|
||||
action: rule.action,
|
||||
match: rule.match,
|
||||
value: rule.value,
|
||||
priority: rule.priority,
|
||||
enabled: rule.enabled
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,13 @@ export async function setResourceHeaderAuth(
|
||||
resource.resourcePolicyId === null &&
|
||||
resource.defaultResourcePolicyId !== null;
|
||||
|
||||
const headerAuthHash =
|
||||
user && password && extendedCompatibility !== null
|
||||
? await hashPassword(
|
||||
Buffer.from(`${user}:${password}`).toString("base64")
|
||||
)
|
||||
: null;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
if (isInlinePolicy) {
|
||||
const policyId = resource.defaultResourcePolicyId!;
|
||||
@@ -116,11 +123,7 @@ export async function setResourceHeaderAuth(
|
||||
eq(resourcePolicyHeaderAuth.resourcePolicyId, policyId)
|
||||
);
|
||||
|
||||
if (user && password && extendedCompatibility !== null) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(`${user}:${password}`).toString("base64")
|
||||
);
|
||||
|
||||
if (headerAuthHash !== null && extendedCompatibility !== null) {
|
||||
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||
resourcePolicyId: policyId,
|
||||
headerAuthHash,
|
||||
@@ -140,11 +143,7 @@ export async function setResourceHeaderAuth(
|
||||
)
|
||||
);
|
||||
|
||||
if (user && password && extendedCompatibility !== null) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(`${user}:${password}`).toString("base64")
|
||||
);
|
||||
|
||||
if (headerAuthHash !== null && extendedCompatibility !== null) {
|
||||
await Promise.all([
|
||||
trx
|
||||
.insert(resourceHeaderAuth)
|
||||
|
||||
@@ -340,7 +340,8 @@ function PolicyAccessRulesSectionEdit({
|
||||
? rules.filter((rule) => !rule.fromPolicy)
|
||||
: rules;
|
||||
const rulesPayload = rulesToValidate.map(
|
||||
({ action, match, value, priority, enabled }) => ({
|
||||
({ ruleId, action, match, value, priority, enabled, new: isNew }) => ({
|
||||
...(isNew ? {} : { ruleId }),
|
||||
action,
|
||||
match,
|
||||
value,
|
||||
|
||||
Reference in New Issue
Block a user