Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: factory URL flow with untrusted repository #1210

Merged
merged 2 commits into from
Oct 4, 2024
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
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 @@ -162,7 +163,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 @@ -722,7 +723,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);
};
},
);
Loading