Skip to content

Commit b816b9d

Browse files
committed
Introduce sshkeyservice
1 parent fedfc4a commit b816b9d

File tree

4 files changed

+191
-42
lines changed

4 files changed

+191
-42
lines changed

components/server/src/container-module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ import { RedisPublisher, newRedisClient } from "@gitpod/gitpod-db/lib";
129129
import { UserService } from "./user/user-service";
130130
import { RelationshipUpdater } from "./authorization/relationship-updater";
131131
import { WorkspaceService } from "./workspace/workspace-service";
132+
import { SSHKeyService } from "./user/sshkey-service";
132133

133134
export const productionContainerModule = new ContainerModule(
134135
(bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => {
@@ -143,6 +144,8 @@ export const productionContainerModule = new ContainerModule(
143144
bind(UserDeletionService).toSelf().inSingletonScope();
144145
bind(AuthorizationService).to(AuthorizationServiceImpl).inSingletonScope();
145146

147+
bind(SSHKeyService).toSelf().inSingletonScope();
148+
146149
bind(TokenService).toSelf().inSingletonScope();
147150
bind(TokenProvider).toService(TokenService);
148151

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { BUILTIN_INSTLLATION_ADMIN_USER_ID, TypeORM, UserDB } from "@gitpod/gitpod-db/lib";
8+
import { Organization, SSHPublicKeyValue, User } from "@gitpod/gitpod-protocol";
9+
import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
10+
import * as chai from "chai";
11+
import { Container } from "inversify";
12+
import "mocha";
13+
import { createTestContainer } from "../test/service-testing-container-module";
14+
import { resetDB } from "@gitpod/gitpod-db/lib/test/reset-db";
15+
import { SSHKeyService } from "./sshkey-service";
16+
import { OrganizationService } from "../orgs/organization-service";
17+
18+
const expect = chai.expect;
19+
20+
describe("SSHKeyService", async () => {
21+
let container: Container;
22+
let ss: SSHKeyService;
23+
24+
let owner: User;
25+
let member: User;
26+
let org: Organization;
27+
28+
const testSSHkeys: SSHPublicKeyValue[] = [
29+
{
30+
name: "foo",
31+
key: "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBN+Mh3U/3We4VYtV1QmWUFIzFLTUeegl1Ao5/QGtCRGAZn8bxX9KlCrrWISIjSYAwCajIEGSPEZwPNMBoK8XD8Q= test@gitpod",
32+
},
33+
{
34+
name: "bar",
35+
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK0wmN/Cr3JXqmLW7u+g9pTh+wyqDHpSQEIQczXkVx9q bar@gitpod",
36+
},
37+
];
38+
39+
beforeEach(async () => {
40+
container = createTestContainer();
41+
Experiments.configureTestingClient({
42+
centralizedPermissions: true,
43+
});
44+
45+
const userDB = container.get<UserDB>(UserDB);
46+
owner = await userDB.newUser();
47+
48+
const os = container.get(OrganizationService);
49+
org = await os.createOrganization(owner.id, "myorg");
50+
51+
member = await userDB.newUser();
52+
const invite = await os.getOrCreateInvite(owner.id, org.id);
53+
await os.joinOrganization(member.id, invite.id);
54+
55+
const adminUser = await userDB.findUserById(BUILTIN_INSTLLATION_ADMIN_USER_ID)!;
56+
if (!adminUser) {
57+
throw new Error("admin user not found");
58+
}
59+
60+
ss = container.get(SSHKeyService);
61+
});
62+
63+
afterEach(async () => {
64+
// Clean-up database
65+
await resetDB(container.get(TypeORM));
66+
});
67+
68+
it("should add ssh key", async () => {
69+
const resp1 = await ss.hasSSHPublicKey(member.id);
70+
expect(resp1).to.be.false;
71+
72+
await ss.addSSHPublicKey(member.id, testSSHkeys[0]);
73+
74+
const resp2 = await ss.hasSSHPublicKey(member.id);
75+
expect(resp2).to.be.true;
76+
});
77+
78+
it("should list ssh keys", async () => {
79+
await ss.addSSHPublicKey(member.id, testSSHkeys[0]);
80+
await ss.addSSHPublicKey(member.id, testSSHkeys[1]);
81+
82+
const keys = await ss.getSSHPublicKeys(member.id);
83+
expect(keys.length).to.equal(2);
84+
expect(testSSHkeys.some((k) => k.name === keys[0].name && k.key === keys[0].key)).to.be.true;
85+
expect(testSSHkeys.some((k) => k.name === keys[1].name && k.key === keys[1].key)).to.be.true;
86+
});
87+
88+
it("should delete ssh keys", async () => {
89+
await ss.addSSHPublicKey(member.id, testSSHkeys[0]);
90+
await ss.addSSHPublicKey(member.id, testSSHkeys[1]);
91+
92+
const keys = await ss.getSSHPublicKeys(member.id);
93+
expect(keys.length).to.equal(2);
94+
95+
await ss.deleteSSHPublicKey(member.id, keys[0].id);
96+
97+
const keys2 = await ss.getSSHPublicKeys(member.id);
98+
expect(keys2.length).to.equal(1);
99+
});
100+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { DBWithTracing, TracedWorkspaceDB, UserDB, WorkspaceDB } from "@gitpod/gitpod-db/lib";
8+
import { SSHPublicKeyValue, UserSSHPublicKeyValue, WorkspaceInstance } from "@gitpod/gitpod-protocol";
9+
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
10+
import { inject, injectable } from "inversify";
11+
import { UpdateSSHKeyRequest } from "@gitpod/ws-manager/lib";
12+
import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider";
13+
14+
@injectable()
15+
export class SSHKeyService {
16+
constructor(
17+
@inject(TracedWorkspaceDB) private readonly workspaceDb: DBWithTracing<WorkspaceDB>,
18+
@inject(UserDB) private readonly userDB: UserDB,
19+
@inject(WorkspaceManagerClientProvider)
20+
private readonly workspaceManagerClientProvider: WorkspaceManagerClientProvider,
21+
) {}
22+
23+
async hasSSHPublicKey(userId: string): Promise<boolean> {
24+
return this.userDB.hasSSHPublicKey(userId);
25+
}
26+
27+
async getSSHPublicKeys(userId: string): Promise<UserSSHPublicKeyValue[]> {
28+
const list = await this.userDB.getSSHPublicKeys(userId);
29+
return list.map((e) => ({
30+
id: e.id,
31+
name: e.name,
32+
key: e.key,
33+
fingerprint: e.fingerprint,
34+
creationTime: e.creationTime,
35+
lastUsedTime: e.lastUsedTime,
36+
}));
37+
}
38+
39+
async addSSHPublicKey(userId: string, value: SSHPublicKeyValue): Promise<UserSSHPublicKeyValue> {
40+
const data = await this.userDB.addSSHPublicKey(userId, value);
41+
this.updateSSHKeysForRegularRunningInstances(userId).catch(() => {
42+
/* noop */
43+
});
44+
return {
45+
id: data.id,
46+
name: data.name,
47+
key: data.key,
48+
fingerprint: data.fingerprint,
49+
creationTime: data.creationTime,
50+
lastUsedTime: data.lastUsedTime,
51+
};
52+
}
53+
54+
async deleteSSHPublicKey(userId: string, id: string): Promise<void> {
55+
await this.userDB.deleteSSHPublicKey(userId, id);
56+
this.updateSSHKeysForRegularRunningInstances(userId).catch(() => {
57+
/* noop */
58+
});
59+
}
60+
61+
private async updateSSHKeysForRegularRunningInstances(userId: string) {
62+
const updateSSHKeysOfInstance = async (instance: WorkspaceInstance, sshKeys: string[]) => {
63+
try {
64+
const req = new UpdateSSHKeyRequest();
65+
req.setId(instance.id);
66+
req.setKeysList(sshKeys);
67+
const cli = await this.workspaceManagerClientProvider.get(instance.region);
68+
await cli.updateSSHPublicKey({}, req);
69+
} catch (err) {
70+
const logCtx = { userId, instanceId: instance.id };
71+
log.error(logCtx, "Could not update ssh public key for instance", err);
72+
}
73+
};
74+
try {
75+
const sshKeys = (await this.userDB.getSSHPublicKeys(userId)).map((e) => e.key);
76+
const instances = await this.workspaceDb.trace({}).findRegularRunningInstances(userId);
77+
return Promise.allSettled(instances.map((instance) => updateSSHKeysOfInstance(instance, sshKeys)));
78+
} catch (err) {
79+
log.error("Failed to update ssh keys on running instances.", err);
80+
}
81+
}
82+
}

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ import {
111111
PortProtocol as ProtoPortProtocol,
112112
StopWorkspacePolicy,
113113
TakeSnapshotRequest,
114-
UpdateSSHKeyRequest,
115114
} from "@gitpod/ws-manager/lib/core_pb";
116115
import * as crypto from "crypto";
117116
import { inject, injectable } from "inversify";
@@ -194,6 +193,7 @@ import { RedisSubscriber } from "../messaging/redis-subscriber";
194193
import { UsageService } from "../orgs/usage-service";
195194
import { UserService } from "../user/user-service";
196195
import { WorkspaceService } from "./workspace-service";
196+
import { SSHKeyService } from "../user/sshkey-service";
197197

198198
// shortcut
199199
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
@@ -236,6 +236,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
236236
@inject(UserDeletionService) private readonly userDeletionService: UserDeletionService,
237237
@inject(IAnalyticsWriter) private readonly analytics: IAnalyticsWriter,
238238
@inject(AuthorizationService) private readonly authorizationService: AuthorizationService,
239+
@inject(SSHKeyService) private readonly sshKeyservice: SSHKeyService,
239240

240241
@inject(TeamDB) private readonly teamDB: TeamDB,
241242
@inject(OrganizationService) private readonly organizationService: OrganizationService,
@@ -2445,59 +2446,22 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
24452446

24462447
async hasSSHPublicKey(ctx: TraceContext): Promise<boolean> {
24472448
const user = await this.checkUser("hasSSHPublicKey");
2448-
return this.userDB.hasSSHPublicKey(user.id);
2449+
return this.sshKeyservice.hasSSHPublicKey(user.id);
24492450
}
24502451

24512452
async getSSHPublicKeys(ctx: TraceContext): Promise<UserSSHPublicKeyValue[]> {
24522453
const user = await this.checkUser("getSSHPublicKeys");
2453-
const list = await this.userDB.getSSHPublicKeys(user.id);
2454-
return list.map((e) => ({
2455-
id: e.id,
2456-
name: e.name,
2457-
key: e.key,
2458-
fingerprint: e.fingerprint,
2459-
creationTime: e.creationTime,
2460-
lastUsedTime: e.lastUsedTime,
2461-
}));
2454+
return this.sshKeyservice.getSSHPublicKeys(user.id);
24622455
}
24632456

24642457
async addSSHPublicKey(ctx: TraceContext, value: SSHPublicKeyValue): Promise<UserSSHPublicKeyValue> {
24652458
const user = await this.checkUser("addSSHPublicKey");
2466-
const data = await this.userDB.addSSHPublicKey(user.id, value);
2467-
this.updateSSHKeysForRegularRunningInstances(ctx, user.id).catch(console.error);
2468-
return {
2469-
id: data.id,
2470-
name: data.name,
2471-
key: data.key,
2472-
fingerprint: data.fingerprint,
2473-
creationTime: data.creationTime,
2474-
lastUsedTime: data.lastUsedTime,
2475-
};
2459+
return this.sshKeyservice.addSSHPublicKey(user.id, value);
24762460
}
24772461

24782462
async deleteSSHPublicKey(ctx: TraceContext, id: string): Promise<void> {
24792463
const user = await this.checkUser("deleteSSHPublicKey");
2480-
await this.userDB.deleteSSHPublicKey(user.id, id);
2481-
this.updateSSHKeysForRegularRunningInstances(ctx, user.id).catch(console.error);
2482-
return;
2483-
}
2484-
2485-
private async updateSSHKeysForRegularRunningInstances(ctx: TraceContext, userId: string) {
2486-
const keys = (await this.userDB.getSSHPublicKeys(userId)).map((e) => e.key);
2487-
const instances = await this.workspaceDb.trace(ctx).findRegularRunningInstances(userId);
2488-
const updateKeyOfInstance = async (instance: WorkspaceInstance) => {
2489-
try {
2490-
const req = new UpdateSSHKeyRequest();
2491-
req.setId(instance.id);
2492-
req.setKeysList(keys);
2493-
const cli = await this.workspaceManagerClientProvider.get(instance.region);
2494-
await cli.updateSSHPublicKey(ctx, req);
2495-
} catch (err) {
2496-
const logCtx = { userId, instanceId: instance.id };
2497-
log.error(logCtx, "Could not update ssh public key for instance", err);
2498-
}
2499-
};
2500-
return Promise.allSettled(instances.map((e) => updateKeyOfInstance(e)));
2464+
return this.sshKeyservice.deleteSSHPublicKey(user.id, id);
25012465
}
25022466

25032467
async setProjectEnvironmentVariable(

0 commit comments

Comments
 (0)