Skip to content
This repository has been archived by the owner on Apr 4, 2023. It is now read-only.

Commit

Permalink
fix: Adapt the Secrets plugin API to use kubernetes secrets (#1166)
Browse files Browse the repository at this point in the history
Rework the Secrets API from upstream theia to use kubernetes secrets
  • Loading branch information
vinokurig authored Aug 18, 2021
1 parent 41bb0c1 commit 4432ee3
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 0 deletions.
1 change: 1 addition & 0 deletions che-theia-init-sources.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions extensions/eclipse-che-theia-credentials/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
conf
node_modules
dist
coverage
yarn-error.log
.vscode
lib
*.tgz
*.log
.eslintcache
55 changes: 55 additions & 0 deletions extensions/eclipse-che-theia-credentials/package.json
Original file line number Diff line number Diff line change
@@ -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": [
"<rootDir>/lib"
],
"preset": "ts-jest"
}
}
Original file line number Diff line number Diff line change
@@ -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<CredentialsChangeEvent>();
readonly onDidChangePassword: Event<CredentialsChangeEvent> = this.onDidChangePasswordEmitter.event;

async deletePassword(service: string, account: string): Promise<boolean> {
const result = await this.credentialsServer.deletePassword(this.getExtensionId(service), account);
if (result) {
this.onDidChangePasswordEmitter.fire({ service, account });
}
return result;
}

findCredentials(service: string): Promise<Array<{ account: string; password: string }>> {
return this.credentialsServer.findCredentials(this.getExtensionId(service));
}

findPassword(service: string): Promise<string | undefined> {
return this.credentialsServer.findPassword(this.getExtensionId(service));
}

async getPassword(service: string, account: string): Promise<string | undefined> {
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<void> {
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 + '-', '');
}
}
Original file line number Diff line number Diff line change
@@ -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<CredentialsServer>(CREDENTIALS_SERVICE_PATH)
)
.inSingletonScope();
});
Original file line number Diff line number Diff line change
@@ -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<void>;
getPassword(service: string, account: string): Promise<PasswordContent | undefined>;
deletePassword(service: string, account: string): Promise<boolean>;
findPassword(service: string): Promise<string | undefined>;
findCredentials(service: string): Promise<Array<{ account: string; password: string }>>;
}
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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<Array<{ account: string; password: string }>> {
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<string | undefined> {
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<PasswordContent | undefined> {
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<void> {
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<string> {
// grab current workspace
const workspace = await this.workspaceService.currentWorkspace();
return workspace.attributes?.[this.INFRASTRUCTURE_NAMESPACE] || workspace.namespace || '';
}
}
Original file line number Diff line number Diff line change
@@ -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();
});
13 changes: 13 additions & 0 deletions extensions/eclipse-che-theia-credentials/tests/no-op.spec.ts
Original file line number Diff line number Diff line change
@@ -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 () {});
});
14 changes: 14 additions & 0 deletions extensions/eclipse-che-theia-credentials/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../configs/base.tsconfig.json",
"compilerOptions": {
"lib": [
"es6",
"dom"
],
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
]
}
3 changes: 3 additions & 0 deletions generator/src/templates/assembly-compile.tsconfig.mst.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
},
{
"path": "{{ packageRefPrefix}}eclipse-che-theia-mini-browser/tsconfig.json"
},
{
"path": "{{ packageRefPrefix}}eclipse-che-theia-credentials/tsconfig.json"
}
]
}

0 comments on commit 4432ee3

Please sign in to comment.