diff --git a/che-theia-init-sources.yml b/che-theia-init-sources.yml index 703902687..381936f08 100644 --- a/che-theia-init-sources.yml +++ b/che-theia-init-sources.yml @@ -21,6 +21,7 @@ sources: - extensions/eclipse-che-theia-remote-api - extensions/eclipse-che-theia-remote-impl-che-server - extensions/eclipse-che-theia-remote-impl-k8s + - extensions/eclipse-che-theia-credentials plugins: - plugins/containers-plugin - plugins/ext-plugin diff --git a/extensions/eclipse-che-theia-credentials/.gitignore b/extensions/eclipse-che-theia-credentials/.gitignore new file mode 100644 index 000000000..7813a26ec --- /dev/null +++ b/extensions/eclipse-che-theia-credentials/.gitignore @@ -0,0 +1,10 @@ +conf +node_modules +dist +coverage +yarn-error.log +.vscode +lib +*.tgz +*.log +.eslintcache diff --git a/extensions/eclipse-che-theia-credentials/package.json b/extensions/eclipse-che-theia-credentials/package.json new file mode 100644 index 000000000..2d6ca155b --- /dev/null +++ b/extensions/eclipse-che-theia-credentials/package.json @@ -0,0 +1,55 @@ +{ + "name": "@eclipse-che/theia-credentials", + "keywords": [ + "theia-extension" + ], + "version": "0.0.1", + "description": "Eclipse Che - Theia credentials", + "dependencies": { + "@theia/core": "next", + "@kubernetes/client-node": "^0.12.1", + "@eclipse-che/theia-remote-impl-che-server": "0.0.1" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/credentials-frontend-module" + }, + { + "backend": "lib/node/che-theia-credentials-backend-module" + } + ], + "license": "EPL-2.0", + "files": [ + "lib", + "src", + "scripts", + "conf" + ], + "scripts": { + "prepare": "yarn clean && yarn build && yarn test", + "clean": "rimraf lib", + "format": "if-env SKIP_FORMAT=true && echo 'skip format check' || prettier --check '{src,tests}/**/*.ts' package.json", + "format:fix": "prettier --write '{src,tests}/**/*.ts' package.json", + "lint": "if-env SKIP_LINT=true && echo 'skip lint check' || eslint --cache=true --no-error-on-unmatched-pattern=true '{src,tests}/**/*.ts'", + "lint:fix": "eslint --fix --cache=true --no-error-on-unmatched-pattern=true \"{src,tests}/**/*.{ts,tsx}\"", + "compile": "tsc", + "build": "concurrently -n \"format,lint,compile\" -c \"red,green,blue\" \"yarn format\" \"yarn lint\" \"yarn compile\"", + "watch": "tsc -w", + "test": "if-env SKIP_TEST=true && echo 'skip test' || jest --forceExit" + }, + "jest": { + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}" + ], + "coverageDirectory": "coverage", + "modulePathIgnorePatterns": [ + "/lib" + ], + "preset": "ts-jest" + } +} diff --git a/extensions/eclipse-che-theia-credentials/src/browser/che-credentials-service.ts b/extensions/eclipse-che-theia-credentials/src/browser/che-credentials-service.ts new file mode 100644 index 000000000..44f890991 --- /dev/null +++ b/extensions/eclipse-che-theia-credentials/src/browser/che-credentials-service.ts @@ -0,0 +1,56 @@ +/********************************************************************** + * Copyright (c) 2019-2021 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 + ***********************************************************************/ + +import { CredentialsChangeEvent, CredentialsService } from '@theia/core/lib/browser/credentials-service'; +import { Emitter, Event } from '@theia/core'; +import { inject, injectable } from 'inversify'; + +import { CredentialsServer } from '../common/credentials-protocol'; + +@injectable() +export class CheCredentialsService implements CredentialsService { + @inject(CredentialsServer) + private readonly credentialsServer: CredentialsServer; + + private readonly onDidChangePasswordEmitter = new Emitter(); + readonly onDidChangePassword: Event = this.onDidChangePasswordEmitter.event; + + async deletePassword(service: string, account: string): Promise { + const result = await this.credentialsServer.deletePassword(this.getExtensionId(service), account); + if (result) { + this.onDidChangePasswordEmitter.fire({ service, account }); + } + return result; + } + + findCredentials(service: string): Promise> { + return this.credentialsServer.findCredentials(this.getExtensionId(service)); + } + + findPassword(service: string): Promise { + return this.credentialsServer.findPassword(this.getExtensionId(service)); + } + + async getPassword(service: string, account: string): Promise { + const passwordContent = await this.credentialsServer.getPassword(this.getExtensionId(service), account); + if (passwordContent) { + return JSON.stringify(passwordContent); + } + } + + async setPassword(service: string, account: string, password: string): Promise { + await this.credentialsServer.setPassword(this.getExtensionId(service), account, JSON.parse(password)); + this.onDidChangePasswordEmitter.fire({ service, account }); + } + + private getExtensionId(service: string): string { + return service.replace(window.location.hostname + '-', ''); + } +} diff --git a/extensions/eclipse-che-theia-credentials/src/browser/credentials-frontend-module.ts b/extensions/eclipse-che-theia-credentials/src/browser/credentials-frontend-module.ts new file mode 100644 index 000000000..ecab14431 --- /dev/null +++ b/extensions/eclipse-che-theia-credentials/src/browser/credentials-frontend-module.ts @@ -0,0 +1,26 @@ +/********************************************************************** + * Copyright (c) 2021 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 + ***********************************************************************/ + +import { CREDENTIALS_SERVICE_PATH, CredentialsServer } from '../common/credentials-protocol'; + +import { CheCredentialsService } from './che-credentials-service'; +import { ContainerModule } from 'inversify'; +import { CredentialsService } from '@theia/core/lib/browser/credentials-service'; +import { WebSocketConnectionProvider } from '@theia/core/lib/browser'; + +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(CheCredentialsService).toSelf().inSingletonScope(); + rebind(CredentialsService).to(CheCredentialsService).inSingletonScope(); + bind(CredentialsServer) + .toDynamicValue(context => + context.container.get(WebSocketConnectionProvider).createProxy(CREDENTIALS_SERVICE_PATH) + ) + .inSingletonScope(); +}); diff --git a/extensions/eclipse-che-theia-credentials/src/common/credentials-protocol.ts b/extensions/eclipse-che-theia-credentials/src/common/credentials-protocol.ts new file mode 100644 index 000000000..8538991a3 --- /dev/null +++ b/extensions/eclipse-che-theia-credentials/src/common/credentials-protocol.ts @@ -0,0 +1,25 @@ +/********************************************************************** + * Copyright (c) 2021 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 + ***********************************************************************/ + +export const CREDENTIALS_SERVICE_PATH = '/services/credentials'; +export const CredentialsServer = Symbol('CredentialsServer'); + +export interface PasswordContent { + extensionId: string; + content: string; +} + +export interface CredentialsServer { + setPassword(service: string, account: string, passwordData: PasswordContent): Promise; + getPassword(service: string, account: string): Promise; + deletePassword(service: string, account: string): Promise; + findPassword(service: string): Promise; + findCredentials(service: string): Promise>; +} diff --git a/extensions/eclipse-che-theia-credentials/src/node/che-credentials-server.ts b/extensions/eclipse-che-theia-credentials/src/node/che-credentials-server.ts new file mode 100644 index 000000000..b0aa4afba --- /dev/null +++ b/extensions/eclipse-che-theia-credentials/src/node/che-credentials-server.ts @@ -0,0 +1,111 @@ +/********************************************************************** + * Copyright (c) 2021 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 + ***********************************************************************/ + +import * as k8s from '@kubernetes/client-node'; + +import { CredentialsServer, PasswordContent } from '../common/credentials-protocol'; +import { inject, injectable } from 'inversify'; + +import { CheK8SServiceImpl } from '@eclipse-che/theia-remote-impl-che-server/lib/node/che-server-k8s-service-impl'; +import { CheServerWorkspaceServiceImpl } from '@eclipse-che/theia-remote-impl-che-server/lib/node/che-server-workspace-service-impl'; + +@injectable() +export class CheCredentialsServer implements CredentialsServer { + @inject(CheK8SServiceImpl) + private readonly cheK8SService: CheK8SServiceImpl; + + @inject(CheServerWorkspaceServiceImpl) + private readonly workspaceService: CheServerWorkspaceServiceImpl; + + private readonly CREDENTIALS_SECRET_NAME = 'workspace-credentials-secret'; + private readonly INFRASTRUCTURE_NAMESPACE = 'infrastructureNamespace'; + + async deletePassword(service: string, account: string): Promise { + try { + const patch = [ + { + op: 'remove', + path: `/data/${this.getSecretDataItemName(service, account)}`, + }, + ]; + const client = this.cheK8SService.makeApiClient(k8s.CoreV1Api); + client.defaultHeaders = { Accept: 'application/json', 'Content-Type': k8s.PatchUtils.PATCH_FORMAT_JSON_PATCH }; + await client.patchNamespacedSecret(this.CREDENTIALS_SECRET_NAME, await this.getWorkspaceNamespace(), patch); + return true; + } catch (e) { + console.error(e); + return false; + } + } + + async findCredentials(service: string): Promise> { + const secret = await this.cheK8SService + .makeApiClient(k8s.CoreV1Api) + .readNamespacedSecret(this.CREDENTIALS_SECRET_NAME, await this.getWorkspaceNamespace()); + const data = secret.body.data; + return data + ? Object.keys(data) + .filter(key => key.startsWith(service)) + .map(key => ({ + account: key.substring(key.indexOf('_') + 1), + password: Buffer.from(data[key], 'base64').toString('ascii'), + })) + : []; + } + + async findPassword(service: string): Promise { + const secret = await this.cheK8SService + .makeApiClient(k8s.CoreV1Api) + .readNamespacedSecret(this.CREDENTIALS_SECRET_NAME, await this.getWorkspaceNamespace()); + const data = secret.body.data; + if (data) { + const result = Object.keys(data).find(key => key.startsWith(service)); + if (result) { + return Buffer.from(data[result], 'base64').toString('ascii'); + } + } + } + + async getPassword(service: string, account: string): Promise { + const secret = await this.cheK8SService + .makeApiClient(k8s.CoreV1Api) + .readNamespacedSecret(this.CREDENTIALS_SECRET_NAME, await this.getWorkspaceNamespace()); + const data = secret.body.data; + if (data && data[this.getSecretDataItemName(service, account)]) { + return { + extensionId: service, + content: Buffer.from(secret.body.data![this.getSecretDataItemName(service, account)], 'base64').toString( + 'ascii' + ), + }; + } + } + + async setPassword(service: string, account: string, password: PasswordContent): Promise { + const client = this.cheK8SService.makeApiClient(k8s.CoreV1Api); + client.defaultHeaders = { + Accept: 'application/json', + 'Content-Type': k8s.PatchUtils.PATCH_FORMAT_STRATEGIC_MERGE_PATCH, + }; + await client.patchNamespacedSecret(this.CREDENTIALS_SECRET_NAME, await this.getWorkspaceNamespace(), { + data: { [this.getSecretDataItemName(service, account)]: Buffer.from(password.content).toString('base64') }, + }); + } + + private getSecretDataItemName(service: string, account: string): string { + return `${service}_${account}`; + } + + private async getWorkspaceNamespace(): Promise { + // grab current workspace + const workspace = await this.workspaceService.currentWorkspace(); + return workspace.attributes?.[this.INFRASTRUCTURE_NAMESPACE] || workspace.namespace || ''; + } +} diff --git a/extensions/eclipse-che-theia-credentials/src/node/che-theia-credentials-backend-module.ts b/extensions/eclipse-che-theia-credentials/src/node/che-theia-credentials-backend-module.ts new file mode 100644 index 000000000..217719308 --- /dev/null +++ b/extensions/eclipse-che-theia-credentials/src/node/che-theia-credentials-backend-module.ts @@ -0,0 +1,24 @@ +/********************************************************************** + * Copyright (c) 2019-2021 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 + ***********************************************************************/ + +import { CREDENTIALS_SERVICE_PATH, CredentialsServer } from '../common/credentials-protocol'; +import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core'; + +import { CheCredentialsServer } from './che-credentials-server'; +import { ContainerModule } from 'inversify'; + +export default new ContainerModule(bind => { + bind(CredentialsServer).to(CheCredentialsServer).inSingletonScope(); + bind(ConnectionHandler) + .toDynamicValue( + context => new JsonRpcConnectionHandler(CREDENTIALS_SERVICE_PATH, () => context.container.get(CredentialsServer)) + ) + .inSingletonScope(); +}); diff --git a/extensions/eclipse-che-theia-credentials/tests/no-op.spec.ts b/extensions/eclipse-che-theia-credentials/tests/no-op.spec.ts new file mode 100644 index 000000000..3ec8409a6 --- /dev/null +++ b/extensions/eclipse-che-theia-credentials/tests/no-op.spec.ts @@ -0,0 +1,13 @@ +/********************************************************************** + * Copyright (c) 2021 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 + ***********************************************************************/ + +describe('no-op', function () { + it('no-op', function () {}); +}); diff --git a/extensions/eclipse-che-theia-credentials/tsconfig.json b/extensions/eclipse-che-theia-credentials/tsconfig.json new file mode 100644 index 000000000..a6a832536 --- /dev/null +++ b/extensions/eclipse-che-theia-credentials/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../configs/base.tsconfig.json", + "compilerOptions": { + "lib": [ + "es6", + "dom" + ], + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ] +} diff --git a/generator/src/templates/assembly-compile.tsconfig.mst.json b/generator/src/templates/assembly-compile.tsconfig.mst.json index cf94075e6..262018203 100644 --- a/generator/src/templates/assembly-compile.tsconfig.mst.json +++ b/generator/src/templates/assembly-compile.tsconfig.mst.json @@ -50,6 +50,9 @@ }, { "path": "{{ packageRefPrefix}}eclipse-che-theia-mini-browser/tsconfig.json" + }, + { + "path": "{{ packageRefPrefix}}eclipse-che-theia-credentials/tsconfig.json" } ] }