From 1cd4b447870cbc67dc8cff1e525ce766bc253560 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Mon, 16 Sep 2024 15:04:09 +0200 Subject: [PATCH 1/6] feat: Dashboard should not allow to create a workspace if repo url is not in allowed list Signed-off-by: Anatolii Bazko --- packages/common/src/dto/api/index.ts | 5 ++ .../__tests__/serverConfigApi.spec.ts | 8 +++ .../services/serverConfigApi.ts | 4 ++ .../src/devworkspaceClient/types/index.ts | 8 +++ .../routes/api/__tests__/serverConfig.spec.ts | 1 + .../__mocks__/getDevWorkspaceClient.ts | 3 + .../src/routes/api/serverConfig.ts | 2 + .../__tests__/index.spec.tsx | 11 ++++ .../components/UntrustedSourceModal/index.tsx | 14 ++++- .../Initialize/__tests__/index.spec.tsx | 26 +++++++++ .../CreatingSteps/Initialize/index.tsx | 41 +++++++++++++- .../StartWorkspace/__tests__/index.spec.tsx | 1 + .../ServerConfig/__tests__/helpers.spec.ts | 29 ++++++++++ .../ServerConfig/__tests__/selectors.spec.ts | 56 +++++++++++++++++++ .../src/store/ServerConfig/__tests__/stubs.ts | 1 + .../src/store/ServerConfig/helpers.ts | 36 ++++++++++++ .../src/store/ServerConfig/index.ts | 1 + .../src/store/ServerConfig/selectors.ts | 12 ++++ .../src/store/__mocks__/storeBuilder.ts | 1 + run/local-run.sh | 4 +- 20 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 packages/dashboard-frontend/src/store/ServerConfig/__tests__/helpers.spec.ts create mode 100644 packages/dashboard-frontend/src/store/ServerConfig/helpers.ts diff --git a/packages/common/src/dto/api/index.ts b/packages/common/src/dto/api/index.ts index a1e2d018d..ae1fc9d96 100644 --- a/packages/common/src/dto/api/index.ts +++ b/packages/common/src/dto/api/index.ts @@ -136,6 +136,7 @@ export interface IServerConfig { pluginRegistryURL: string; pluginRegistryInternalURL: string; dashboardLogo?: { base64data: string; mediatype: string }; + allowedSourceUrls: string[]; } export interface IAdvancedAuthorization { @@ -158,12 +159,16 @@ export interface IUserProfile { export interface IWorkspacePreferences { 'skip-authorisation': GitProvider[]; 'trusted-sources'?: TrustedSources; + 'allowed-sources'?: AllowedSources; [key: string]: unknown; } export type TrustedSourceAll = '*'; export type TrustedSourceUrl = string; export type TrustedSources = TrustedSourceAll | TrustedSourceUrl[]; +export type AllowedSourceUrl = string; +export type AllowedSources = AllowedSourceUrl[]; + export type IEditors = Array; export type IEventList = CoreV1EventList; diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts index 931379b7a..33dfba7a5 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts @@ -160,6 +160,11 @@ describe('Server Config API Service', () => { const res = serverConfigService.getAdvancedAuthorization(buildCustomResource()); expect(res).toEqual({ allowUsers: ['user1', 'user2'] }); }); + + test('getting allowed source urls', () => { + const res = serverConfigService.getAllowedSourceUrls(buildCustomResource()); + expect(res).toEqual(['https://github.com']); + }); }); function buildCustomResourceList(): { body: CustomResourceDefinitionList } { @@ -219,6 +224,9 @@ function buildCustomResource(options?: { openVSXURL?: string }): CheClusterCusto secondsOfRunBeforeIdling: -1, maxNumberOfRunningWorkspacesPerCluster: 100, storage: { pvcStrategy: 'per-user' }, + allowedSource: { + urls: ['https://github.com'], + }, }, networking: { auth: { diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts b/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts index 5248676e6..648ff499e 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts @@ -228,4 +228,8 @@ export class ServerConfigApiService implements IServerConfigApi { getAutoProvision(cheCustomResource: CheClusterCustomResource): boolean { return cheCustomResource.spec.devEnvironments?.defaultNamespace?.autoProvision || false; } + + getAllowedSourceUrls(cheCustomResource: CheClusterCustomResource): string[] { + return cheCustomResource.spec.devEnvironments?.allowedSource?.urls || []; + } } diff --git a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts index 45c9dc247..8ce46b1e8 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts @@ -178,6 +178,9 @@ export type CheClusterCustomResourceSpecDevEnvironments = { maxNumberOfRunningWorkspacesPerUser?: number; maxNumberOfRunningWorkspacesPerCluster?: number; maxNumberOfWorkspacesPerUser?: number; + allowedSource?: { + urls?: string[]; + }; }; export function isCheClusterCustomResourceSpecDevEnvironments( @@ -362,6 +365,11 @@ export interface IServerConfigApi { * Returns the autoProvision value */ getAutoProvision(cheCustomResource: CheClusterCustomResource): boolean; + + /** + * Returns the allowed source URLs + */ + getAllowedSourceUrls(cheCustomResource: CheClusterCustomResource): string[]; } export interface IKubeConfigApi { diff --git a/packages/dashboard-backend/src/routes/api/__tests__/serverConfig.spec.ts b/packages/dashboard-backend/src/routes/api/__tests__/serverConfig.spec.ts index bcdfe6fd8..482173d68 100644 --- a/packages/dashboard-backend/src/routes/api/__tests__/serverConfig.spec.ts +++ b/packages/dashboard-backend/src/routes/api/__tests__/serverConfig.spec.ts @@ -67,6 +67,7 @@ describe('Server Config Route', () => { base64data: 'base64-encoded-data', mediatype: 'image/svg+xml', }, + allowedSourceUrls: [], }); }); }); diff --git a/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts b/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts index 35a514897..55ad00496 100644 --- a/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts +++ b/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts @@ -156,6 +156,8 @@ export const stubAutoProvision = true; export const stubAdvancedAuthorization = {}; +export const stubAllowedSourceUrls: string[] = []; + export const stubWorkspacePreferences: api.IWorkspacePreferences = { 'skip-authorisation': ['github'], 'trusted-sources': '*', @@ -186,6 +188,7 @@ export const getDevWorkspaceClient = jest.fn( getDashboardLogo: _cheCustomResource => dashboardLogo, getAutoProvision: _cheCustomResource => stubAutoProvision, getAdvancedAuthorization: _cheCustomResource => stubAdvancedAuthorization, + getAllowedSourceUrls: _cheCustomResource => stubAllowedSourceUrls, } as IServerConfigApi, devworkspaceApi: { create: (_devworkspace, _namespace) => diff --git a/packages/dashboard-backend/src/routes/api/serverConfig.ts b/packages/dashboard-backend/src/routes/api/serverConfig.ts index ea517f521..b2118554c 100644 --- a/packages/dashboard-backend/src/routes/api/serverConfig.ts +++ b/packages/dashboard-backend/src/routes/api/serverConfig.ts @@ -47,6 +47,7 @@ export function registerServerConfigRoute(instance: FastifyInstance) { const dashboardLogo = serverConfigApi.getDashboardLogo(cheCustomResource); const advancedAuthorization = serverConfigApi.getAdvancedAuthorization(cheCustomResource); const autoProvision = serverConfigApi.getAutoProvision(cheCustomResource); + const allowedSourceUrls = serverConfigApi.getAllowedSourceUrls(cheCustomResource); const serverConfig: api.IServerConfig = { containerBuild, @@ -72,6 +73,7 @@ export function registerServerConfigRoute(instance: FastifyInstance) { cheNamespace, pluginRegistryURL, pluginRegistryInternalURL, + allowedSourceUrls, }; if (dashboardLogo !== undefined) { diff --git a/packages/dashboard-frontend/src/components/UntrustedSourceModal/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/UntrustedSourceModal/__tests__/index.spec.tsx index b65cf90e6..c97c53913 100644 --- a/packages/dashboard-frontend/src/components/UntrustedSourceModal/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/components/UntrustedSourceModal/__tests__/index.spec.tsx @@ -62,6 +62,17 @@ describe('Untrusted Repo Warning Modal', () => { expect(modal).toBeNull(); }); + test('modal is hidden :: allowed sources configured', () => { + const store = storeBuilder + .withDwServerConfig({ + allowedSourceUrls: ['*'], + }) + .build(); + renderComponent(store, 'source-location', false); + const modal = screen.queryByRole('dialog'); + expect(modal).toBeNull(); + }); + test('modal is visible', () => { const store = storeBuilder .withWorkspacePreferences({ diff --git a/packages/dashboard-frontend/src/components/UntrustedSourceModal/index.tsx b/packages/dashboard-frontend/src/components/UntrustedSourceModal/index.tsx index 57a76aca1..f0ba47d98 100644 --- a/packages/dashboard-frontend/src/components/UntrustedSourceModal/index.tsx +++ b/packages/dashboard-frontend/src/components/UntrustedSourceModal/index.tsx @@ -26,6 +26,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { AppAlerts } from '@/services/alerts/appAlerts'; import { AppState } from '@/store'; +import { selectIsAllowedSourcesConfigured } from '@/store/ServerConfig/selectors'; import { workspacePreferencesActionCreators } from '@/store/Workspaces/Preferences'; import { selectPreferencesIsTrustedSource, @@ -43,6 +44,7 @@ export type State = { canContinue: boolean; continueButtonDisabled: boolean; isTrusted: boolean; + isAllowedSourcesConfigured: boolean; trustAllCheckbox: boolean; }; @@ -57,6 +59,7 @@ class UntrustedSourceModal extends React.Component { canContinue: true, continueButtonDisabled: false, isTrusted: this.props.isTrustedSource(props.location), + isAllowedSourcesConfigured: this.props.isAllowedSourcesConfigured, trustAllCheckbox: false, }; } @@ -84,26 +87,32 @@ class UntrustedSourceModal extends React.Component { return true; } + if (this.state.isAllowedSourcesConfigured !== nextState.isAllowedSourcesConfigured) { + return true; + } + return false; } public componentDidMount(): void { - if (this.props.isOpen && this.state.isTrusted) { + if (this.props.isOpen && (this.state.isTrusted || this.state.isAllowedSourcesConfigured)) { this.handleContinue(); } } public componentDidUpdate(prevProps: Readonly): void { const isTrusted = this.props.isTrustedSource(this.props.location); + const isAllowedSourcesConfigured = this.props.isAllowedSourcesConfigured; this.setState({ isTrusted, + isAllowedSourcesConfigured, }); if ( prevProps.isOpen === false && this.props.isOpen === true && - isTrusted === true && + (isTrusted === true || isAllowedSourcesConfigured) && this.state.canContinue === true ) { this.handleContinue(); @@ -229,6 +238,7 @@ class UntrustedSourceModal extends React.Component { const mapStateToProps = (state: AppState) => ({ trustedSources: selectPreferencesTrustedSources(state), isTrustedSource: selectPreferencesIsTrustedSource(state), + isAllowedSourcesConfigured: selectIsAllowedSourcesConfigured(state), }); const connector = connect(mapStateToProps, workspacePreferencesActionCreators, null, { diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/__tests__/index.spec.tsx index 51e2166c0..6b39d089e 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/__tests__/index.spec.tsx @@ -351,6 +351,32 @@ describe('Creating steps, initializing', () => { expect(mockOnNextStep).not.toHaveBeenCalled(); }); + test('source URL is not allowed', async () => { + const store = new FakeStoreBuilder() + .withDwServerConfig({ allowedSourceUrls: ['allowed-source'] }) + .withInfrastructureNamespace([{ name: 'user-che', attributes: { phase: 'Active' } }]) + .withSshKeys({ + keys: [{ name: 'key1', keyPub: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD' }], + }) + .build(); + const searchParams = new URLSearchParams({ + [FACTORY_URL_ATTR]: factoryUrl, + }); + + renderComponent(store, searchParams); + + const stepTitle = screen.getByTestId('step-title'); + expect(stepTitle.textContent).toContain('Initializing'); + + await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS); + + const stepTitleNext = screen.getByTestId('step-title'); + expect(stepTitleNext.textContent).toContain('Initializing'); + + expect(mockOnNextStep).not.toHaveBeenCalled(); + expect(mockOnError).toHaveBeenCalled(); + }); + test('samples are trusted', async () => { const store = new FakeStoreBuilder() .withInfrastructureNamespace([{ name: 'user-che', attributes: { phase: 'Active' } }]) diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/index.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/index.tsx index 3547d44ba..12cb4db42 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/index.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/index.tsx @@ -36,6 +36,8 @@ import { AppState } from '@/store'; import { selectAllWorkspacesLimit } from '@/store/ClusterConfig/selectors'; import { selectIsRegistryDevfile } from '@/store/DevfileRegistries/selectors'; import { selectInfrastructureNamespaces } from '@/store/InfrastructureNamespaces/selectors'; +import { isSourceAllowed } from '@/store/ServerConfig/helpers'; +import { selectAllowedSources } from '@/store/ServerConfig/selectors'; import { selectSshKeys } from '@/store/SshKeys/selectors'; import { selectPreferencesIsTrustedSource } from '@/store/Workspaces/Preferences'; import { selectAllWorkspaces } from '@/store/Workspaces/selectors'; @@ -47,6 +49,8 @@ export type Props = MappedProps & export type State = ProgressStepState & { factoryParams: FactoryParams; isSourceTrusted: boolean; + allowedSources: string[]; + isSourceAllowed: boolean; isWarning: boolean; }; @@ -61,6 +65,8 @@ class CreatingStepInitialize extends ProgressStep { isSourceTrusted: false, name: this.name, isWarning: false, + allowedSources: [], + isSourceAllowed: false, }; } @@ -101,6 +107,13 @@ class CreatingStepInitialize extends ProgressStep { return true; } + if ( + this.state.isSourceAllowed !== nextState.isSourceAllowed || + this.state.allowedSources !== nextState.allowedSources + ) { + return true; + } + // name or warning changed if (this.state.name !== nextState.name || this.state.isWarning !== nextState.isWarning) { return true; @@ -126,8 +139,18 @@ class CreatingStepInitialize extends ProgressStep { // skip source validation for devworkspace resources (samples) if (useDevWorkspaceResources === false) { + const isSourceAllowed = this.isSourceAllowed(sourceUrl); + if (isSourceAllowed === false) { + throw new Error( + `The specified source URL "${sourceUrl}" is not permitted for creating a workspace. Please contact your system administrator.`, + ); + } + + const allowedSources = this.getAllowedSources(); + // check if the source is trusted - const isSourceTrusted = this.isSourceTrusted(sourceUrl); + const isSourceTrusted = this.isSourceTrusted(sourceUrl) || allowedSources.length > 0; + if (isSourceTrusted === true) { this.setState({ isSourceTrusted, @@ -248,6 +271,21 @@ class CreatingStepInitialize extends ProgressStep { return false; } + private isSourceAllowed(sourceUrl: string): boolean { + if (this.props.allowedSources.length === 0) { + return true; + } + + const isRegistryDevfile = this.props.isRegistryDevfile(sourceUrl); + const isAllowed = isSourceAllowed(this.props.allowedSources, sourceUrl); + + return isRegistryDevfile || isAllowed; + } + + private getAllowedSources(): string[] { + return this.props.allowedSources; + } + render(): React.ReactElement { const { distance, hasChildren } = this.props; const { name, lastError } = this.state; @@ -292,6 +330,7 @@ const mapStateToProps = (state: AppState) => ({ infrastructureNamespaces: selectInfrastructureNamespaces(state), isRegistryDevfile: selectIsRegistryDevfile(state), isTrustedSource: selectPreferencesIsTrustedSource(state), + allowedSources: selectAllowedSources(state), sshKeys: selectSshKeys(state), }); diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/StartingSteps/StartWorkspace/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/StartingSteps/StartWorkspace/__tests__/index.spec.tsx index fa1a505d9..d18f4aec9 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/StartingSteps/StartWorkspace/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/StartingSteps/StartWorkspace/__tests__/index.spec.tsx @@ -86,6 +86,7 @@ const serverConfig: api.IServerConfig = { }, pluginRegistryURL: '', pluginRegistryInternalURL: '', + allowedSourceUrls: [], }; describe('Starting steps, starting a workspace', () => { diff --git a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/helpers.spec.ts b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/helpers.spec.ts new file mode 100644 index 000000000..234905914 --- /dev/null +++ b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/helpers.spec.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2018-2024 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { isSourceAllowed } from '@/store/ServerConfig/helpers'; + +describe('helpers', () => { + describe('isAllowedSourceUrl', () => { + test('allowed urls', () => { + expect(isSourceAllowed(['https://a/*'], 'https://a/b/c')).toBe(true); + expect(isSourceAllowed(['https://a/*/c'], 'https://a/b/c')).toBe(true); + expect(isSourceAllowed(['https://a/b/c'], 'https://a/b/c')).toBe(true); + expect(isSourceAllowed(['*'], 'https://a/b/c/')).toBe(true); + }); + + test('disallowed urls', () => { + expect(isSourceAllowed([], 'https://a/b/c')).toBe(false); + expect(isSourceAllowed(['https://a'], 'https://a/b/c/')).toBe(false); + }); + }); +}); diff --git a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts index 35507b0d8..06bfd572e 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts @@ -20,11 +20,13 @@ import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; import { serverConfig } from '@/store/ServerConfig/__tests__/stubs'; import { selectAdvancedAuthorization, + selectAllowedSources, selectAutoProvision, selectDashboardLogo, selectDefaultComponents, selectDefaultEditor, selectDefaultPlugins, + selectIsAllowedSourcesConfigured, selectOpenVSXUrl, selectPluginRegistryInternalUrl, selectPluginRegistryUrl, @@ -270,4 +272,58 @@ describe('serverConfig selectors', () => { expect(selectedDashboardLogo).toBeTruthy(); }); }); + + describe('selectAllowedSources', () => { + it('should return provided value', () => { + const fakeStore = new FakeStoreBuilder() + .withDwServerConfig( + Object.assign({}, serverConfig, { + allowedSourceUrls: ['https://test.com'], + } as api.IServerConfig), + ) + .build() as MockStoreEnhanced>; + const state = fakeStore.getState(); + + const allowedSourceUrls = selectAllowedSources(state); + expect(allowedSourceUrls).toEqual(['https://test.com']); + }); + + it('should return default value', () => { + const fakeStore = new FakeStoreBuilder().build() as MockStoreEnhanced< + AppState, + ThunkDispatch + >; + const state = fakeStore.getState(); + + const allowedSourceUrls = selectAllowedSources(state); + expect(allowedSourceUrls).toEqual([]); + }); + }); + + describe('selectIsAllowedSourcesConfigured', () => { + it('allowed sources configured', () => { + const fakeStore = new FakeStoreBuilder() + .withDwServerConfig( + Object.assign({}, serverConfig, { + allowedSourceUrls: ['https://test.com'], + } as api.IServerConfig), + ) + .build() as MockStoreEnhanced>; + const state = fakeStore.getState(); + + const isAllowedSourcesConfigured = selectIsAllowedSourcesConfigured(state); + expect(isAllowedSourcesConfigured).toBeTruthy(); + }); + + it('allowed sources NOTE configured', () => { + const fakeStore = new FakeStoreBuilder().build() as MockStoreEnhanced< + AppState, + ThunkDispatch + >; + const state = fakeStore.getState(); + + const isAllowedSourcesConfigured = selectIsAllowedSourcesConfigured(state); + expect(isAllowedSourcesConfigured).toBeFalsy(); + }); + }); }); diff --git a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/stubs.ts b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/stubs.ts index c98168815..e8b7aee51 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/stubs.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/stubs.ts @@ -59,4 +59,5 @@ export const serverConfig: api.IServerConfig = { base64data: 'base64-encoded-data', mediatype: 'image/png', }, + allowedSourceUrls: [], }; diff --git a/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts b/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts new file mode 100644 index 000000000..2d412ed63 --- /dev/null +++ b/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018-2024 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +export function isSourceAllowed(allowedSourceUrls: string[], url: string): boolean { + for (const allowedSourceUrl of allowedSourceUrls) { + if (allowedSourceUrl.indexOf('*') !== -1) { + let pattern = allowedSourceUrl.trim(); + if (!pattern.startsWith('*')) { + pattern = `^${pattern}`; + } + if (!pattern.endsWith('*')) { + pattern = `${pattern}$`; + } + pattern = pattern.replace(/\*/g, '.*'); + const regex = new RegExp(pattern); + if (regex.test(url)) { + return true; + } + } else { + if (allowedSourceUrl.trim() === url) { + return true; + } + } + } + + return false; +} diff --git a/packages/dashboard-frontend/src/store/ServerConfig/index.ts b/packages/dashboard-frontend/src/store/ServerConfig/index.ts index 60acd5f1a..2bada9991 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/index.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/index.ts @@ -95,6 +95,7 @@ const unloadedState: State = { cheNamespace: '', pluginRegistryURL: '', pluginRegistryInternalURL: '', + allowedSourceUrls: [], }, error: undefined, }; diff --git a/packages/dashboard-frontend/src/store/ServerConfig/selectors.ts b/packages/dashboard-frontend/src/store/ServerConfig/selectors.ts index 666b434de..8044328f5 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/selectors.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/selectors.ts @@ -73,4 +73,16 @@ export const selectAutoProvision = createSelector( state => state.config.defaultNamespace.autoProvision, ); +export const selectAllowedSources = createSelector( + selectState, + state => state.config.allowedSourceUrls || [], +); + +export const selectIsAllowedSourcesConfigured = createSelector( + selectAllowedSources, + allowedSources => { + return allowedSources.length > 0; + }, +); + export const selectServerConfigError = createSelector(selectState, state => state.error); diff --git a/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts b/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts index aa25e1e73..5259b5b7d 100644 --- a/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts +++ b/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts @@ -84,6 +84,7 @@ export class FakeStoreBuilder { }, pluginRegistryURL: '', pluginRegistryInternalURL: '', + allowedSourceUrls: [], } as api.IServerConfig, }, clusterInfo: { diff --git a/run/local-run.sh b/run/local-run.sh index 87c3f9db2..2fec7bacb 100755 --- a/run/local-run.sh +++ b/run/local-run.sh @@ -103,7 +103,9 @@ if [ ! -d $DASHBOARD_FRONTEND/lib/public/dashboard/devfile-registry ]; then -o "$DASHBOARD_FRONTEND/lib/public/dashboard/devfile-registry/air-gap" \ -i "packages/devfile-registry/air-gap/index.json" - sed -i "" 's|CHE_DASHBOARD_INTERNAL_URL|http://localhost:8080|g' "$DASHBOARD_FRONTEND/lib/public/dashboard/devfile-registry/air-gap/index.json" + if [ -s "$DASHBOARD_FRONTEND/lib/public/dashboard/devfile-registry/air-gap/index.json" ]; then + sed -i 's|CHE_DASHBOARD_INTERNAL_URL|http://localhost:8080|g' "$DASHBOARD_FRONTEND/lib/public/dashboard/devfile-registry/air-gap/index.json" + fi fi export CLUSTER_ACCESS_TOKEN=$(oc whoami -t) From 85273a30e6b0a6199b3fa324ffc24d44b1903562 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Mon, 16 Sep 2024 16:27:23 +0200 Subject: [PATCH 2/6] fixup Signed-off-by: Anatolii Bazko --- packages/common/src/dto/api/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/common/src/dto/api/index.ts b/packages/common/src/dto/api/index.ts index ae1fc9d96..ecbe10176 100644 --- a/packages/common/src/dto/api/index.ts +++ b/packages/common/src/dto/api/index.ts @@ -159,16 +159,12 @@ export interface IUserProfile { export interface IWorkspacePreferences { 'skip-authorisation': GitProvider[]; 'trusted-sources'?: TrustedSources; - 'allowed-sources'?: AllowedSources; [key: string]: unknown; } export type TrustedSourceAll = '*'; export type TrustedSourceUrl = string; export type TrustedSources = TrustedSourceAll | TrustedSourceUrl[]; -export type AllowedSourceUrl = string; -export type AllowedSources = AllowedSourceUrl[]; - export type IEditors = Array; export type IEventList = CoreV1EventList; From 17ac168d20aa400a9f90b2c3726dda6e03191cb4 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Tue, 17 Sep 2024 14:23:00 +0200 Subject: [PATCH 3/6] fixup Signed-off-by: Anatolii Bazko --- .../services/__tests__/serverConfigApi.spec.ts | 2 +- .../src/devworkspaceClient/services/serverConfigApi.ts | 2 +- .../dashboard-backend/src/devworkspaceClient/types/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts index 33dfba7a5..b79dfaa74 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts @@ -224,7 +224,7 @@ function buildCustomResource(options?: { openVSXURL?: string }): CheClusterCusto secondsOfRunBeforeIdling: -1, maxNumberOfRunningWorkspacesPerCluster: 100, storage: { pvcStrategy: 'per-user' }, - allowedSource: { + allowedSourced: { urls: ['https://github.com'], }, }, diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts b/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts index 648ff499e..5f06fc79c 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts @@ -230,6 +230,6 @@ export class ServerConfigApiService implements IServerConfigApi { } getAllowedSourceUrls(cheCustomResource: CheClusterCustomResource): string[] { - return cheCustomResource.spec.devEnvironments?.allowedSource?.urls || []; + return cheCustomResource.spec.devEnvironments?.allowedSourced?.urls || []; } } diff --git a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts index 8ce46b1e8..d8feb057a 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts @@ -178,7 +178,7 @@ export type CheClusterCustomResourceSpecDevEnvironments = { maxNumberOfRunningWorkspacesPerUser?: number; maxNumberOfRunningWorkspacesPerCluster?: number; maxNumberOfWorkspacesPerUser?: number; - allowedSource?: { + allowedSourced?: { urls?: string[]; }; }; From 2d62c4931cd24f5672802ac21df00b2044869c02 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Wed, 18 Sep 2024 09:56:20 +0200 Subject: [PATCH 4/6] Update packages/dashboard-frontend/src/store/ServerConfig/helpers.ts Co-authored-by: Oleksii Orel --- packages/dashboard-frontend/src/store/ServerConfig/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts b/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts index 2d412ed63..0b57c2826 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts @@ -12,7 +12,7 @@ export function isSourceAllowed(allowedSourceUrls: string[], url: string): boolean { for (const allowedSourceUrl of allowedSourceUrls) { - if (allowedSourceUrl.indexOf('*') !== -1) { + if (allowedSourceUrl.includes('*')) { let pattern = allowedSourceUrl.trim(); if (!pattern.startsWith('*')) { pattern = `^${pattern}`; From b35ed85eaeb5ec2cbdca49f934c354c3b37be371 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Wed, 18 Sep 2024 10:21:59 +0200 Subject: [PATCH 5/6] fixup Signed-off-by: Anatolii Bazko --- .../services/__tests__/serverConfigApi.spec.ts | 2 +- .../src/devworkspaceClient/services/serverConfigApi.ts | 2 +- .../dashboard-backend/src/devworkspaceClient/types/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts index b79dfaa74..3f867128e 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts @@ -224,7 +224,7 @@ function buildCustomResource(options?: { openVSXURL?: string }): CheClusterCusto secondsOfRunBeforeIdling: -1, maxNumberOfRunningWorkspacesPerCluster: 100, storage: { pvcStrategy: 'per-user' }, - allowedSourced: { + allowedSources: { urls: ['https://github.com'], }, }, diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts b/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts index 5f06fc79c..e212ff298 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts @@ -230,6 +230,6 @@ export class ServerConfigApiService implements IServerConfigApi { } getAllowedSourceUrls(cheCustomResource: CheClusterCustomResource): string[] { - return cheCustomResource.spec.devEnvironments?.allowedSourced?.urls || []; + return cheCustomResource.spec.devEnvironments?.allowedSources?.urls || []; } } diff --git a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts index d8feb057a..8c0d99eb9 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts @@ -178,7 +178,7 @@ export type CheClusterCustomResourceSpecDevEnvironments = { maxNumberOfRunningWorkspacesPerUser?: number; maxNumberOfRunningWorkspacesPerCluster?: number; maxNumberOfWorkspacesPerUser?: number; - allowedSourced?: { + allowedSources?: { urls?: string[]; }; }; From bd1a46b9158de1e636f9f407c1cacab7611a9a88 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Wed, 18 Sep 2024 11:53:55 +0200 Subject: [PATCH 6/6] fixup Signed-off-by: Anatolii Bazko --- .../CreatingSteps/Initialize/index.tsx | 14 ++------------ .../store/ServerConfig/__tests__/helpers.spec.ts | 3 ++- .../store/ServerConfig/__tests__/selectors.spec.ts | 2 +- .../src/store/ServerConfig/helpers.ts | 6 +++++- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/index.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/index.tsx index 12cb4db42..2be1f9252 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/index.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Initialize/index.tsx @@ -146,10 +146,9 @@ class CreatingStepInitialize extends ProgressStep { ); } - const allowedSources = this.getAllowedSources(); - // check if the source is trusted - const isSourceTrusted = this.isSourceTrusted(sourceUrl) || allowedSources.length > 0; + const isSourceTrusted = + this.isSourceTrusted(sourceUrl) || this.props.allowedSources.length > 0; if (isSourceTrusted === true) { this.setState({ @@ -272,20 +271,11 @@ class CreatingStepInitialize extends ProgressStep { } private isSourceAllowed(sourceUrl: string): boolean { - if (this.props.allowedSources.length === 0) { - return true; - } - const isRegistryDevfile = this.props.isRegistryDevfile(sourceUrl); const isAllowed = isSourceAllowed(this.props.allowedSources, sourceUrl); return isRegistryDevfile || isAllowed; } - - private getAllowedSources(): string[] { - return this.props.allowedSources; - } - render(): React.ReactElement { const { distance, hasChildren } = this.props; const { name, lastError } = this.state; diff --git a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/helpers.spec.ts b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/helpers.spec.ts index 234905914..4aa1b2ec8 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/helpers.spec.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/helpers.spec.ts @@ -19,10 +19,11 @@ describe('helpers', () => { expect(isSourceAllowed(['https://a/*/c'], 'https://a/b/c')).toBe(true); expect(isSourceAllowed(['https://a/b/c'], 'https://a/b/c')).toBe(true); expect(isSourceAllowed(['*'], 'https://a/b/c/')).toBe(true); + expect(isSourceAllowed(undefined, 'https://a/b/c')).toBe(true); + expect(isSourceAllowed([], 'https://a/b/c')).toBe(true); }); test('disallowed urls', () => { - expect(isSourceAllowed([], 'https://a/b/c')).toBe(false); expect(isSourceAllowed(['https://a'], 'https://a/b/c/')).toBe(false); }); }); diff --git a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts index 06bfd572e..d9c1af2d7 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts @@ -315,7 +315,7 @@ describe('serverConfig selectors', () => { expect(isAllowedSourcesConfigured).toBeTruthy(); }); - it('allowed sources NOTE configured', () => { + it('allowed sources NOT configured', () => { const fakeStore = new FakeStoreBuilder().build() as MockStoreEnhanced< AppState, ThunkDispatch diff --git a/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts b/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts index 0b57c2826..f1c53c9ec 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/helpers.ts @@ -10,7 +10,11 @@ * Red Hat, Inc. - initial API and implementation */ -export function isSourceAllowed(allowedSourceUrls: string[], url: string): boolean { +export function isSourceAllowed(allowedSourceUrls: string[] | undefined, url: string): boolean { + if (allowedSourceUrls === undefined || allowedSourceUrls.length === 0) { + return true; + } + for (const allowedSourceUrl of allowedSourceUrls) { if (allowedSourceUrl.includes('*')) { let pattern = allowedSourceUrl.trim();