Skip to content

Commit c9a6b1b

Browse files
committed
Add protocol and implement for ssh public keys
1 parent 565d03d commit c9a6b1b

File tree

14 files changed

+352
-0
lines changed

14 files changed

+352
-0
lines changed

components/gitpod-db/src/tables.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ export class GitpodTableDescriptionProvider implements TableDescriptionProvider
268268
deletionColumn: "deleted",
269269
timeColumn: "_lastModified",
270270
},
271+
{
272+
name: "d_b_user_ssh_public_key",
273+
primaryKeys: ["id"],
274+
timeColumn: "_lastModified",
275+
},
271276
/**
272277
* BEWARE
273278
*

components/gitpod-db/src/typeorm/deleted-entry-gc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const tables: TableWithDeletion[] = [
6363
{ deletionColumn: "deleted", name: "d_b_project_info" },
6464
{ deletionColumn: "deleted", name: "d_b_project_usage" },
6565
{ deletionColumn: "deleted", name: "d_b_team_subscription2" },
66+
{ deletionColumn: "deleted", name: "d_b_user_ssh_public_key" },
6667
];
6768

6869
interface TableWithDeletion {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Copyright (c) 2020 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 { PrimaryColumn, Column, Entity, Index } from "typeorm";
8+
import { TypeORM } from "../typeorm";
9+
import { UserSSHPublicKey } from "@gitpod/gitpod-protocol";
10+
import { Transformer } from "../transformer";
11+
import { encryptionService } from "../user-db-impl";
12+
13+
@Entity("d_b_user_ssh_public_key")
14+
export class DBUserSshPublicKey implements UserSSHPublicKey {
15+
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
16+
id: string;
17+
18+
@Column(TypeORM.UUID_COLUMN_TYPE)
19+
@Index("ind_userId")
20+
userId: string;
21+
22+
@Column("varchar")
23+
name: string;
24+
25+
@Column({
26+
type: "simple-json",
27+
// Relies on the initialization of the var in UserDbImpl
28+
transformer: Transformer.compose(
29+
Transformer.SIMPLE_JSON([]),
30+
Transformer.encrypted(() => encryptionService),
31+
),
32+
})
33+
key: string;
34+
35+
@Column("varchar")
36+
@Index("ind_unique_userId_fingerprint", ["userId", "fingerprint"])
37+
fingerprint: string;
38+
39+
@Column({
40+
type: "timestamp",
41+
precision: 6,
42+
default: () => "CURRENT_TIMESTAMP(6)",
43+
transformer: Transformer.MAP_ISO_STRING_TO_TIMESTAMP_DROP,
44+
})
45+
@Index("ind_creationTime")
46+
creationTime: string;
47+
48+
@Column({
49+
type: "timestamp",
50+
precision: 6,
51+
transformer: Transformer.MAP_ISO_STRING_TO_TIMESTAMP_DROP,
52+
})
53+
lastUsedTime?: string;
54+
55+
// This column triggers the db-sync deletion mechanism. It's not intended for public consumption.
56+
@Column()
57+
deleted: boolean;
58+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) 2022 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 { MigrationInterface, QueryRunner } from "typeorm";
8+
9+
export class UserSshPublicKey1654842204415 implements MigrationInterface {
10+
public async up(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(
12+
"CREATE TABLE IF NOT EXISTS `d_b_user_ssh_public_key` ( `id` char(36) NOT NULL, `userId` char(36) NOT NULL, `name` varchar(255) NOT NULL, `key` text NOT NULL, `fingerprint` varchar(255) NOT NULL, `deleted` tinyint(4) NOT NULL DEFAULT '0', `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `creationTime` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `lastUsedTime` timestamp(6) NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY ind_userId (`userId`), KEY ind_creationTime (`creationTime`), UNIQUE KEY ind_unique_userId_fingerprint (`userId`, `fingerprint`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;",
13+
);
14+
}
15+
16+
public async down(queryRunner: QueryRunner): Promise<void> {}
17+
}

components/gitpod-db/src/typeorm/user-db-impl.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import {
1010
GitpodTokenType,
1111
Identity,
1212
IdentityLookup,
13+
SSHPublicKeyValue,
1314
Token,
1415
TokenEntry,
1516
User,
1617
UserEnvVar,
18+
UserSSHPublicKey,
1719
} from "@gitpod/gitpod-protocol";
1820
import { EncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
1921
import {
@@ -41,6 +43,7 @@ import { DBTokenEntry } from "./entity/db-token-entry";
4143
import { DBUser } from "./entity/db-user";
4244
import { DBUserEnvVar } from "./entity/db-user-env-vars";
4345
import { DBWorkspace } from "./entity/db-workspace";
46+
import { DBUserSshPublicKey } from "./entity/db-user-ssh-public-key";
4447
import { TypeORM } from "./typeorm";
4548
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
4649

@@ -95,6 +98,10 @@ export class TypeORMUserDBImpl implements UserDB {
9598
return (await this.getEntityManager()).getRepository<DBUserEnvVar>(DBUserEnvVar);
9699
}
97100

101+
protected async getSSHPublicKeyRepo(): Promise<Repository<DBUserSshPublicKey>> {
102+
return (await this.getEntityManager()).getRepository<DBUserSshPublicKey>(DBUserSshPublicKey);
103+
}
104+
98105
public async newUser(): Promise<User> {
99106
const user: User = {
100107
id: uuidv4(),
@@ -395,6 +402,42 @@ export class TypeORMUserDBImpl implements UserDB {
395402
await repo.save(envVar);
396403
}
397404

405+
public async getSSHPublicKeys(userId: string): Promise<UserSSHPublicKey[]> {
406+
const repo = await this.getSSHPublicKeyRepo();
407+
return repo.find({ where: { userId, deleted: false }, order: { creationTime: "ASC" } });
408+
}
409+
410+
public async addSSHPublicKey(userId: string, value: SSHPublicKeyValue): Promise<UserSSHPublicKey> {
411+
const repo = await this.getSSHPublicKeyRepo();
412+
const fingerprint = SSHPublicKeyValue.getFingerprint(value);
413+
const allKeys = await repo.find({ where: { userId } });
414+
const prevOne = allKeys.find((e) => e.fingerprint === fingerprint);
415+
if (!!prevOne) {
416+
if (prevOne.deleted === false) {
417+
throw new Error(`Duplicate public key with ${prevOne.name}`);
418+
}
419+
prevOne.deleted = false;
420+
return repo.save(prevOne);
421+
}
422+
const availableKeys = allKeys.filter((e) => e.deleted === false);
423+
if (availableKeys.length > SSHPublicKeyValue.MAXIMUM_KEY_LENGTH) {
424+
throw new Error(`The maximum of public keys is ${SSHPublicKeyValue.MAXIMUM_KEY_LENGTH}`);
425+
}
426+
return repo.save({
427+
userId,
428+
fingerprint,
429+
name: value.name,
430+
key: value.key,
431+
creationTime: new Date().toISOString(),
432+
deleted: false,
433+
});
434+
}
435+
436+
public async deleteSSHPublicKey(userId: string, id: string): Promise<void> {
437+
const repo = await this.getSSHPublicKeyRepo();
438+
await repo.update({ userId, id }, { deleted: true });
439+
}
440+
398441
public async findAllUsers(
399442
offset: number,
400443
limit: number,

components/gitpod-db/src/user-db.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import {
1010
GitpodTokenType,
1111
Identity,
1212
IdentityLookup,
13+
SSHPublicKeyValue,
1314
Token,
1415
TokenEntry,
1516
User,
1617
UserEnvVar,
18+
UserSSHPublicKey,
1719
} from "@gitpod/gitpod-protocol";
1820
import { OAuthTokenRepository, OAuthUserRepository } from "@jmondi/oauth2-server";
1921
import { Repository } from "typeorm";
@@ -117,6 +119,11 @@ export interface UserDB extends OAuthUserRepository, OAuthTokenRepository {
117119
deleteEnvVar(envVar: UserEnvVar): Promise<void>;
118120
getEnvVars(userId: string): Promise<UserEnvVar[]>;
119121

122+
// User SSH Keys
123+
getSSHPublicKeys(userId: string): Promise<UserSSHPublicKey[]>;
124+
addSSHPublicKey(userId: string, value: SSHPublicKeyValue): Promise<UserSSHPublicKey>;
125+
deleteSSHPublicKey(userId: string, id: string): Promise<void>;
126+
120127
findAllUsers(
121128
offset: number,
122129
limit: number,

components/gitpod-protocol/go/gitpod-service.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ type APIInterface interface {
6464
GetEnvVars(ctx context.Context) (res []*UserEnvVarValue, err error)
6565
SetEnvVar(ctx context.Context, variable *UserEnvVarValue) (err error)
6666
DeleteEnvVar(ctx context.Context, variable *UserEnvVarValue) (err error)
67+
GetSSHPublicKeys(ctx context.Context) (res []*UserSSHPublicKeyValue, err error)
68+
AddSSHPublicKey(ctx context.Context, value *SSHPublicKeyValue) (res *UserSSHPublicKeyValue, err error)
69+
DeleteSSHPublicKey(ctx context.Context, id string) (err error)
6770
GetContentBlobUploadURL(ctx context.Context, name string) (url string, err error)
6871
GetContentBlobDownloadURL(ctx context.Context, name string) (url string, err error)
6972
GetGitpodTokens(ctx context.Context) (res []*APIToken, err error)
@@ -168,6 +171,12 @@ const (
168171
FunctionSetEnvVar FunctionName = "setEnvVar"
169172
// FunctionDeleteEnvVar is the name of the deleteEnvVar function
170173
FunctionDeleteEnvVar FunctionName = "deleteEnvVar"
174+
// FunctionGetSSHPublicKeys is the name of the getSSHPublicKeys function
175+
FunctionGetSSHPublicKeys FunctionName = "getSSHPublicKeys"
176+
// FunctionAddSSHPublicKey is the name of the addSSHPublicKey function
177+
FunctionAddSSHPublicKey FunctionName = "addSSHPublicKey"
178+
// FunctionDeleteSSHPublicKey is the name of the deleteSSHPublicKey function
179+
FunctionDeleteSSHPublicKey FunctionName = "deleteSSHPublicKey"
171180
// FunctionGetContentBlobUploadURL is the name fo the getContentBlobUploadUrl function
172181
FunctionGetContentBlobUploadURL FunctionName = "getContentBlobUploadUrl"
173182
// FunctionGetContentBlobDownloadURL is the name fo the getContentBlobDownloadUrl function
@@ -1117,6 +1126,39 @@ func (gp *APIoverJSONRPC) DeleteEnvVar(ctx context.Context, variable *UserEnvVar
11171126
return
11181127
}
11191128

1129+
// GetSSHPublicKeys calls getSSHPublicKeys on the server
1130+
func (gp *APIoverJSONRPC) GetSSHPublicKeys(ctx context.Context) (res []*UserSSHPublicKeyValue, err error) {
1131+
if gp == nil {
1132+
err = errNotConnected
1133+
return
1134+
}
1135+
var _params []interface{}
1136+
err = gp.C.Call(ctx, "getSSHPublicKeys", _params, &res)
1137+
return
1138+
}
1139+
1140+
// AddSSHPublicKey calls addSSHPublicKey on the server
1141+
func (gp *APIoverJSONRPC) AddSSHPublicKey(ctx context.Context, value *SSHPublicKeyValue) (res *UserSSHPublicKeyValue, err error) {
1142+
if gp == nil {
1143+
err = errNotConnected
1144+
return
1145+
}
1146+
_params := []interface{}{value}
1147+
err = gp.C.Call(ctx, "addSSHPublicKey", _params, &res)
1148+
return
1149+
}
1150+
1151+
// DeleteSSHPublicKey calls deleteSSHPublicKey on the server
1152+
func (gp *APIoverJSONRPC) DeleteSSHPublicKey(ctx context.Context, id string) (err error) {
1153+
if gp == nil {
1154+
err = errNotConnected
1155+
return
1156+
}
1157+
_params := []interface{}{id}
1158+
err = gp.C.Call(ctx, "deleteSSHPublicKey", _params, nil)
1159+
return
1160+
}
1161+
11201162
// GetContentBlobUploadURL calls getContentBlobUploadUrl on the server
11211163
func (gp *APIoverJSONRPC) GetContentBlobUploadURL(ctx context.Context, name string) (url string, err error) {
11221164
if gp == nil {
@@ -1789,6 +1831,19 @@ type UserEnvVarValue struct {
17891831
Value string `json:"value,omitempty"`
17901832
}
17911833

1834+
type SSHPublicKeyValue struct {
1835+
Name string `json:"name,omitempty"`
1836+
Key string `json:"key,omitempty"`
1837+
}
1838+
1839+
type UserSSHPublicKeyValue struct {
1840+
ID string `json:"id,omitempty"`
1841+
Name string `json:"name,omitempty"`
1842+
Fingerprint string `json:"fingerprint,omitempty"`
1843+
CreationTime string `json:"creationTime,omitempty"`
1844+
LastUsedTime string `json:"lastUsedTime,omitempty"`
1845+
}
1846+
17921847
// GenerateNewGitpodTokenOptions is the GenerateNewGitpodTokenOptions message type
17931848
type GenerateNewGitpodTokenOptions struct {
17941849
Name string `json:"name,omitempty"`

components/gitpod-protocol/go/mock.go

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
GuessGitTokenScopesParams,
2525
GuessedGitTokenScopes,
2626
ProjectEnvVar,
27+
UserSSHPublicKeyValue,
28+
SSHPublicKeyValue,
2729
} from "./protocol";
2830
import {
2931
Team,
@@ -147,6 +149,11 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
147149
setEnvVar(variable: UserEnvVarValue): Promise<void>;
148150
deleteEnvVar(variable: UserEnvVarValue): Promise<void>;
149151

152+
// User SSH Keys
153+
getSSHPublicKeys(): Promise<UserSSHPublicKeyValue[]>;
154+
addSSHPublicKey(value: SSHPublicKeyValue): Promise<UserSSHPublicKeyValue>;
155+
deleteSSHPublicKey(id: string): Promise<void>;
156+
150157
// Teams
151158
getTeams(): Promise<Team[]>;
152159
getTeamMembers(teamId: string): Promise<TeamMemberInfo[]>;

0 commit comments

Comments
 (0)