Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions workspaces/orchestrator/.changeset/five-meals-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@red-hat-developer-hub/backstage-plugin-orchestrator-backend': patch
---

- Update dependecy @urql/core to fix CVE-2026-3118
- Reworks the filter and query builder code to use query variables
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"@backstage/plugin-scaffolder-backend": "^2.2.0",
"@backstage/plugin-scaffolder-node": "^0.11.0",
"@red-hat-developer-hub/backstage-plugin-orchestrator-common": "workspace:^",
"@urql/core": "^4.1.4",
"@urql/core": "^6.0.1",
"ajv-formats": "^2.1.1",
"cloudevents": "^8.0.0",
"express": "^4.21.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import {
TypeName,
} from '@red-hat-developer-hub/backstage-plugin-orchestrator-common';

import { randomBytes } from 'node:crypto';

import { FilterClause, FilterClauseVariable } from '../types/filterClause';

type ProcessType = 'ProcessDefinition' | 'ProcessInstance';

const supportedOperators = [
Expand Down Expand Up @@ -73,44 +77,88 @@ function handleLogicalFilter(
introspection: IntrospectionField[],
type: ProcessType,
filter: LogicalFilter,
): string {
if (!filter.operator) return '';
): FilterClause {
if (!filter.operator) return {} as FilterClause;

const subClauses = filter.filters.map(f =>
buildFilterCondition(introspection, type, f),
);

return `${filter.operator.toLowerCase()}: {${subClauses.join(', ')}}`;
const filterClause: FilterClause = {
clause: `${filter.operator.toLowerCase()}: {${subClauses.map(cl => cl.clause).join(', ')}}`,
clauseVariable: subClauses.flatMap(cl => cl.clauseVariable),
};
return filterClause;
}

function handleNestedFilter(
introspection: IntrospectionField[],
type: ProcessType,
filter: NestedFilter,
): string {
): FilterClause {
const subClauses = buildFilterCondition(
introspection,
type,
filter.nested,
true,
);

return `${filter.field}: {${subClauses}}`;
const filterClause: FilterClause = {
clauseVariable: subClauses.clauseVariable,
clause: `${filter.field}: {${subClauses.clause}}`,
};

return filterClause;
}

function handleBetweenOperator(filter: FieldFilter): string {
function handleBetweenOperator(filter: FieldFilter): FilterClause {
if (!Array.isArray(filter.value) || filter.value.length !== 2) {
throw new Error('Between operator requires an array of two elements');
}
return `${filter.field}: {${getGraphQLOperator(
const filterClauseVariableArray: FilterClauseVariable[] = [];
const clauseVariableName1 = `clauseVariable${nonSecureRandomAlphaNumeric()}`;
const filterClauseVariable1: FilterClauseVariable = {
clauseVariableName: clauseVariableName1,
formattedValue: filter.value[0],
clauseVariableType: 'String',
};

const clauseVariableName2 = `clauseVariable${nonSecureRandomAlphaNumeric()}`;
const filterClauseVariable2: FilterClauseVariable = {
clauseVariableName: clauseVariableName2,
formattedValue: filter.value[1],
clauseVariableType: 'String',
};

const clause = `${filter.field}: {${getGraphQLOperator(
FieldFilterOperatorEnum.Between,
)}: {from: "${filter.value[0]}", to: "${filter.value[1]}"}}`;
)}: {from: $${clauseVariableName1}, to: $${clauseVariableName2}}}`;
filterClauseVariableArray.push(filterClauseVariable1, filterClauseVariable2);
const filterClause: FilterClause = {
clause: clause,
clauseVariable: filterClauseVariableArray,
};

return filterClause;
}

function handleIsNullOperator(filter: FieldFilter): string {
return `${filter.field}: {${getGraphQLOperator(
FieldFilterOperatorEnum.IsNull,
)}: ${convertToBoolean(filter.value)}}`;
function handleIsNullOperator(filter: FieldFilter): FilterClause {
const clauseVariableName = `clauseVariable${nonSecureRandomAlphaNumeric()}`;
const clause = `${filter.field}: {${getGraphQLOperator(FieldFilterOperatorEnum.IsNull)}: $${clauseVariableName}}`;

const filterClauseVariable: FilterClauseVariable = {
clauseVariableName: clauseVariableName,
formattedValue: convertToBoolean(filter.value),
clauseVariableType: 'Boolean',
};
const filterClauseVariableArray: FilterClauseVariable[] = [];
filterClauseVariableArray.push(filterClauseVariable);
const clauseObject: FilterClause = {
clauseVariable: filterClauseVariableArray,
clause,
};

return clauseObject;
}

function isEnumFilter(
Expand All @@ -136,32 +184,58 @@ function handleBinaryOperator(
binaryFilter: FieldFilter,
fieldDef: IntrospectionField | undefined,
type: 'ProcessDefinition' | 'ProcessInstance',
): string {
): FilterClause {
if (isEnumFilter(binaryFilter.field, type)) {
if (!isValidEnumOperator(binaryFilter.operator)) {
throw new Error(
`Invalid operator ${binaryFilter.operator} for enum field ${binaryFilter.field} filter`,
);
}
}
const formattedValue = Array.isArray(binaryFilter.value)
? `[${binaryFilter.value
.map(v => formatValue(binaryFilter.field, v, fieldDef, type))
.join(', ')}]`
: formatValue(binaryFilter.field, binaryFilter.value, fieldDef, type);
return `${binaryFilter.field}: {${getGraphQLOperator(
binaryFilter.operator,
)}: ${formattedValue}}`;
let formattedValue: any;
let paramType: string;
if (Array.isArray(binaryFilter.value)) {
formattedValue = binaryFilter.value.map(v =>
formatValue(binaryFilter.field, v, fieldDef, type),
);
paramType = isEnumFilter(binaryFilter.field, type)
? '[ProcessInstanceState!]'
: '[String!]';
} else {
formattedValue = formatValue(
binaryFilter.field,
binaryFilter.value,
fieldDef,
type,
);
paramType = 'String';
}

const clauseVariableName = `clauseVariable${nonSecureRandomAlphaNumeric()}`;
const clause = `${binaryFilter.field}: {${getGraphQLOperator(binaryFilter.operator)}: $${clauseVariableName}}`;
const filterClauseVariable: FilterClauseVariable = {
Comment on lines +195 to +216

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Enum variable type wrong 🐞 Bug ≡ Correctness

handleBinaryOperator sets the GraphQL variable type to "String" for scalar filters even when
filtering the enum field "state". This can produce an invalid GraphQL operation (variable typed
String used where ProcessInstanceState is expected), breaking fetchInstances() when filtering by
state with EQ/LIKE/etc.
Agent Prompt
### Issue description
`handleBinaryOperator` generates GraphQL variable definitions for filter values. For the enum field `state`, scalar filters currently declare the variable as `String`, while array filters declare `[ProcessInstanceState!]`. This inconsistency can cause GraphQL validation errors because a variable declared as `String` may not be usable where an enum type is expected.

### Issue Context
- `isEnumFilter()` flags `ProcessInstance.state` as an enum field.
- `handleBinaryOperator()` currently sets `paramType = 'String'` for all scalar values.

### Fix Focus Areas
- workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts[183-212]

### Suggested fix
- When `isEnumFilter(binaryFilter.field, type)` is true and `binaryFilter.value` is not an array, set `paramType` to `ProcessInstanceState` (or the schema’s correct enum input type) instead of `String`.
- Consider centralizing the mapping from fieldDef/type -> GraphQL variable type so scalar/array cases stay consistent (e.g., `ProcessInstanceState` vs `[ProcessInstanceState!]`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

clauseVariableName: clauseVariableName,
formattedValue: formattedValue,
clauseVariableType: paramType,
};
const filterClauseVariableArray: FilterClauseVariable[] = [];
filterClauseVariableArray.push(filterClauseVariable);
const clauseObject: FilterClause = {
clauseVariable: filterClauseVariableArray,
clause,
};

return clauseObject;
}

export function buildFilterCondition(
introspection: IntrospectionField[],
type: ProcessType,
filters?: Filter,
isNested?: boolean,
): string {
): FilterClause {
if (!filters) {
return '';
return {} as FilterClause;
}

if (isNestedFilter(filters)) {
Expand Down Expand Up @@ -255,7 +329,7 @@ function formatValue(
type: ProcessType,
): string {
if (!fieldDef) {
return `"${fieldValue}"`;
return `${fieldValue}`;
}

if (!isFieldFilterSupported) {
Expand All @@ -270,7 +344,7 @@ function formatValue(
fieldDef.type.name === TypeName.Id ||
fieldDef.type.name === TypeName.Date
) {
return `"${fieldValue}"`;
return `${fieldValue}`;
}
throw new Error(
`Failed to format value for ${fieldName} ${fieldValue} with type ${fieldDef.type.name}`,
Expand Down Expand Up @@ -301,3 +375,9 @@ function getGraphQLOperator(operator: FieldFilterOperatorEnum): string {
throw new Error(`Operation "${operator}" not supported`);
}
}

// Function for getting 4 random digits to append to the clause variable name.
// Not used for any secrets or anything
function nonSecureRandomAlphaNumeric() {
return randomBytes(8).toString('hex');
}
Loading
Loading