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

Implemented an ability to reject the authorisation opt-out flag from the dashboard #972

Merged
merged 5 commits into from
Nov 21, 2023
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
10 changes: 4 additions & 6 deletions packages/common/src/dto/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,13 @@ export interface IUserProfile {
email: string;
username: string;
}
export interface IDevWorkspacePreferences {
'skip-authorisation': GitProvider[];
[key: string]: string | string[];
}

export type IEventList = CoreV1EventList;
export type IPodList = V1PodList;
export type PodLogs = {
[containerName: string]: {
logs: string;
failure: boolean;
};
};

export interface IDevWorkspaceList {
apiVersion?: string;
Expand Down
3 changes: 3 additions & 0 deletions packages/dashboard-backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { registerServerConfigRoute } from '@/routes/api/serverConfig';
import { registerSShKeysRoutes } from '@/routes/api/sshKeys';
import { registerUserProfileRoute } from '@/routes/api/userProfile';
import { registerWebsocket } from '@/routes/api/websocket';
import { registerWorkspacePreferencesRoute } from '@/routes/api/workspacePreferences';
import { registerYamlResolverRoute } from '@/routes/api/yamlResolver';
import { registerFactoryAcceptanceRedirect } from '@/routes/factoryAcceptanceRedirect';
import { registerWorkspaceRedirect } from '@/routes/workspaceRedirect';
Expand Down Expand Up @@ -117,5 +118,7 @@ export default async function buildApp(server: FastifyInstance): Promise<unknown
registerGettingStartedSamplesRoutes(isLocalRun(), server),

registerSShKeysRoutes(server),

registerWorkspacePreferencesRoute(server),
]);
}
13 changes: 13 additions & 0 deletions packages/dashboard-backend/src/constants/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ export const namespacedTemplateSchema: JSONSchema7 = {
required: ['namespace', 'templateName'],
};

export const namespacedWorkspacePreferencesSchema: JSONSchema7 = {
type: 'object',
properties: {
namespace: {
type: 'string',
},
provider: {
type: 'string',
},
},
required: ['namespace', 'provider'],
};

export const namespacedSchema: JSONSchema7 = {
type: 'object',
properties: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import {
IDevWorkspaceApi,
IDevWorkspacePreferencesApi,
IDevWorkspaceTemplateApi,
IDockerConfigApi,
IEventApi,
Expand Down Expand Up @@ -68,4 +69,7 @@ export class DevWorkspaceClient implements IDevWorkspaceClient {
get sshKeysApi(): IShhKeysApi {
throw new Error('Method not implemented.');
}
get devWorkspacePreferencesApi(): IDevWorkspacePreferencesApi {
throw new Error('Method not implemented.');
}
}
6 changes: 6 additions & 0 deletions packages/dashboard-backend/src/devworkspaceClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import * as k8s from '@kubernetes/client-node';

import { DevWorkspaceApiService } from '@/devworkspaceClient/services/devWorkspaceApi';
import { DevWorkspacePreferencesApiService } from '@/devworkspaceClient/services/devWorkspacePreferencesApi';
import { DevWorkspaceTemplateApiService } from '@/devworkspaceClient/services/devWorkspaceTemplateApi';
import { DockerConfigApiService } from '@/devworkspaceClient/services/dockerConfigApi';
import { EventApiService } from '@/devworkspaceClient/services/eventApi';
Expand All @@ -29,6 +30,7 @@ import { UserProfileApiService } from '@/devworkspaceClient/services/userProfile
import {
IDevWorkspaceApi,
IDevWorkspaceClient,
IDevWorkspacePreferencesApi,
IDevWorkspaceTemplateApi,
IDockerConfigApi,
IEventApi,
Expand Down Expand Up @@ -108,4 +110,8 @@ export class DevWorkspaceClient implements IDevWorkspaceClient {
get sshKeysApi(): IShhKeysApi {
return new SshKeysService(this.kubeConfig);
}

get devWorkspacePreferencesApi(): IDevWorkspacePreferencesApi {
return new DevWorkspacePreferencesApiService(this.kubeConfig);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2018-2023 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 { api } from '@eclipse-che/common';
import * as k8s from '@kubernetes/client-node';

import { createError } from '@/devworkspaceClient/services/helpers/createError';
import {
CoreV1API,
prepareCoreV1API,
} from '@/devworkspaceClient/services/helpers/prepareCoreV1API';
import { IDevWorkspacePreferencesApi } from '@/devworkspaceClient/types';

const ERROR_LABEL = 'CORE_V1_API_ERROR';
const DEV_WORKSPACE_PREFERENCES_CONFIGMAP = 'workspace-preferences-configmap';

const SKIP_AUTHORIZATION_KEY = 'skip-authorisation';

export class DevWorkspacePreferencesApiService implements IDevWorkspacePreferencesApi {
private readonly coreV1API: CoreV1API;

constructor(kc: k8s.KubeConfig) {
this.coreV1API = prepareCoreV1API(kc);
}

async getWorkspacePreferences(namespace: string): Promise<api.IDevWorkspacePreferences> {
try {
const response = await this.coreV1API.readNamespacedConfigMap(
DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace,
);
const data = response.body.data;
if (data === undefined) {
throw new Error('Data is empty');
}

const skipAuthorization =
data[SKIP_AUTHORIZATION_KEY] && data[SKIP_AUTHORIZATION_KEY] !== '[]'
? data[SKIP_AUTHORIZATION_KEY].replace(/^\[/, '').replace(/\]$/, '').split(', ')
: [];

return Object.assign({}, data, {
[SKIP_AUTHORIZATION_KEY]: skipAuthorization,
}) as api.IDevWorkspacePreferences;
} catch (e) {
throw createError(e, ERROR_LABEL, 'Unable to get workspace preferences data');
}
}

public async removeProviderFromSkipAuthorizationList(
namespace: string,
provider: api.GitProvider,
): Promise<void> {
const devWorkspacePreferences = await this.getWorkspacePreferences(namespace);

const skipAuthorization = devWorkspacePreferences[SKIP_AUTHORIZATION_KEY].filter(
(val: string) => val !== provider,
);
const skipAuthorizationStr =
skipAuthorization.length > 0 ? `[${skipAuthorization.sort().join(', ')}]` : '[]';
const data = Object.assign({}, devWorkspacePreferences, {
[SKIP_AUTHORIZATION_KEY]: skipAuthorizationStr,
});

try {
await this.coreV1API.patchNamespacedConfigMap(
DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace,
{ data },
undefined,
undefined,
undefined,
undefined,
undefined,
{
headers: {
'content-type': k8s.PatchUtils.PATCH_FORMAT_STRATEGIC_MERGE_PATCH,
},
},
);
} catch (error) {
const message = `Unable to update workspace preferences in the namespace "${namespace}"`;
throw createError(undefined, ERROR_LABEL, message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,6 @@ describe('SSH Keys API', () => {
response: {} as IncomingMessage,
});
},
// replaceNamespacedSecret: () => {
// return Promise.resolve({
// body: {} as V1Secret,
// response: {} as IncomingMessage,
// });
// },
deleteNamespacedSecret: () => {
return Promise.resolve({
body: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,21 @@ export interface IUserProfileApi {
getUserProfile(namespace: string): Promise<api.IUserProfile | undefined>;
}

export interface IDevWorkspacePreferencesApi {
/**
* Returns workspace preferences object that contains skip-authorisation info.
*/
getWorkspacePreferences(namespace: string): Promise<api.IDevWorkspacePreferences>;

/**
* Removes the target provider from skip-authorisation property from the workspace preferences object.
*/
removeProviderFromSkipAuthorizationList(
namespace: string,
provider: api.GitProvider,
): Promise<void>;
}

export interface IPersonalAccessTokenApi {
/**
* Reads all the PAT secrets from the specified namespace.
Expand Down Expand Up @@ -390,6 +405,7 @@ export interface IDevWorkspaceClient {
gitConfigApi: IGitConfigApi;
gettingStartedSampleApi: IGettingStartedSampleApi;
sshKeysApi: IShhKeysApi;
devWorkspacePreferencesApi: IDevWorkspacePreferencesApi;
}

export interface IWatcherService<T = Record<string, unknown>> {
Expand Down
5 changes: 5 additions & 0 deletions packages/dashboard-backend/src/models/restParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@
*/

import { V1alpha2DevWorkspace, V1alpha2DevWorkspaceTemplate } from '@devfile/api';
import { api } from '@eclipse-che/common';

export interface INamespacedParams {
namespace: string;
}
export interface IWorkspacePreferencesParams {
namespace: string;
provider: api.GitProvider;
}

export interface IDockerConfigParams {
dockerconfig: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2018-2023 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 { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';

import { baseApiPath } from '@/constants/config';
import { namespacedSchema, namespacedWorkspacePreferencesSchema } from '@/constants/schemas';
import { restParams } from '@/models';
import { getDevWorkspaceClient } from '@/routes/api/helpers/getDevWorkspaceClient';
import { getToken } from '@/routes/api/helpers/getToken';
import { getSchema } from '@/services/helpers';

const tags = ['WorkspacePreferences'];

export function registerWorkspacePreferencesRoute(instance: FastifyInstance) {
instance.register(async server => {
server.get(
`${baseApiPath}/workspace-preferences/namespace/:namespace`,
getSchema({ tags, params: namespacedSchema }),
async function (request: FastifyRequest) {
const { namespace } = request.params as restParams.INamespacedParams;
const token = getToken(request);
const { devWorkspacePreferencesApi } = getDevWorkspaceClient(token);
return devWorkspacePreferencesApi.getWorkspacePreferences(namespace);
},
);

server.delete(
akurinnoy marked this conversation as resolved.
Show resolved Hide resolved
`${baseApiPath}/workspace-preferences/namespace/:namespace/skip-authorisation/:provider`,
getSchema({
tags,
params: namespacedWorkspacePreferencesSchema,
response: {
204: {
description: 'The Provider is successfully removed from skip-authorisation list',
type: 'null',
},
},
}),
async function (request: FastifyRequest, reply: FastifyReply) {
const { namespace, provider } = request.params as restParams.IWorkspacePreferencesParams;
const token = getToken(request);
const { devWorkspacePreferencesApi } = getDevWorkspaceClient(token);
await devWorkspacePreferencesApi.removeProviderFromSkipAuthorizationList(
namespace,
provider,
);
reply.code(204);
return reply.send();
},
);
});
}
2 changes: 1 addition & 1 deletion packages/dashboard-frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ module.exports = {
},
],
},
setupFilesAfterEnv: ['./jest.setup.ts'],
setupFilesAfterEnv: ['./jest.setup.tsx'],
setupFiles: ['./src/inversify.config.ts'],
collectCoverageFrom: [
...base.collectCoverageFrom,
Expand Down
13 changes: 0 additions & 13 deletions packages/dashboard-frontend/jest.setup.ts

This file was deleted.

40 changes: 40 additions & 0 deletions packages/dashboard-frontend/jest.setup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2018-2023 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 '@testing-library/jest-dom';

import React from 'react';

jest.mock('@patternfly/react-core', () => {
return {
...jest.requireActual('@patternfly/react-core'),
// mock the Tooltip component from @patternfly/react-core
Tooltip: jest.fn(props => {
const { content, children, ...rest } = props;
return (
<div data-testid="patternfly-tooltip">
<span data-testid="tooltip-props">{JSON.stringify(rest)}</span>
<div data-testid="tooltip-content">{content}</div>
<div data-testid="tooltip-placed-to">{children}</div>
</div>
);
}),
};
});

jest.mock('@/components/CheTooltip', () => {
return {
CheTooltip: jest.fn(props => {
return React.createElement('div', null, props.children, props.content);
}),
};
});
Loading
Loading