Skip to content

Commit 8c2a74f

Browse files
committed
[server,db,protocol] support ssh public key
1 parent 6afe081 commit 8c2a74f

File tree

13 files changed

+413
-0
lines changed

13 files changed

+413
-0
lines changed

components/gitpod-db/src/tables.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,12 @@ export class GitpodTableDescriptionProvider implements TableDescriptionProvider
262262
deletionColumn: "deleted",
263263
timeColumn: "_lastModified",
264264
},
265+
{
266+
name: "d_b_user_ssh_public_key",
267+
primaryKeys: ["id"],
268+
deletionColumn: "deleted",
269+
timeColumn: "_lastModified",
270+
},
265271
];
266272

267273
public getSortedTables(): TableDescription[] {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 { 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+
fingerprint: string;
37+
38+
@Column({
39+
type: "timestamp",
40+
precision: 6,
41+
default: () => "CURRENT_TIMESTAMP(6)",
42+
transformer: Transformer.MAP_ISO_STRING_TO_TIMESTAMP_DROP,
43+
})
44+
@Index("ind_creationTime")
45+
creationTime: string;
46+
47+
@Column({
48+
default: "",
49+
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED,
50+
})
51+
lastUsedTime?: string;
52+
53+
// This column triggers the db-sync deletion mechanism. It's not intended for public consumption.
54+
@Column()
55+
deleted: boolean;
56+
}
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` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY ind_userId (`userId`), KEY ind_creationTime (`creationTime`) ) 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: 44 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,43 @@ export class TypeORMUserDBImpl implements UserDB {
395402
await repo.save(envVar);
396403
}
397404

405+
public async hasSSHPublicKey(userId: string): Promise<boolean> {
406+
const repo = await this.getSSHPublicKeyRepo();
407+
return !!(await repo.findOne({ where: { userId, deleted: false } }));
408+
}
409+
410+
public async getSSHPublicKeys(userId: string): Promise<UserSSHPublicKey[]> {
411+
const repo = await this.getSSHPublicKeyRepo();
412+
return repo.find({ where: { userId, deleted: false }, order: { creationTime: "ASC" } });
413+
}
414+
415+
public async addSSHPublicKey(userId: string, value: SSHPublicKeyValue): Promise<UserSSHPublicKey> {
416+
const repo = await this.getSSHPublicKeyRepo();
417+
const fingerprint = SSHPublicKeyValue.getFingerprint(value);
418+
const allKeys = await repo.find({ where: { userId, deleted: false } });
419+
const prevOne = allKeys.find((e) => e.fingerprint === fingerprint);
420+
if (!!prevOne) {
421+
throw new Error(`Duplicate public key with ${prevOne.name}`);
422+
}
423+
if (allKeys.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+
id: uuidv4(),
428+
userId,
429+
fingerprint,
430+
name: value.name,
431+
key: value.key,
432+
creationTime: new Date().toISOString(),
433+
deleted: false,
434+
});
435+
}
436+
437+
public async deleteSSHPublicKey(userId: string, id: string): Promise<void> {
438+
const repo = await this.getSSHPublicKeyRepo();
439+
await repo.update({ userId, id }, { deleted: true });
440+
}
441+
398442
public async findAllUsers(
399443
offset: number,
400444
limit: number,

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

Lines changed: 8 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,12 @@ export interface UserDB extends OAuthUserRepository, OAuthTokenRepository {
117119
deleteEnvVar(envVar: UserEnvVar): Promise<void>;
118120
getEnvVars(userId: string): Promise<UserEnvVar[]>;
119121

122+
// User SSH Keys
123+
hasSSHPublicKey(userId: string): Promise<boolean>;
124+
getSSHPublicKeys(userId: string): Promise<UserSSHPublicKey[]>;
125+
addSSHPublicKey(userId: string, value: SSHPublicKeyValue): Promise<UserSSHPublicKey>;
126+
deleteSSHPublicKey(userId: string, id: string): Promise<void>;
127+
120128
findAllUsers(
121129
offset: number,
122130
limit: number,

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ 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+
HasSSHPublicKey(ctx context.Context) (res bool, err error)
68+
GetSSHPublicKeys(ctx context.Context) (res []*UserSSHPublicKeyValue, err error)
69+
AddSSHPublicKey(ctx context.Context, value *SSHPublicKeyValue) (res *UserSSHPublicKeyValue, err error)
70+
DeleteSSHPublicKey(ctx context.Context, id string) (err error)
6771
GetContentBlobUploadURL(ctx context.Context, name string) (url string, err error)
6872
GetContentBlobDownloadURL(ctx context.Context, name string) (url string, err error)
6973
GetGitpodTokens(ctx context.Context) (res []*APIToken, err error)
@@ -168,6 +172,14 @@ const (
168172
FunctionSetEnvVar FunctionName = "setEnvVar"
169173
// FunctionDeleteEnvVar is the name of the deleteEnvVar function
170174
FunctionDeleteEnvVar FunctionName = "deleteEnvVar"
175+
// FunctionHasSSHPublicKey is the name of the hasSSHPublicKey function
176+
FunctionHasSSHPublicKey FunctionName = "hasSSHPublicKey"
177+
// FunctionGetSSHPublicKeys is the name of the getSSHPublicKeys function
178+
FunctionGetSSHPublicKeys FunctionName = "getSSHPublicKeys"
179+
// FunctionAddSSHPublicKey is the name of the addSSHPublicKey function
180+
FunctionAddSSHPublicKey FunctionName = "addSSHPublicKey"
181+
// FunctionDeleteSSHPublicKey is the name of the deleteSSHPublicKey function
182+
FunctionDeleteSSHPublicKey FunctionName = "deleteSSHPublicKey"
171183
// FunctionGetContentBlobUploadURL is the name fo the getContentBlobUploadUrl function
172184
FunctionGetContentBlobUploadURL FunctionName = "getContentBlobUploadUrl"
173185
// FunctionGetContentBlobDownloadURL is the name fo the getContentBlobDownloadUrl function
@@ -1117,6 +1129,50 @@ func (gp *APIoverJSONRPC) DeleteEnvVar(ctx context.Context, variable *UserEnvVar
11171129
return
11181130
}
11191131

1132+
// HasSSHPublicKey calls hasSSHPublicKey on the server
1133+
func (gp *APIoverJSONRPC) HasSSHPublicKey(ctx context.Context) (res bool, err error) {
1134+
if gp == nil {
1135+
err = errNotConnected
1136+
return
1137+
}
1138+
var _params []interface{}
1139+
err = gp.C.Call(ctx, "hasSSHPublicKey", _params, &res)
1140+
return
1141+
}
1142+
1143+
// GetSSHPublicKeys calls getSSHPublicKeys on the server
1144+
func (gp *APIoverJSONRPC) GetSSHPublicKeys(ctx context.Context) (res []*UserSSHPublicKeyValue, err error) {
1145+
if gp == nil {
1146+
err = errNotConnected
1147+
return
1148+
}
1149+
var _params []interface{}
1150+
err = gp.C.Call(ctx, "getSSHPublicKeys", _params, &res)
1151+
return
1152+
}
1153+
1154+
// AddSSHPublicKey calls addSSHPublicKey on the server
1155+
func (gp *APIoverJSONRPC) AddSSHPublicKey(ctx context.Context, value *SSHPublicKeyValue) (res *UserSSHPublicKeyValue, err error) {
1156+
if gp == nil {
1157+
err = errNotConnected
1158+
return
1159+
}
1160+
_params := []interface{}{value}
1161+
err = gp.C.Call(ctx, "addSSHPublicKey", _params, &res)
1162+
return
1163+
}
1164+
1165+
// DeleteSSHPublicKey calls deleteSSHPublicKey on the server
1166+
func (gp *APIoverJSONRPC) DeleteSSHPublicKey(ctx context.Context, id string) (err error) {
1167+
if gp == nil {
1168+
err = errNotConnected
1169+
return
1170+
}
1171+
_params := []interface{}{id}
1172+
err = gp.C.Call(ctx, "deleteSSHPublicKey", _params, nil)
1173+
return
1174+
}
1175+
11201176
// GetContentBlobUploadURL calls getContentBlobUploadUrl on the server
11211177
func (gp *APIoverJSONRPC) GetContentBlobUploadURL(ctx context.Context, name string) (url string, err error) {
11221178
if gp == nil {
@@ -1790,6 +1846,19 @@ type UserEnvVarValue struct {
17901846
Value string `json:"value,omitempty"`
17911847
}
17921848

1849+
type SSHPublicKeyValue struct {
1850+
Name string `json:"name,omitempty"`
1851+
Key string `json:"key,omitempty"`
1852+
}
1853+
1854+
type UserSSHPublicKeyValue struct {
1855+
ID string `json:"id,omitempty"`
1856+
Name string `json:"name,omitempty"`
1857+
Fingerprint string `json:"fingerprint,omitempty"`
1858+
CreationTime string `json:"creationTime,omitempty"`
1859+
LastUsedTime string `json:"lastUsedTime,omitempty"`
1860+
}
1861+
17931862
// GenerateNewGitpodTokenOptions is the GenerateNewGitpodTokenOptions message type
17941863
type GenerateNewGitpodTokenOptions struct {
17951864
Name string `json:"name,omitempty"`

components/gitpod-protocol/go/mock.go

Lines changed: 59 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: 8 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,
@@ -148,6 +150,12 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
148150
setEnvVar(variable: UserEnvVarValue): Promise<void>;
149151
deleteEnvVar(variable: UserEnvVarValue): Promise<void>;
150152

153+
// User SSH Keys
154+
hasSSHPublicKey(): Promise<boolean>;
155+
getSSHPublicKeys(): Promise<UserSSHPublicKeyValue[]>;
156+
addSSHPublicKey(value: SSHPublicKeyValue): Promise<UserSSHPublicKeyValue>;
157+
deleteSSHPublicKey(id: string): Promise<void>;
158+
151159
// Teams
152160
getTeams(): Promise<Team[]>;
153161
getTeamMembers(teamId: string): Promise<TeamMemberInfo[]>;

0 commit comments

Comments
 (0)