Skip to content

Commit b11852f

Browse files
committed
Add protocol and implement for ssh public keys
1 parent 12b58f4 commit b11852f

File tree

12 files changed

+294
-0
lines changed

12 files changed

+294
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
12+
@Entity("d_b_user_ssh_public_key")
13+
export class DBUserSshPublicKey implements UserSSHPublicKey {
14+
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
15+
id: string;
16+
17+
@Column(TypeORM.UUID_COLUMN_TYPE)
18+
@Index("ind_userId")
19+
userId: string;
20+
21+
@Column("varchar")
22+
name: string;
23+
24+
@Column("varchar")
25+
key: string;
26+
27+
@Column("varchar")
28+
fingerprint: string;
29+
30+
@Column({
31+
type: "timestamp",
32+
precision: 6,
33+
default: () => "CURRENT_TIMESTAMP(6)",
34+
transformer: Transformer.MAP_ISO_STRING_TO_TIMESTAMP_DROP,
35+
})
36+
@Index("ind_creationTime")
37+
creationTime: string;
38+
39+
@Column({
40+
type: "timestamp",
41+
precision: 6,
42+
transformer: Transformer.MAP_ISO_STRING_TO_TIMESTAMP_DROP,
43+
})
44+
lastUsedTime?: string;
45+
}
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, `creationTime` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `lastUsedTime` timestamp(6)), 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: 28 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,27 @@ 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 }, order: { creationTime: "DESC" } });
408+
}
409+
410+
public async addSSHPublicKey(userId: string, value: SSHPublicKeyValue): Promise<UserSSHPublicKey> {
411+
const repo = await this.getSSHPublicKeyRepo();
412+
return repo.save({
413+
userId,
414+
name: value.name,
415+
key: value.key,
416+
fingerprint: SSHPublicKeyValue.getFingerprint(value),
417+
creationTime: new Date().toISOString(),
418+
});
419+
}
420+
421+
public async deleteSSHPublicKey(userId: string, id: string): Promise<void> {
422+
const repo = await this.getSSHPublicKeyRepo();
423+
await repo.delete({ userId, id });
424+
}
425+
398426
public async findAllUsers(
399427
offset: number,
400428
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[]>;

components/gitpod-protocol/src/protocol.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,48 @@ export namespace UserEnvVar {
356356
}
357357
}
358358

359+
export interface SSHPublicKeyValue {
360+
name: string;
361+
key: string;
362+
}
363+
export interface UserSSHPublicKey extends SSHPublicKeyValue {
364+
id: string;
365+
key: string;
366+
userId: string;
367+
fingerprint: string;
368+
creationTime: string;
369+
lastUsedTime?: string;
370+
}
371+
372+
export type UserSSHPublicKeyValue = Omit<UserSSHPublicKey, "key" | "userId">;
373+
374+
export namespace SSHPublicKeyValue {
375+
export function validate(value: SSHPublicKeyValue) {
376+
// Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', '[email protected]', or '[email protected]'.
377+
const regex =
378+
/^(?<type>ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519|sk-ecdsa-sha2-nistp256@openssh\.com|sk-ssh-ed25519@openssh\.com) (?<key>.*?)( (?<email>.*?))?$/;
379+
const resultGroup = regex.exec(value.key.trim());
380+
if (!resultGroup) {
381+
throw new Error("SSH public key is not available");
382+
}
383+
return {
384+
type: resultGroup.groups?.["type"] as string,
385+
key: resultGroup.groups?.["key"] as string,
386+
email: resultGroup.groups?.["email"] || undefined,
387+
};
388+
}
389+
390+
export function getFingerprint(value: SSHPublicKeyValue) {
391+
const data = SSHPublicKeyValue.validate(value);
392+
let buf = Buffer.from(data.key, "base64");
393+
// gitlab style
394+
const hash = createHash("md5").update(buf).digest("hex");
395+
// github style
396+
// const hash = createHash('sha256').update(buf).digest('base64');
397+
return hash;
398+
}
399+
}
400+
359401
export interface GitpodToken {
360402
/** Hash value (SHA256) of the token (primary key). */
361403
tokenHash: string;

components/public-api-server/pkg/apiv1/workspace_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,18 @@ func (f *FakeGitpodAPI) DeleteEnvVar(ctx context.Context, variable *gitpod.UserE
370370
panic("implement me")
371371
}
372372

373+
func (f *FakeGitpodAPI) GetSSHPublicKeys(ctx context.Context) (res []*gitpod.UserSSHPublicKeyValue, err error) {
374+
panic("implement me")
375+
}
376+
377+
func (f *FakeGitpodAPI) AddSSHPublicKey(ctx context.Context, value *gitpod.SSHPublicKeyValue) (res *gitpod.UserSSHPublicKeyValue, err error) {
378+
panic("implement me")
379+
}
380+
381+
func (f *FakeGitpodAPI) DeleteSSHPublicKey(ctx context.Context, id string) (err error) {
382+
panic("implement me")
383+
}
384+
373385
func (f *FakeGitpodAPI) GetContentBlobUploadURL(ctx context.Context, name string) (url string, err error) {
374386
panic("implement me")
375387
}

components/server/src/auth/rate-limiter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
9292
getAllEnvVars: { group: "default", points: 1 },
9393
setEnvVar: { group: "default", points: 1 },
9494
deleteEnvVar: { group: "default", points: 1 },
95+
getSSHPublicKeys: { group: "default", points: 1 },
96+
addSSHPublicKey: { group: "default", points: 1 },
97+
deleteSSHPublicKey: { group: "default", points: 1 },
9598
setProjectEnvironmentVariable: { group: "default", points: 1 },
9699
getProjectEnvironmentVariables: { group: "default", points: 1 },
97100
deleteProjectEnvironmentVariable: { group: "default", points: 1 },

0 commit comments

Comments
 (0)