Compare commits

...

6 Commits
1.19.4 ... dev

Author SHA1 Message Date
miloschwartz
7506c0420d properly pass org policy error message in olm register 2026-06-26 17:11:32 -04:00
Owen Schwartz
5572822c4a Merge pull request #3351 from fosrl/copilot/fix-rest-ruleid-changing
fix: preserve rule IDs when saving policy rules via the GUI
2026-06-26 15:05:31 -04:00
Owen
ea3f1c341b Move hashing outside of transaction 2026-06-26 14:40:39 -04:00
Owen
35dffe71cb Make error statement debug 2026-06-26 14:40:31 -04:00
copilot-swe-agent[bot]
5428bf4ed0 fix: preserve rule IDs when saving policy rules through the GUI
The `setResourcePolicyRules` endpoint was deleting all existing rules and
re-inserting them on every save, causing all ruleIDs to change.

Backend: Accept an optional `ruleId` per rule in the request body and
perform an upsert — update existing rules (matched by ruleId), insert
new ones (no ruleId), and delete only rules absent from the incoming list.

Frontend: Include `ruleId` in the rules payload for existing (non-new)
rules so the backend can match and preserve them.
2026-06-26 14:37:34 +00:00
copilot-swe-agent[bot]
9a89579e08 Initial plan 2026-06-26 14:33:35 +00:00
6 changed files with 92 additions and 38 deletions

View File

@@ -172,7 +172,9 @@ 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.error(blueprintMessage); logger.debug(
`Org ${orgId} blueprint apply issues: ${blueprintMessage}`
);
error = err; error = err;
} }

View File

@@ -197,15 +197,6 @@ 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}`,
@@ -238,7 +229,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
olm.olmId olm.olmId
); );
return; return;
} else if (!policyCheck.allowed) { } else if (!policyCheck.allowed || policyCheck.error) {
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,6 +76,15 @@ 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)
@@ -86,13 +95,7 @@ export async function setResourcePolicyHeaderAuth(
) )
); );
if (headerAuth !== null) { if (headerAuth !== null && headerAuthHash !== 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 { eq } from "drizzle-orm"; import { and, eq, notInArray } 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,6 +14,7 @@ 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"],
@@ -121,17 +122,74 @@ export async function setResourcePolicyRules(
.set({ applyRules }) .set({ applyRules })
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
await trx const incomingRuleIds = rules
.delete(resourcePolicyRules) .map((r) => r.ruleId)
.where( .filter((id): id is number => id !== undefined);
eq(resourcePolicyRules.resourcePolicyId, resourcePolicyId)
);
if (rules.length > 0) { // Delete rules that are no longer in the incoming list
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(
rules.map((rule) => ({ newRules.map((rule) => ({
resourcePolicyId, resourcePolicyId,
...rule action: rule.action,
match: rule.match,
value: rule.value,
priority: rule.priority,
enabled: rule.enabled
})) }))
); );
} }

View File

@@ -107,6 +107,13 @@ 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!;
@@ -116,11 +123,7 @@ export async function setResourceHeaderAuth(
eq(resourcePolicyHeaderAuth.resourcePolicyId, policyId) eq(resourcePolicyHeaderAuth.resourcePolicyId, policyId)
); );
if (user && password && extendedCompatibility !== null) { if (headerAuthHash !== null && 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,
@@ -140,11 +143,7 @@ export async function setResourceHeaderAuth(
) )
); );
if (user && password && extendedCompatibility !== null) { if (headerAuthHash !== null && 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,7 +340,8 @@ function PolicyAccessRulesSectionEdit({
? rules.filter((rule) => !rule.fromPolicy) ? rules.filter((rule) => !rule.fromPolicy)
: rules; : rules;
const rulesPayload = rulesToValidate.map( const rulesPayload = rulesToValidate.map(
({ action, match, value, priority, enabled }) => ({ ({ ruleId, action, match, value, priority, enabled, new: isNew }) => ({
...(isNew ? {} : { ruleId }),
action, action,
match, match,
value, value,