Compare commits

..

1 Commits
dev ... 1.19.3

Author SHA1 Message Date
Owen Schwartz
7590e8d8a1 Merge pull request #3345 from fosrl/dev
Show utility subnet on org
2026-06-25 13:05:32 -07:00
7 changed files with 423 additions and 455 deletions

View File

@@ -172,9 +172,7 @@ export async function applyBlueprint({
} catch (err) { } catch (err) {
blueprintSucceeded = false; blueprintSucceeded = false;
blueprintMessage = `Blueprint applied with errors: ${err}`; blueprintMessage = `Blueprint applied with errors: ${err}`;
logger.debug( logger.error(blueprintMessage);
`Org ${orgId} blueprint apply issues: ${blueprintMessage}`
);
error = err; error = err;
} }

View File

@@ -29,7 +29,7 @@ type ClientRow = typeof clients.$inferSelect;
function runQueuedClientAssociationRebuilds( function runQueuedClientAssociationRebuilds(
userId: string, userId: string,
queuedClients: ClientRow[] queuedClients: ClientRow[]
) { ): void {
if (queuedClients.length === 0) { if (queuedClients.length === 0) {
return; return;
} }
@@ -39,403 +39,425 @@ function runQueuedClientAssociationRebuilds(
uniqueClientsById.set(client.clientId, client); uniqueClientsById.set(client.clientId, client);
} }
for (const client of uniqueClientsById.values()) { void (async () => {
rebuildClientAssociationsFromClient(client).catch((error) => { for (const client of uniqueClientsById.values()) {
logger.error( try {
`Error rebuilding client associations for client ${client.clientId} (user ${userId}): ${String( await rebuildClientAssociationsFromClient(client);
error } catch (error) {
)}` logger.error(
); `Failed rebuilding associations for client ${client.clientId} (user ${userId}): ${String(error)}`
}); );
} }
}
logger.debug( logger.debug(
`Queued association rebuild completed for ${uniqueClientsById.size} client(s) (user ${userId})` `Queued association rebuild completed for ${uniqueClientsById.size} client(s) (user ${userId})`
); );
})();
} }
export async function calculateUserClientsForOrgs( export async function calculateUserClientsForOrgs(
userId: string userId: string
): Promise<void> { ): Promise<void> {
const trx = primaryDb; const trx = primaryDb;
const queuedAssociationRebuilds: ClientRow[] = []; const queuedAssociationRebuilds: ClientRow[] = [];
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
const adminRoleCache = new Map<string, typeof roles.$inferSelect | null>();
const exitNodesCache = new Map<
string,
Awaited<ReturnType<typeof listExitNodes>>
>();
const isOrgLicensedCache = new Map<string, boolean>();
const existingClientCache = new Map<
string,
typeof clients.$inferSelect | null
>();
const roleClientAccessCache = new Map<string, boolean>();
const userClientAccessCache = new Map<string, boolean>();
const getOrgOlmKey = (orgId: string, olmId: string) => `${orgId}:${olmId}`; const execute = async (transaction: Transaction | typeof db) => {
const getRoleClientKey = (roleId: number, clientId: number) => const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
`${roleId}:${clientId}`; const adminRoleCache = new Map<
const getUserClientKey = (cachedUserId: string, clientId: number) => string,
`${cachedUserId}:${clientId}`; typeof roles.$inferSelect | null
>();
const exitNodesCache = new Map<
string,
Awaited<ReturnType<typeof listExitNodes>>
>();
const isOrgLicensedCache = new Map<string, boolean>();
const existingClientCache = new Map<
string,
typeof clients.$inferSelect | null
>();
const roleClientAccessCache = new Map<string, boolean>();
const userClientAccessCache = new Map<string, boolean>();
const getOrg = async (orgId: string) => { const getOrgOlmKey = (orgId: string, olmId: string) =>
if (orgCache.has(orgId)) { `${orgId}:${olmId}`;
return orgCache.get(orgId) ?? null; const getRoleClientKey = (roleId: number, clientId: number) =>
} `${roleId}:${clientId}`;
const getUserClientKey = (cachedUserId: string, clientId: number) =>
`${cachedUserId}:${clientId}`;
const [org] = await trx const getOrg = async (orgId: string) => {
.select() if (orgCache.has(orgId)) {
.from(orgs) return orgCache.get(orgId) ?? null;
.where(eq(orgs.orgId, orgId));
orgCache.set(orgId, org ?? null);
return org ?? null;
};
const getAdminRole = async (orgId: string) => {
if (adminRoleCache.has(orgId)) {
return adminRoleCache.get(orgId) ?? null;
}
const [adminRole] = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
adminRoleCache.set(orgId, adminRole ?? null);
return adminRole ?? null;
};
const getExitNodes = async (orgId: string) => {
if (exitNodesCache.has(orgId)) {
return exitNodesCache.get(orgId)!;
}
const exitNodes = await listExitNodes(orgId);
exitNodesCache.set(orgId, exitNodes);
return exitNodes;
};
const getIsOrgLicensed = async (orgId: string) => {
if (isOrgLicensedCache.has(orgId)) {
return isOrgLicensedCache.get(orgId)!;
}
const isOrgLicensed = await isLicensedOrSubscribed(
orgId,
tierMatrix.deviceApprovals
);
isOrgLicensedCache.set(orgId, isOrgLicensed);
return isOrgLicensed;
};
const getExistingClient = async (orgId: string, olmId: string) => {
const key = getOrgOlmKey(orgId, olmId);
if (existingClientCache.has(key)) {
return existingClientCache.get(key) ?? null;
}
const [existingClient] = await trx
.select()
.from(clients)
.where(
and(
eq(clients.userId, userId),
eq(clients.orgId, orgId),
eq(clients.olmId, olmId)
)
)
.limit(1);
existingClientCache.set(key, existingClient ?? null);
return existingClient ?? null;
};
const hasRoleClientAccess = async (roleId: number, clientId: number) => {
const key = getRoleClientKey(roleId, clientId);
if (roleClientAccessCache.has(key)) {
return roleClientAccessCache.get(key)!;
}
const [existingRoleClient] = await trx
.select()
.from(roleClients)
.where(
and(
eq(roleClients.roleId, roleId),
eq(roleClients.clientId, clientId)
)
)
.limit(1);
const hasAccess = Boolean(existingRoleClient);
roleClientAccessCache.set(key, hasAccess);
return hasAccess;
};
const hasUserClientAccess = async (
cachedUserId: string,
clientId: number
) => {
const key = getUserClientKey(cachedUserId, clientId);
if (userClientAccessCache.has(key)) {
return userClientAccessCache.get(key)!;
}
const [existingUserClient] = await trx
.select()
.from(userClients)
.where(
and(
eq(userClients.userId, cachedUserId),
eq(userClients.clientId, clientId)
)
)
.limit(1);
const hasAccess = Boolean(existingUserClient);
userClientAccessCache.set(key, hasAccess);
return hasAccess;
};
// Get all OLMs for this user
const userOlms = await trx
.select()
.from(olms)
.where(eq(olms.userId, userId));
if (userOlms.length === 0) {
// No OLMs for this user, but we should still clean up any orphaned clients
await cleanupOrphanedClients(
userId,
trx,
[],
queuedAssociationRebuilds
);
return;
}
// Get all user orgs with all roles (for org list and role-based logic)
const userOrgRoleRows = await trx
.select()
.from(userOrgs)
.innerJoin(
userOrgRoles,
and(
eq(userOrgs.userId, userOrgRoles.userId),
eq(userOrgs.orgId, userOrgRoles.orgId)
)
)
.innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where(eq(userOrgs.userId, userId));
const userOrgIds = [
...new Set(userOrgRoleRows.map((r) => r.userOrgs.orgId))
];
const orgIdToRoleRows = new Map<string, (typeof userOrgRoleRows)[0][]>();
for (const r of userOrgRoleRows) {
const list = orgIdToRoleRows.get(r.userOrgs.orgId) ?? [];
list.push(r);
orgIdToRoleRows.set(r.userOrgs.orgId, list);
}
const orgRequiresDeviceApprovalRole = new Map<string, boolean>();
for (const [orgId, roleRowsForOrg] of orgIdToRoleRows.entries()) {
orgRequiresDeviceApprovalRole.set(
orgId,
roleRowsForOrg.some((r) => r.roles.requireDeviceApproval)
);
}
// For each OLM, ensure there's a client in each org the user is in
for (const olm of userOlms) {
for (const orgId of orgIdToRoleRows.keys()) {
const roleRowsForOrg = orgIdToRoleRows.get(orgId)!;
const userOrg = roleRowsForOrg[0].userOrgs;
const org = await getOrg(orgId);
if (!org) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org not found`
);
continue;
} }
if (!org.subnet) { const [org] = await transaction
logger.warn( .select()
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org has no subnet configured` .from(orgs)
); .where(eq(orgs.orgId, orgId));
continue; orgCache.set(orgId, org ?? null);
return org ?? null;
};
const getAdminRole = async (orgId: string) => {
if (adminRoleCache.has(orgId)) {
return adminRoleCache.get(orgId) ?? null;
} }
// Get admin role for this org (needed for access grants) const [adminRole] = await transaction
const adminRole = await getAdminRole(orgId); .select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
adminRoleCache.set(orgId, adminRole ?? null);
if (!adminRole) { return adminRole ?? null;
logger.warn( };
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no admin role found`
); const getExitNodes = async (orgId: string) => {
continue; if (exitNodesCache.has(orgId)) {
return exitNodesCache.get(orgId)!;
} }
// Check if a client already exists for this OLM+user+org combination const exitNodes = await listExitNodes(orgId);
const existingClient = await getExistingClient(orgId, olm.olmId); exitNodesCache.set(orgId, exitNodes);
if (existingClient) { return exitNodes;
// Ensure admin role has access to the client };
const hasRoleAccess = await hasRoleClientAccess(
adminRole.roleId,
existingClient.clientId
);
if (!hasRoleAccess) { const getIsOrgLicensed = async (orgId: string) => {
await trx.insert(roleClients).values({ if (isOrgLicensedCache.has(orgId)) {
roleId: adminRole.roleId, return isOrgLicensedCache.get(orgId)!;
clientId: existingClient.clientId }
});
roleClientAccessCache.set( const isOrgLicensed = await isLicensedOrSubscribed(
getRoleClientKey( orgId,
adminRole.roleId, tierMatrix.deviceApprovals
existingClient.clientId );
), isOrgLicensedCache.set(orgId, isOrgLicensed);
true
); return isOrgLicensed;
logger.debug( };
`Granted admin role access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
const getExistingClient = async (orgId: string, olmId: string) => {
const key = getOrgOlmKey(orgId, olmId);
if (existingClientCache.has(key)) {
return existingClientCache.get(key) ?? null;
}
const [existingClient] = await transaction
.select()
.from(clients)
.where(
and(
eq(clients.userId, userId),
eq(clients.orgId, orgId),
eq(clients.olmId, olmId)
)
)
.limit(1);
existingClientCache.set(key, existingClient ?? null);
return existingClient ?? null;
};
const hasRoleClientAccess = async (
roleId: number,
clientId: number
) => {
const key = getRoleClientKey(roleId, clientId);
if (roleClientAccessCache.has(key)) {
return roleClientAccessCache.get(key)!;
}
const [existingRoleClient] = await transaction
.select()
.from(roleClients)
.where(
and(
eq(roleClients.roleId, roleId),
eq(roleClients.clientId, clientId)
)
)
.limit(1);
const hasAccess = Boolean(existingRoleClient);
roleClientAccessCache.set(key, hasAccess);
return hasAccess;
};
const hasUserClientAccess = async (
cachedUserId: string,
clientId: number
) => {
const key = getUserClientKey(cachedUserId, clientId);
if (userClientAccessCache.has(key)) {
return userClientAccessCache.get(key)!;
}
const [existingUserClient] = await transaction
.select()
.from(userClients)
.where(
and(
eq(userClients.userId, cachedUserId),
eq(userClients.clientId, clientId)
)
)
.limit(1);
const hasAccess = Boolean(existingUserClient);
userClientAccessCache.set(key, hasAccess);
return hasAccess;
};
// Get all OLMs for this user
const userOlms = await transaction
.select()
.from(olms)
.where(eq(olms.userId, userId));
if (userOlms.length === 0) {
// No OLMs for this user, but we should still clean up any orphaned clients
await cleanupOrphanedClients(
userId,
transaction,
[],
queuedAssociationRebuilds
);
return;
}
// Get all user orgs with all roles (for org list and role-based logic)
const userOrgRoleRows = await transaction
.select()
.from(userOrgs)
.innerJoin(
userOrgRoles,
and(
eq(userOrgs.userId, userOrgRoles.userId),
eq(userOrgs.orgId, userOrgRoles.orgId)
)
)
.innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where(eq(userOrgs.userId, userId));
const userOrgIds = [
...new Set(userOrgRoleRows.map((r) => r.userOrgs.orgId))
];
const orgIdToRoleRows = new Map<
string,
(typeof userOrgRoleRows)[0][]
>();
for (const r of userOrgRoleRows) {
const list = orgIdToRoleRows.get(r.userOrgs.orgId) ?? [];
list.push(r);
orgIdToRoleRows.set(r.userOrgs.orgId, list);
}
const orgRequiresDeviceApprovalRole = new Map<string, boolean>();
for (const [orgId, roleRowsForOrg] of orgIdToRoleRows.entries()) {
orgRequiresDeviceApprovalRole.set(
orgId,
roleRowsForOrg.some((r) => r.roles.requireDeviceApproval)
);
}
// For each OLM, ensure there's a client in each org the user is in
for (const olm of userOlms) {
for (const orgId of orgIdToRoleRows.keys()) {
const roleRowsForOrg = orgIdToRoleRows.get(orgId)!;
const userOrg = roleRowsForOrg[0].userOrgs;
const org = await getOrg(orgId);
if (!org) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org not found`
); );
continue;
} }
// Ensure user has access to the client if (!org.subnet) {
const hasUserAccess = await hasUserClientAccess( logger.warn(
userId, `Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org has no subnet configured`
existingClient.clientId );
continue;
}
// Get admin role for this org (needed for access grants)
const adminRole = await getAdminRole(orgId);
if (!adminRole) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no admin role found`
);
continue;
}
// Check if a client already exists for this OLM+user+org combination
const existingClient = await getExistingClient(
orgId,
olm.olmId
); );
if (!hasUserAccess) { if (existingClient) {
await trx.insert(userClients).values({ // Ensure admin role has access to the client
const hasRoleAccess = await hasRoleClientAccess(
adminRole.roleId,
existingClient.clientId
);
if (!hasRoleAccess) {
await transaction.insert(roleClients).values({
roleId: adminRole.roleId,
clientId: existingClient.clientId
});
roleClientAccessCache.set(
getRoleClientKey(
adminRole.roleId,
existingClient.clientId
),
true
);
logger.debug(
`Granted admin role access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
);
}
// Ensure user has access to the client
const hasUserAccess = await hasUserClientAccess(
userId, userId,
clientId: existingClient.clientId existingClient.clientId
});
userClientAccessCache.set(
getUserClientKey(userId, existingClient.clientId),
true
); );
if (!hasUserAccess) {
await transaction.insert(userClients).values({
userId,
clientId: existingClient.clientId
});
userClientAccessCache.set(
getUserClientKey(userId, existingClient.clientId),
true
);
logger.debug(
`Granted user access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
);
}
logger.debug( logger.debug(
`Granted user access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})` `Client already exists for OLM ${olm.olmId} in org ${orgId} (user ${userId}), skipping creation`
); );
continue;
} }
// Get exit nodes for this org
const exitNodesList = await getExitNodes(orgId);
if (exitNodesList.length === 0) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no exit nodes found`
);
continue;
}
const randomExitNode =
exitNodesList[
Math.floor(Math.random() * exitNodesList.length)
];
// Get next available subnet
const { value: newSubnet, release: releaseSubnetLock } =
await getNextAvailableClientSubnet(orgId, transaction);
const subnet = newSubnet.split("/")[0];
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
const niceId = await getUniqueClientName(orgId);
const isOrgLicensed = await getIsOrgLicensed(userOrg.orgId);
const requireApproval =
build !== "oss" &&
isOrgLicensed &&
orgRequiresDeviceApprovalRole.get(orgId) === true;
const newClientData: InferInsertModel<typeof clients> = {
userId,
orgId: userOrg.orgId,
exitNodeId: randomExitNode.exitNodeId,
name: olm.name || "User Client",
subnet: updatedSubnet,
olmId: olm.olmId,
type: "olm",
niceId,
approvalState: requireApproval ? "pending" : null
};
// Create the client
const [newClient] = await transaction
.insert(clients)
.values(newClientData)
.returning();
await releaseSubnetLock();
existingClientCache.set(
getOrgOlmKey(orgId, olm.olmId),
newClient
);
// create approval request
if (requireApproval) {
await transaction
.insert(approvals)
.values({
timestamp: Math.floor(new Date().getTime() / 1000),
orgId: userOrg.orgId,
clientId: newClient.clientId,
userId,
type: "user_device"
})
.returning();
}
queuedAssociationRebuilds.push(newClient);
// Grant admin role access to the client
await transaction.insert(roleClients).values({
roleId: adminRole.roleId,
clientId: newClient.clientId
});
roleClientAccessCache.set(
getRoleClientKey(adminRole.roleId, newClient.clientId),
true
);
// Grant user access to the client
await transaction.insert(userClients).values({
userId,
clientId: newClient.clientId
});
userClientAccessCache.set(
getUserClientKey(userId, newClient.clientId),
true
);
logger.debug( logger.debug(
`Client already exists for OLM ${olm.olmId} in org ${orgId} (user ${userId}), skipping creation` `Created client for OLM ${olm.olmId} in org ${orgId} (user ${userId}) with access granted to admin role and user`
); );
continue;
} }
// Get exit nodes for this org
const exitNodesList = await getExitNodes(orgId);
if (exitNodesList.length === 0) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no exit nodes found`
);
continue;
}
const randomExitNode =
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
// Get next available subnet
const { value: newSubnet, release: releaseSubnetLock } =
await getNextAvailableClientSubnet(orgId, trx);
const subnet = newSubnet.split("/")[0];
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
const niceId = await getUniqueClientName(orgId);
const isOrgLicensed = await getIsOrgLicensed(userOrg.orgId);
const requireApproval =
build !== "oss" &&
isOrgLicensed &&
orgRequiresDeviceApprovalRole.get(orgId) === true;
const newClientData: InferInsertModel<typeof clients> = {
userId,
orgId: userOrg.orgId,
exitNodeId: randomExitNode.exitNodeId,
name: olm.name || "User Client",
subnet: updatedSubnet,
olmId: olm.olmId,
type: "olm",
niceId,
approvalState: requireApproval ? "pending" : null
};
// Create the client
const [newClient] = await trx
.insert(clients)
.values(newClientData)
.returning();
await releaseSubnetLock();
existingClientCache.set(getOrgOlmKey(orgId, olm.olmId), newClient);
// create approval request
if (requireApproval) {
await trx
.insert(approvals)
.values({
timestamp: Math.floor(new Date().getTime() / 1000),
orgId: userOrg.orgId,
clientId: newClient.clientId,
userId,
type: "user_device"
})
.returning();
}
queuedAssociationRebuilds.push(newClient);
// Grant admin role access to the client
await trx.insert(roleClients).values({
roleId: adminRole.roleId,
clientId: newClient.clientId
});
roleClientAccessCache.set(
getRoleClientKey(adminRole.roleId, newClient.clientId),
true
);
// Grant user access to the client
await trx.insert(userClients).values({
userId,
clientId: newClient.clientId
});
userClientAccessCache.set(
getUserClientKey(userId, newClient.clientId),
true
);
logger.debug(
`Created client for OLM ${olm.olmId} in org ${orgId} (user ${userId}) with access granted to admin role and user`
);
} }
}
// Clean up clients in orgs the user is no longer in // Clean up clients in orgs the user is no longer in
await cleanupOrphanedClients( await cleanupOrphanedClients(
userId, userId,
trx, transaction,
userOrgIds, userOrgIds,
queuedAssociationRebuilds queuedAssociationRebuilds
); );
};
runQueuedClientAssociationRebuilds(userId, queuedAssociationRebuilds); runQueuedClientAssociationRebuilds(userId, queuedAssociationRebuilds);
} }
@@ -474,7 +496,7 @@ async function cleanupOrphanedClients(
) )
.returning(); .returning();
// Queue deleted clients for post-trx association cleanup. // Queue deleted clients for post-transaction association cleanup.
for (const deletedClient of deletedClients) { for (const deletedClient of deletedClients) {
queuedAssociationRebuilds.push(deletedClient); queuedAssociationRebuilds.push(deletedClient);

View File

@@ -197,6 +197,15 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
policyCheck 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) { if (policyCheck.policies?.passwordAge?.compliant === false) {
logger.warn( logger.warn(
`[handleOlmRegisterMessage] 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}`,
@@ -229,7 +238,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
olm.olmId olm.olmId
); );
return; return;
} else if (!policyCheck.allowed || policyCheck.error) { } else if (!policyCheck.allowed) {
logger.warn( logger.warn(
`[handleOlmRegisterMessage] 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, clientId: client.clientId } { orgId: client.orgId, clientId: client.clientId }

View File

@@ -76,15 +76,6 @@ export async function setResourcePolicyHeaderAuth(
const { resourcePolicyId } = parsedParams.data; const { resourcePolicyId } = parsedParams.data;
const { headerAuth } = parsedBody.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 db.transaction(async (trx) => {
await trx await trx
.delete(resourcePolicyHeaderAuth) .delete(resourcePolicyHeaderAuth)
@@ -95,7 +86,13 @@ export async function setResourcePolicyHeaderAuth(
) )
); );
if (headerAuth !== null && headerAuthHash !== null) { if (headerAuth !== null) {
const headerAuthHash = await hashPassword(
Buffer.from(
`${headerAuth.user}:${headerAuth.password}`
).toString("base64")
);
await trx.insert(resourcePolicyHeaderAuth).values({ await trx.insert(resourcePolicyHeaderAuth).values({
resourcePolicyId, resourcePolicyId,
headerAuthHash, headerAuthHash,

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, resourcePolicyRules, resourcePolicies } from "@server/db"; import { db, resourcePolicyRules, resourcePolicies } from "@server/db";
import { and, eq, notInArray } from "drizzle-orm"; import { eq } 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";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -14,7 +14,6 @@ import {
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
const ruleSchema = z.strictObject({ const ruleSchema = z.strictObject({
ruleId: z.int().positive().optional(),
action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({ action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({
type: "string", type: "string",
enum: ["ACCEPT", "DROP", "PASS"], enum: ["ACCEPT", "DROP", "PASS"],
@@ -122,74 +121,17 @@ export async function setResourcePolicyRules(
.set({ applyRules }) .set({ applyRules })
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
const incomingRuleIds = rules await trx
.map((r) => r.ruleId) .delete(resourcePolicyRules)
.filter((id): id is number => id !== undefined); .where(
eq(resourcePolicyRules.resourcePolicyId, resourcePolicyId)
);
// Delete rules that are no longer in the incoming list if (rules.length > 0) {
if (incomingRuleIds.length > 0) {
await trx
.delete(resourcePolicyRules)
.where(
and(
eq(
resourcePolicyRules.resourcePolicyId,
resourcePolicyId
),
notInArray(
resourcePolicyRules.ruleId,
incomingRuleIds
)
)
);
} else {
await trx
.delete(resourcePolicyRules)
.where(
eq(
resourcePolicyRules.resourcePolicyId,
resourcePolicyId
)
);
}
// 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( await trx.insert(resourcePolicyRules).values(
newRules.map((rule) => ({ rules.map((rule) => ({
resourcePolicyId, resourcePolicyId,
action: rule.action, ...rule
match: rule.match,
value: rule.value,
priority: rule.priority,
enabled: rule.enabled
})) }))
); );
} }

View File

@@ -107,13 +107,6 @@ export async function setResourceHeaderAuth(
resource.resourcePolicyId === null && resource.resourcePolicyId === null &&
resource.defaultResourcePolicyId !== null; resource.defaultResourcePolicyId !== null;
const headerAuthHash =
user && password && extendedCompatibility !== null
? await hashPassword(
Buffer.from(`${user}:${password}`).toString("base64")
)
: null;
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
if (isInlinePolicy) { if (isInlinePolicy) {
const policyId = resource.defaultResourcePolicyId!; const policyId = resource.defaultResourcePolicyId!;
@@ -123,7 +116,11 @@ export async function setResourceHeaderAuth(
eq(resourcePolicyHeaderAuth.resourcePolicyId, policyId) eq(resourcePolicyHeaderAuth.resourcePolicyId, policyId)
); );
if (headerAuthHash !== null && extendedCompatibility !== null) { if (user && password && extendedCompatibility !== null) {
const headerAuthHash = await hashPassword(
Buffer.from(`${user}:${password}`).toString("base64")
);
await trx.insert(resourcePolicyHeaderAuth).values({ await trx.insert(resourcePolicyHeaderAuth).values({
resourcePolicyId: policyId, resourcePolicyId: policyId,
headerAuthHash, headerAuthHash,
@@ -143,7 +140,11 @@ export async function setResourceHeaderAuth(
) )
); );
if (headerAuthHash !== null && extendedCompatibility !== null) { if (user && password && extendedCompatibility !== null) {
const headerAuthHash = await hashPassword(
Buffer.from(`${user}:${password}`).toString("base64")
);
await Promise.all([ await Promise.all([
trx trx
.insert(resourceHeaderAuth) .insert(resourceHeaderAuth)

View File

@@ -340,8 +340,7 @@ function PolicyAccessRulesSectionEdit({
? rules.filter((rule) => !rule.fromPolicy) ? rules.filter((rule) => !rule.fromPolicy)
: rules; : rules;
const rulesPayload = rulesToValidate.map( const rulesPayload = rulesToValidate.map(
({ ruleId, action, match, value, priority, enabled, new: isNew }) => ({ ({ action, match, value, priority, enabled }) => ({
...(isNew ? {} : { ruleId }),
action, action,
match, match,
value, value,