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: 3 additions & 3 deletions libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,9 @@
"Remove version": "Remove version",
"Add version": "Add version",
"Must be a valid JSON Schema (JSON or YAML)": "Must be a valid JSON Schema (JSON or YAML)",
"Must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)": "Must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)",
"Each entry must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)": "Each entry must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)",
"Must be a valid semver range (e.g. >=1.0.0 <2.0.0)": "Must be a valid semver range (e.g. >=1.0.0 <2.0.0)",
"Must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading \"v\" is not allowed.": "Must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading \"v\" is not allowed.",
"Each entry must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading \"v\" is not allowed.": "Each entry must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading \"v\" is not allowed.",
"Must be a valid semver range (e.g. >=1.0.0 <2.0.0). Leading \"v\" is not allowed.": "Must be a valid semver range (e.g. >=1.0.0 <2.0.0). Leading \"v\" is not allowed.",
"Must be a valid YAML or JSON object": "Must be a valid YAML or JSON object",
"Version is required": "Version is required",
"Version must be unique": "Version must be unique",
Expand Down
4 changes: 2 additions & 2 deletions libs/types/alpha/models/ApiVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
* APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources.
*/
export enum ApiVersion {
V1ALPHA1 = 'v1alpha1',
FLIGHTCTL_IO_V1ALPHA1 = 'flightctl.io/v1alpha1',
ApiVersionV1alpha1 = 'v1alpha1',
ApiVersionFlightctlIoV1alpha1 = 'flightctl.io/v1alpha1',
}
2 changes: 1 addition & 1 deletion libs/types/alpha/models/CatalogItemVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { CatalogItemDeprecation } from './CatalogItemDeprecation';
*/
export type CatalogItemVersion = (CatalogItemConfigurable & {
/**
* Semantic version identifier (e.g., 1.2.3, v2.0.0-rc1). Required for version ordering and upgrade graph.
* Semantic version identifier (e.g., 1.2.3, 2.0.0-rc1). Required for version ordering and upgrade graph.
*/
version: string;
/**
Expand Down
10 changes: 8 additions & 2 deletions libs/types/models/K8sProviderSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ export type K8sProviderSpec = {
* Whether this K8s provider is enabled.
*/
enabled?: boolean;
organizationAssignment: AuthOrganizationAssignment;
roleAssignment: AuthRoleAssignment;
/**
* How users from this auth provider are assigned to organizations.
*/
organizationAssignment: AuthOrganizationAssignment | null;
/**
* How users from this auth provider are assigned roles.
*/
roleAssignment: AuthRoleAssignment | null;
/**
* Optional suffix to strip from ClusterRole names when normalizing role names. Used for multi-release deployments where ClusterRoles have namespace-specific names (e.g., flightctl-admin-<namespace>).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const formVersionsToApi = (values: AddCatalogItemFormValues) => {
channels: v.channels,
replaces: v.replaces || undefined,
skips: skips.length ? skips : undefined,
skipRange: v.skipRange || undefined,
skipRange: toApiCatalogSkipRange(v.skipRange || ''),
readme: v.readme || undefined,
config: configurable ? parseYamlField(v.config) : undefined,
configSchema: configurable ? parseSchemaField(v.configSchema) : undefined,
Expand Down Expand Up @@ -175,7 +175,7 @@ export const getCatalogItemResource = (values: AddCatalogItemFormValues, catalog
const type = values.type as CatalogItemType;

return {
apiVersion: ApiVersion.V1ALPHA1,
apiVersion: ApiVersion.ApiVersionV1alpha1,
kind: 'CatalogItem',
metadata: {
name: values.name,
Expand Down Expand Up @@ -345,18 +345,61 @@ const jsonSchemaFieldSchema = (t: TFunction) =>
}
});

const optionalSemver = (t: TFunction) =>
Yup.string().test('valid-semver', t('Must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)'), (value) => {
if (!value) {
return true;
const hasForbiddenLeadingVPrefix = (s: string): boolean => s.trimStart().startsWith('v');

const isValidCatalogSingleSemver = (value: string): boolean =>
!hasForbiddenLeadingVPrefix(value) && semver.valid(value) !== null;

const RANGE_OPERATOR_TRIM = /^[=<>~^]+/;

// Ensure that no leading "v" are present.
// Allow the user to type extra spaces after operators; they will be removed when the range is saved.
// Valid examples: ">= 1.0.0 < 2.0.0", ">=1.0.0 <2.0.0"
const isValidCatalogSemverRange = (value: string): boolean => {
const trimmed = value.trim();
if (!trimmed) {
return false;
}
const normalizedRange = semver.validRange(trimmed);
if (normalizedRange === null) {
return false;
}
const versionParts = normalizedRange.split(/\s+/);
for (const part of versionParts) {
const version = part.replace(RANGE_OPERATOR_TRIM, '');
if (!version || hasForbiddenLeadingVPrefix(version)) {
return false;
}
return semver.valid(value) !== null;
});
}
return true;
};
Comment on lines +358 to +375
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm semver.validRange strips leading "v" in 7.7.3 and that current code accepts these inputs.
node -e '
  const semver = require("semver");
  const cases = ["v1.0.0", ">=v1.0.0 <2.0.0", ">= v1.0.0 < 2.0.0", "v1.0.0 - v2.0.0"];
  for (const c of cases) console.log(JSON.stringify(c), "->", JSON.stringify(semver.validRange(c)));
'

Repository: flightctl/flightctl-ui

Length of output: 203


🏁 Script executed:

cat -n libs/ui-components/src/components/Catalog/AddCatalogItemWizard/utils.ts | sed -n '340,385p'

Repository: flightctl/flightctl-ui

Length of output: 1807


🏁 Script executed:

rg -n "hasForbiddenLeadingVPrefix" libs/ui-components/src/components/Catalog/AddCatalogItemWizard/utils.ts -A 3 -B 1

Repository: flightctl/flightctl-ui

Length of output: 537


🏁 Script executed:

fd -e "\.test\.(ts|tsx|js|jsx)$" libs/ui-components/src/components/Catalog/AddCatalogItemWizard | head -5

Repository: flightctl/flightctl-ui

Length of output: 620


🏁 Script executed:

fd . 'libs/ui-components/src/components/Catalog/AddCatalogItemWizard' --type f

Repository: flightctl/flightctl-ui

Length of output: 812


🏁 Script executed:

rg -l "isValidCatalogSemverRange|optionalSemverRange" libs/ui-components --type ts --type tsx

Repository: flightctl/flightctl-ui

Length of output: 94


🏁 Script executed:

rg -l "isValidCatalogSemverRange|optionalSemverRange" libs/ui-components -t ts

Repository: flightctl/flightctl-ui

Length of output: 138


🏁 Script executed:

fd . 'libs/ui-components/src/components/Catalog/AddCatalogItemWizard' --type f -name "*.test.*"

Repository: flightctl/flightctl-ui

Length of output: 297


🏁 Script executed:

rg "optionalSemverRange|isValidCatalogSemverRange" libs/ui-components/src/components/Catalog/AddCatalogItemWizard/utils.ts -A 5 -B 2

Repository: flightctl/flightctl-ui

Length of output: 1236


🏁 Script executed:

fd . 'libs/ui-components/src/components/Catalog/AddCatalogItemWizard' --type f | grep -i test

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

rg "v1\.0\.0|hasForbiddenLeadingVPrefix" libs/ui-components --type ts -l

Repository: flightctl/flightctl-ui

Length of output: 138


🏁 Script executed:

cat -n libs/ui-components/src/components/Catalog/AddCatalogItemWizard/utils.ts | sed -n '386,430p'

Repository: flightctl/flightctl-ui

Length of output: 1623


🏁 Script executed:

rg "isValidCatalogSemverRange|isValidCatalogSingleSemver|optionalSemver" libs/ui-components/src/components/Catalog/AddCatalogItemWizard -A 10 | grep -i "test\|describe\|it("

Repository: flightctl/flightctl-ui

Length of output: 979


🏁 Script executed:

find libs/ui-components -name "*.test.ts" -o -name "*.test.tsx" -o -name "*.spec.ts" -o -name "*.spec.tsx" 2>/dev/null | head -10

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

cat -n libs/ui-components/src/components/Catalog/AddCatalogItemWizard/utils.ts | sed -n '1,50p'

Repository: flightctl/flightctl-ui

Length of output: 1833


Range validator accepts leading "v" because it checks the normalized output instead of the user input.

semver.validRange() strips a leading v before returning the normalized range (per semver 7.7.3 docs). The current isValidCatalogSemverRange implementation normalizes first (line 363) and then checks for a leading v on the normalized parts (lines 367–373), meaning the check can never catch user input like "v1.0.0", ">=v1.0.0 <2.0.0", or "v1.0.0 - v2.0.0". All of these pass validation and are silently normalized to clean output by toApiCatalogSkipRange (line 383), defeating the error message on line 418 that says "Leading "v" is not allowed."

By contrast, isValidCatalogSingleSemver (lines 350–351) correctly checks the original string first before calling semver.valid().

Move the leading-v check to the original input before delegating to semver.validRange():

Proposed fix
 const isValidCatalogSemverRange = (value: string): boolean => {
   const trimmed = value.trim();
   if (!trimmed) {
     return false;
   }
+  // Check for leading "v" in the original input *before* semver strips it away.
+  for (const part of trimmed.split(/\s+/)) {
+    const operand = part.replace(RANGE_OPERATOR_TRIM, '');
+    if (hasForbiddenLeadingVPrefix(operand)) {
+      return false;
+    }
+  }
   const normalizedRange = semver.validRange(trimmed);
   if (normalizedRange === null) {
     return false;
   }
-  const versionParts = normalizedRange.split(/\s+/);
-  for (const part of versionParts) {
-    const version = part.replace(RANGE_OPERATOR_TRIM, '');
-    if (!version || hasForbiddenLeadingVPrefix(version)) {
-      return false;
-    }
-  }
   return true;
 };

Add unit tests covering "v1.0.0", ">=v1.0.0 <2.0.0", ">= v1.0.0 < 2.0.0", and "v1.0.0 - v2.0.0".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isValidCatalogSemverRange = (value: string): boolean => {
const trimmed = value.trim();
if (!trimmed) {
return false;
}
const normalizedRange = semver.validRange(trimmed);
if (normalizedRange === null) {
return false;
}
const versionParts = normalizedRange.split(/\s+/);
for (const part of versionParts) {
const version = part.replace(RANGE_OPERATOR_TRIM, '');
if (!version || hasForbiddenLeadingVPrefix(version)) {
return false;
}
return semver.valid(value) !== null;
});
}
return true;
};
const isValidCatalogSemverRange = (value: string): boolean => {
const trimmed = value.trim();
if (!trimmed) {
return false;
}
// Check for leading "v" in the original input *before* semver strips it away.
for (const part of trimmed.split(/\s+/)) {
const operand = part.replace(RANGE_OPERATOR_TRIM, '');
if (hasForbiddenLeadingVPrefix(operand)) {
return false;
}
}
const normalizedRange = semver.validRange(trimmed);
if (normalizedRange === null) {
return false;
}
return true;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/ui-components/src/components/Catalog/AddCatalogItemWizard/utils.ts`
around lines 358 - 375, The validator is checking for a leading "v" after
calling semver.validRange(), so inputs like "v1.0.0" slip through; update
isValidCatalogSemverRange to first inspect the original trimmed input for
forbidden leading "v" on each token (use RANGE_OPERATOR_TRIM to strip
operators/spaces and hasForbiddenLeadingVPrefix to detect the prefix) and return
false if any token has a leading "v" before calling semver.validRange(), then
proceed with normalization and the existing checks; also add unit tests for
"v1.0.0", ">=v1.0.0 <2.0.0", ">= v1.0.0 < 2.0.0", and "v1.0.0 - v2.0.0" to cover
these cases.


// Normalize the skip range for the API. Spaces after operators must be removed.
const toApiCatalogSkipRange = (value: string): string | undefined => {
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
const normalized = semver.validRange(trimmed);
return normalized ?? undefined;
};

const optionalSemver = (t: TFunction) =>
Yup.string().test(
'valid-semver',
t('Must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading "v" is not allowed.'),
(value) => {
if (!value) {
return true;
}
return isValidCatalogSingleSemver(value);
},
);

const optionalSemverList = (t: TFunction) =>
Yup.string().test(
'valid-semver-list',
t('Each entry must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)'),
t('Each entry must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading "v" is not allowed.'),
(value) => {
if (!value?.trim()) {
return true;
Expand All @@ -365,17 +408,21 @@ const optionalSemverList = (t: TFunction) =>
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.every((v) => semver.valid(v) !== null);
.every(isValidCatalogSingleSemver);
},
);

const optionalSemverRange = (t: TFunction) =>
Yup.string().test('valid-semver-range', t('Must be a valid semver range (e.g. >=1.0.0 <2.0.0)'), (value) => {
if (!value?.trim()) {
return true;
}
return semver.validRange(value) !== null;
});
Yup.string().test(
'valid-semver-range',
t('Must be a valid semver range (e.g. >=1.0.0 <2.0.0). Leading "v" is not allowed.'),
(value) => {
if (!value?.trim()) {
return true;
}
return isValidCatalogSemverRange(value);
},
);

const yamlFieldSchema = (t: TFunction) =>
Yup.string().test('valid-yaml-json', t('Must be a valid YAML or JSON object'), (value) => {
Expand All @@ -394,12 +441,16 @@ const versionSchema = (t: TFunction, duplicates: Set<string>, configurable: bool
Yup.object().shape({
version: Yup.string()
.required(t('Version is required'))
.test('valid-semver', t('Must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)'), (value) => {
if (!value) {
return true;
}
return semver.valid(value) !== null;
})
.test(
'valid-semver',
t('Must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading "v" is not allowed.'),
(value) => {
if (!value) {
return true;
}
return isValidCatalogSingleSemver(value);
},
)
.test('unique-version', t('Version must be unique'), (value) => {
if (!value) {
return true;
Expand Down Expand Up @@ -642,7 +693,7 @@ export const getCatalogPatches = (catalog: Catalog, values: CreateCatalogFormVal
};

export const getCatalogResource = (values: CreateCatalogFormValues): Catalog => ({
apiVersion: ApiVersion.V1ALPHA1,
apiVersion: ApiVersion.ApiVersionV1alpha1,
kind: 'Catalog',
metadata: { name: values.name },
spec: {
Expand Down
2 changes: 1 addition & 1 deletion libs/ui-components/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const getApiVersion = (api: 'flightctl' | 'imagebuilder' | 'alerts' | 'ca
case 'imagebuilder':
return ImageBuilderApiVersion.ApiVersionV1alpha1;
case 'catalog':
return AlphaVersion.V1ALPHA1;
return AlphaVersion.ApiVersionV1alpha1;
case 'alerts':
default:
return undefined;
Expand Down
Loading