Skip to content

Commit

Permalink
fix: factory URL flow with untrusted repository (#1210)
Browse files Browse the repository at this point in the history
* fix: factory URL flow with untrusted repository

Signed-off-by: Oleksii Kurinnyi <[email protected]>

* Update packages/dashboard-frontend/src/store/Workspaces/Preferences/helpers.ts

Co-authored-by: Oleksii Orel <[email protected]>

---------

Signed-off-by: Oleksii Kurinnyi <[email protected]>
Co-authored-by: Oleksii Orel <[email protected]>
  • Loading branch information
akurinnoy and olexii4 committed Oct 4, 2024
1 parent 2b0fd23 commit afe744f
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@ import { AppAlerts } from '@/services/alerts/appAlerts';
import { AppState } from '@/store';
import { selectIsAllowedSourcesConfigured } from '@/store/ServerConfig/selectors';
import { workspacePreferencesActionCreators } from '@/store/Workspaces/Preferences';
import {
selectPreferencesIsTrustedSource,
selectPreferencesTrustedSources,
} from '@/store/Workspaces/Preferences/selectors';
import { isTrustedRepo } from '@/store/Workspaces/Preferences/helpers';
import { selectPreferencesTrustedSources } from '@/store/Workspaces/Preferences/selectors';

export type Props = MappedProps & {
location: string;
Expand All @@ -58,15 +56,15 @@ class UntrustedSourceModal extends React.Component<Props, State> {
this.state = {
canContinue: true,
continueButtonDisabled: false,
isTrusted: this.props.isTrustedSource(props.location),
isTrusted: isTrustedRepo(props.trustedSources, props.location),
isAllowedSourcesConfigured: this.props.isAllowedSourcesConfigured,
trustAllCheckbox: false,
};
}

public shouldComponentUpdate(nextProps: Readonly<Props>, nextState: Readonly<State>): boolean {
const isTrusted = this.props.isTrustedSource(this.props.location);
const nextIsTrusted = nextProps.isTrustedSource(nextProps.location);
const isTrusted = isTrustedRepo(this.props.trustedSources, this.props.location);
const nextIsTrusted = isTrustedRepo(nextProps.trustedSources, nextProps.location);
if (isTrusted !== nextIsTrusted) {
return true;
}
Expand Down Expand Up @@ -101,7 +99,7 @@ class UntrustedSourceModal extends React.Component<Props, State> {
}

public componentDidUpdate(prevProps: Readonly<Props>): void {
const isTrusted = this.props.isTrustedSource(this.props.location);
const isTrusted = isTrustedRepo(this.props.trustedSources, this.props.location);
const isAllowedSourcesConfigured = this.props.isAllowedSourcesConfigured;

this.setState({
Expand Down Expand Up @@ -237,7 +235,6 @@ class UntrustedSourceModal extends React.Component<Props, State> {

const mapStateToProps = (state: AppState) => ({
trustedSources: selectPreferencesTrustedSources(state),
isTrustedSource: selectPreferencesIsTrustedSource(state),
isAllowedSourcesConfigured: selectIsAllowedSourcesConfigured(state),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ describe('Creating steps, initializing', () => {
[FACTORY_URL_ATTR]: factoryUrl,
});

renderComponent(store, searchParams);
const { reRenderComponent } = renderComponent(store, searchParams);

const stepTitle = screen.getByTestId('step-title');
expect(stepTitle.textContent).not.toContain('untrusted source');
Expand All @@ -349,6 +349,26 @@ describe('Creating steps, initializing', () => {
expect(stepTitleNext.textContent).toContain('untrusted source');

expect(mockOnNextStep).not.toHaveBeenCalled();

// add factory URL to trusted sources
const nextStore = new FakeStoreBuilder()
.withInfrastructureNamespace([{ name: 'user-che', attributes: { phase: 'Active' } }])
.withSshKeys({
keys: [{ name: 'key1', keyPub: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD' }],
})
.withWorkspacePreferences({
'trusted-sources': ['some-trusted-source', factoryUrl],
})
.build();

reRenderComponent(nextStore, searchParams);

await jest.runOnlyPendingTimersAsync();

const _stepTitleNext = screen.getByTestId('step-title');
await waitFor(() => expect(_stepTitleNext.textContent).not.toContain('untrusted source'));

await waitFor(() => expect(mockOnNextStep).toHaveBeenCalled());
});

test('source URL is not allowed', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* Red Hat, Inc. - initial API and implementation
*/

import { helpers } from '@eclipse-che/common';
import { api, helpers } from '@eclipse-che/common';
import { AlertVariant, pluralize } from '@patternfly/react-core';
import isEqual from 'lodash/isEqual';
import React from 'react';
Expand Down Expand Up @@ -39,7 +39,8 @@ import { selectInfrastructureNamespaces } from '@/store/InfrastructureNamespaces
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 { selectPreferencesTrustedSources } from '@/store/Workspaces/Preferences';
import { isTrustedRepo } from '@/store/Workspaces/Preferences/helpers';
import { selectAllWorkspaces } from '@/store/Workspaces/selectors';

export type Props = MappedProps &
Expand Down Expand Up @@ -102,8 +103,13 @@ class CreatingStepInitialize extends ProgressStep<Props, State> {
}

// source URL trusted/untrusted
const { sourceUrl } = nextState.factoryParams;
if (this.state.isSourceTrusted !== this.isSourceTrusted(sourceUrl)) {
const trustedSourcesStr = Array.isArray(this.props.trustedSources)
? this.props.trustedSources.join(',')
: this.props.trustedSources;
const nextTrustedSourcesStr = Array.isArray(nextProps.trustedSources)
? nextProps.trustedSources.join(',')
: nextProps.trustedSources;
if (trustedSourcesStr !== nextTrustedSourcesStr) {
return true;
}

Expand Down Expand Up @@ -148,7 +154,8 @@ class CreatingStepInitialize extends ProgressStep<Props, State> {

// check if the source is trusted
const isSourceTrusted =
this.isSourceTrusted(sourceUrl) || this.props.allowedSources.length > 0;
this.isSourceTrusted(this.props.trustedSources, sourceUrl) ||
this.props.allowedSources.length > 0;

if (isSourceTrusted === true) {
this.setState({
Expand Down Expand Up @@ -261,8 +268,11 @@ class CreatingStepInitialize extends ProgressStep<Props, State> {
}
}

private isSourceTrusted(sourceUrl: string): boolean {
const isTrustedSource = this.props.isTrustedSource(sourceUrl);
private isSourceTrusted(
trustedSources: api.TrustedSources | undefined,
sourceUrl: string,
): boolean {
const isTrustedSource = isTrustedRepo(trustedSources, sourceUrl);
const isRegistryDevfile = this.props.isRegistryDevfile(sourceUrl);
if (isRegistryDevfile || isTrustedSource) {
return true;
Expand Down Expand Up @@ -319,7 +329,7 @@ const mapStateToProps = (state: AppState) => ({
allWorkspacesLimit: selectAllWorkspacesLimit(state),
infrastructureNamespaces: selectInfrastructureNamespaces(state),
isRegistryDevfile: selectIsRegistryDevfile(state),
isTrustedSource: selectPreferencesIsTrustedSource(state),
trustedSources: selectPreferencesTrustedSources(state),
allowedSources: selectAllowedSources(state),
sshKeys: selectSshKeys(state),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ import { Workspace } from '@/services/workspace-adapter';
import { AppState } from '@/store';
import { selectIsRegistryDevfile } from '@/store/DevfileRegistries/selectors';
import * as WorkspaceStore from '@/store/Workspaces';
import { selectPreferencesIsTrustedSource } from '@/store/Workspaces/Preferences';
import { selectPreferencesTrustedSources } from '@/store/Workspaces/Preferences';
import { isTrustedRepo } from '@/store/Workspaces/Preferences/helpers';
import { selectAllWorkspaces } from '@/store/Workspaces/selectors';

export type Props = MappedProps & {
Expand Down Expand Up @@ -157,7 +158,7 @@ class Progress extends React.Component<Props, State> {
} else {
// if workspace is not created yet, check if source is trusted
const { sourceUrl } = this.state.factoryParams;
const isTrustedSource = this.props.isTrustedSource(sourceUrl);
const isTrustedSource = isTrustedRepo(props.trustedSources, sourceUrl);
const isRegistryDevfile = this.props.isRegistryDevfile(sourceUrl);
// trust source if it is taken from the registry or it is in the list of trusted sources
if (isRegistryDevfile || isTrustedSource) {
Expand Down Expand Up @@ -705,7 +706,7 @@ class Progress extends React.Component<Props, State> {
const mapStateToProps = (state: AppState) => ({
allWorkspaces: selectAllWorkspaces(state),
isRegistryDevfile: selectIsRegistryDevfile(state),
isTrustedSource: selectPreferencesIsTrustedSource(state),
trustedSources: selectPreferencesTrustedSources(state),
});

const connector = connect(mapStateToProps, WorkspaceStore.actionCreators, null, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder';
import {
selectPreferences,
selectPreferencesError,
selectPreferencesIsTrustedSource,
selectPreferencesSkipAuthorization,
selectPreferencesTrustedSources,
} from '@/store/Workspaces/Preferences/selectors';
Expand Down Expand Up @@ -64,52 +63,4 @@ describe('Workspace preferences, selectors', () => {

expect(result).toEqual(mockState.preferences['trusted-sources']);
});

describe('check if a location is a trusted source', () => {
it('some sources are trusted', () => {
const store = new FakeStoreBuilder()
.withWorkspacePreferences({
'skip-authorisation': mockState.preferences['skip-authorisation'],
'trusted-sources': ['source1'],
error: mockState.error,
})
.build();
const state = store.getState();
const result = selectPreferencesIsTrustedSource(state);

expect(result('source1')).toBeTruthy();
expect(result('source2')).toBeFalsy();
});

it('all sources are trusted', () => {
const store = new FakeStoreBuilder()
.withWorkspacePreferences({
'skip-authorisation': mockState.preferences['skip-authorisation'],
'trusted-sources': '*',
error: mockState.error,
})
.build();
const state = store.getState();

selectPreferences(state);
const result = selectPreferencesIsTrustedSource(state);

expect(result('any-source')).toBeTruthy();
});

it('no sources are trusted', () => {
const store = new FakeStoreBuilder()
.withWorkspacePreferences({
'skip-authorisation': mockState.preferences['skip-authorisation'],
'trusted-sources': undefined,
error: mockState.error,
})
.build();
const state = store.getState();

const result = selectPreferencesIsTrustedSource(state);

expect(result('any-source')).toBeFalsy();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
* Red Hat, Inc. - initial API and implementation
*/

import { api } from '@eclipse-che/common';

export const gitProviderPatterns = {
github: {
https: /^https:\/\/github\.com\/([^\\/]+\/[^\\/]+)(?:\/.*)?$/,
Expand Down Expand Up @@ -100,13 +102,23 @@ function getRepoPattern(url: string): RegExp | undefined {
}
}

export function isTrustedRepo(trustedUrls: string[], url: string | URL): boolean {
export function isTrustedRepo(
trustedSources: api.TrustedSources | undefined,
url: string | URL,
): boolean {
if (trustedSources === undefined) {
return false;
}
if (trustedSources === '*') {
return true;
}

const urlString = url.toString();
const urlPattern = getRepoPattern(urlString);
const urlRepo = extractRepo(urlString, urlPattern);

// Check if the URL matches any of the trusted repositories
return trustedUrls.some(trustedUrl => {
return trustedSources.some(trustedUrl => {
const trustedUrlPattern = getRepoPattern(trustedUrl);
const trustedUrlRepo = extractRepo(trustedUrl, trustedUrlPattern);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import { createSelector } from 'reselect';

import { AppState } from '@/store';
import { isTrustedRepo } from '@/store/Workspaces/Preferences/helpers';

const selectState = (state: AppState) => state.workspacePreferences;

Expand All @@ -30,20 +29,3 @@ export const selectPreferencesTrustedSources = createSelector(
selectPreferences,
state => state['trusted-sources'],
);

export const selectPreferencesIsTrustedSource = createSelector(
selectPreferencesTrustedSources,
trustedSources => {
return (location: string) => {
if (!trustedSources || (Array.isArray(trustedSources) && trustedSources.length === 0)) {
// no trusted sources yet
return false;
} else if (trustedSources === '*') {
// all sources are trusted
return true;
}

return isTrustedRepo(trustedSources, location);
};
},
);

0 comments on commit afe744f

Please sign in to comment.