Skip to content

Commit 665cacc

Browse files
committed
[db, protocol] Introduce DBOrgEnvVar
1 parent 76781bf commit 665cacc

File tree

6 files changed

+177
-6
lines changed

6 files changed

+177
-6
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
TeamMemberRole,
1111
TeamMembershipInvite,
1212
OrganizationSettings,
13+
OrgEnvVar,
14+
OrgEnvVarWithValue,
1315
} from "@gitpod/gitpod-protocol";
1416
import { DBTeamMembership } from "./typeorm/entity/db-team-membership";
1517
import { TransactionalDB } from "./typeorm/transactional-db-impl";
@@ -43,4 +45,12 @@ export interface TeamDB extends TransactionalDB<TeamDB> {
4345
setOrgSettings(teamId: string, settings: Partial<OrganizationSettings>): Promise<OrganizationSettings>;
4446

4547
hasActiveSSO(organizationId: string): Promise<boolean>;
48+
49+
addOrgEnvironmentVariable(orgId: string, envVar: OrgEnvVarWithValue): Promise<OrgEnvVar>;
50+
updateOrgEnvironmentVariable(orgId: string, envVar: Partial<OrgEnvVarWithValue>): Promise<OrgEnvVar | undefined>;
51+
getOrgEnvironmentVariableById(id: string): Promise<OrgEnvVar | undefined>;
52+
findOrgEnvironmentVariableByName(orgId: string, name: string): Promise<OrgEnvVar | undefined>;
53+
getOrgEnvironmentVariables(orgId: string): Promise<OrgEnvVar[]>;
54+
getOrgEnvironmentVariableValues(envVars: OrgEnvVar[]): Promise<OrgEnvVarWithValue[]>;
55+
deleteOrgEnvironmentVariable(id: string): Promise<void>;
4656
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright (c) 2025 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, Entity, Column } from "typeorm";
8+
import { TypeORM } from "../typeorm";
9+
import { OrgEnvVarWithValue } from "@gitpod/gitpod-protocol";
10+
import { Transformer } from "../transformer";
11+
import { getGlobalEncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
12+
13+
@Entity()
14+
// on DB but not Typeorm: @Index("ind_lastModified", ["_lastModified"]) // DBSync
15+
export class DBOrgEnvVar implements OrgEnvVarWithValue {
16+
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
17+
id: string;
18+
19+
// `orgId` is part of the primary key for safety reasons: This way it's impossible that a user
20+
// (maliciously or by accident) sends us an environment variable that has the same private key (`id`)
21+
// as the environment variable from another project.
22+
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
23+
orgId: string;
24+
25+
@Column()
26+
name: string;
27+
28+
@Column({
29+
type: "simple-json",
30+
transformer: Transformer.compose(
31+
Transformer.SIMPLE_JSON([]),
32+
Transformer.encrypted(getGlobalEncryptionService),
33+
),
34+
})
35+
value: string;
36+
37+
@Column({
38+
type: "varchar",
39+
length: 36,
40+
})
41+
creationTime: string;
42+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) 2025 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 OrgEnvVars1737449780009 implements MigrationInterface {
10+
public async up(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(
12+
"CREATE TABLE IF NOT EXISTS `d_b_org_env_var` (`id` char(36) NOT NULL, `orgId` char(36) NOT NULL, `name` varchar(255) NOT NULL, `value` text NOT NULL, `creationTime` varchar(36) NOT NULL, `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`, `orgId`), KEY `ind_orgid` (orgId), KEY `ind_dbsync` (`_lastModified`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
13+
);
14+
}
15+
16+
public async down(queryRunner: QueryRunner): Promise<void> {}
17+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,11 @@ export class ProjectDBImpl extends TransactionalDBImpl<ProjectDB> implements Pro
222222
return envVars;
223223
}
224224

225-
public async getProjectEnvironmentVariableById(variableId: string): Promise<ProjectEnvVar | undefined> {
225+
public async getProjectEnvironmentVariableById(id: string): Promise<ProjectEnvVar | undefined> {
226226
const envVarRepo = await this.getProjectEnvVarRepo();
227-
const envVarWithValue = await envVarRepo.findOne({ id: variableId, deleted: false });
227+
const envVarWithValue = await envVarRepo.findOne({ id, deleted: false });
228228
if (!envVarWithValue) {
229-
return;
229+
return undefined;
230230
}
231231
const envVar = toProjectEnvVar(envVarWithValue);
232232
return envVar;

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

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import {
88
OrganizationSettings,
9+
OrgEnvVar,
10+
OrgEnvVarWithValue,
911
Team,
1012
TeamMemberInfo,
1113
TeamMemberRole,
@@ -25,21 +27,32 @@ import { DBOrgSettings } from "./entity/db-team-settings";
2527
import { DBUser } from "./entity/db-user";
2628
import { TransactionalDBImpl } from "./transactional-db-impl";
2729
import { TypeORM } from "./typeorm";
30+
import { EncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
31+
import { DBOrgEnvVar } from "./entity/db-org-env-var";
32+
import { filter } from "../utils";
2833

2934
@injectable()
3035
export class TeamDBImpl extends TransactionalDBImpl<TeamDB> implements TeamDB {
31-
constructor(@inject(TypeORM) typeorm: TypeORM, @optional() transactionalEM?: EntityManager) {
36+
constructor(
37+
@inject(TypeORM) typeorm: TypeORM,
38+
@inject(EncryptionService) private readonly encryptionService: EncryptionService,
39+
@optional() transactionalEM?: EntityManager,
40+
) {
3241
super(typeorm, transactionalEM);
3342
}
3443

3544
protected createTransactionalDB(transactionalEM: EntityManager): TeamDB {
36-
return new TeamDBImpl(this.typeorm, transactionalEM);
45+
return new TeamDBImpl(this.typeorm, this.encryptionService, transactionalEM);
3746
}
3847

3948
private async getTeamRepo(): Promise<Repository<DBTeam>> {
4049
return (await this.getEntityManager()).getRepository<DBTeam>(DBTeam);
4150
}
4251

52+
private async getOrgEnvVarRepo(): Promise<Repository<DBOrgEnvVar>> {
53+
return (await this.getEntityManager()).getRepository<DBOrgEnvVar>(DBOrgEnvVar);
54+
}
55+
4356
private async getMembershipRepo(): Promise<Repository<DBTeamMembership>> {
4457
return (await this.getEntityManager()).getRepository<DBTeamMembership>(DBTeamMembership);
4558
}
@@ -408,4 +421,83 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> implements TeamDB {
408421
);
409422
return result.length === 1;
410423
}
424+
425+
public async addOrgEnvironmentVariable(orgId: string, envVar: OrgEnvVarWithValue): Promise<OrgEnvVar> {
426+
const envVarRepo = await this.getOrgEnvVarRepo();
427+
const insertedEnvVar = await envVarRepo.save({
428+
id: uuidv4(),
429+
orgId,
430+
name: envVar.name,
431+
value: envVar.value,
432+
creationTime: new Date().toISOString(),
433+
});
434+
return toOrgEnvVar(insertedEnvVar);
435+
}
436+
437+
public async updateOrgEnvironmentVariable(
438+
orgId: string,
439+
envVar: Partial<OrgEnvVarWithValue>,
440+
): Promise<OrgEnvVar | undefined> {
441+
if (!envVar.id) {
442+
throw new ApplicationError(ErrorCodes.NOT_FOUND, "An environment variable with this ID could not be found");
443+
}
444+
445+
return await this.transaction(async (_, ctx) => {
446+
const envVarRepo = ctx.entityManager.getRepository<DBOrgEnvVar>(DBOrgEnvVar);
447+
448+
await envVarRepo.update(
449+
{ id: envVar.id, orgId },
450+
filter(envVar, (_, v) => v !== null && v !== undefined),
451+
);
452+
453+
const found = await envVarRepo.findOne({ id: envVar.id, orgId });
454+
if (!found) {
455+
return;
456+
}
457+
return toOrgEnvVar(found);
458+
});
459+
}
460+
461+
public async getOrgEnvironmentVariableById(id: string): Promise<OrgEnvVar | undefined> {
462+
const envVarRepo = await this.getOrgEnvVarRepo();
463+
const envVarWithValue = await envVarRepo.findOne({ id });
464+
if (!envVarWithValue) {
465+
return undefined;
466+
}
467+
const envVar = toOrgEnvVar(envVarWithValue);
468+
return envVar;
469+
}
470+
471+
public async findOrgEnvironmentVariableByName(orgId: string, name: string): Promise<OrgEnvVar | undefined> {
472+
const envVarRepo = await this.getOrgEnvVarRepo();
473+
return envVarRepo.findOne({ orgId, name });
474+
}
475+
476+
public async getOrgEnvironmentVariables(orgId: string): Promise<OrgEnvVar[]> {
477+
const envVarRepo = await this.getOrgEnvVarRepo();
478+
const envVarsWithValue = await envVarRepo.find({ orgId });
479+
const envVars = envVarsWithValue.map(toOrgEnvVar);
480+
return envVars;
481+
}
482+
483+
public async getOrgEnvironmentVariableValues(envVars: OrgEnvVar[]): Promise<OrgEnvVarWithValue[]> {
484+
const envVarRepo = await this.getOrgEnvVarRepo();
485+
const envVarsWithValues = await envVarRepo.findByIds(envVars);
486+
return envVarsWithValues;
487+
}
488+
489+
public async deleteOrgEnvironmentVariable(id: string): Promise<void> {
490+
const envVarRepo = await this.getOrgEnvVarRepo();
491+
await envVarRepo.delete({ id });
492+
}
493+
}
494+
495+
/**
496+
* @param envVarWithValue
497+
* @returns DBOrgEnvVar shape turned into an OrgEnvVar by dropping the "value" property
498+
*/
499+
function toOrgEnvVar(envVarWithValue: DBOrgEnvVar): OrgEnvVar {
500+
const envVar = { ...envVarWithValue };
501+
delete (envVar as any)["value"];
502+
return envVar;
411503
}

components/gitpod-protocol/src/protocol.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export namespace NamedWorkspaceFeatureFlag {
233233
}
234234
}
235235

236-
export type EnvVar = UserEnvVar | ProjectEnvVarWithValue | EnvVarWithValue;
236+
export type EnvVar = UserEnvVar | ProjectEnvVarWithValue | OrgEnvVarWithValue | EnvVarWithValue;
237237

238238
export interface EnvVarWithValue {
239239
name: string;
@@ -242,6 +242,7 @@ export interface EnvVarWithValue {
242242

243243
export interface ProjectEnvVarWithValue extends EnvVarWithValue {
244244
id?: string;
245+
/** If a project-scoped env var is "censored", it is only visible in Prebuilds */
245246
censored: boolean;
246247
}
247248

@@ -250,6 +251,15 @@ export interface ProjectEnvVar extends Omit<ProjectEnvVarWithValue, "value"> {
250251
projectId: string;
251252
}
252253

254+
export interface OrgEnvVarWithValue extends EnvVarWithValue {
255+
id?: string;
256+
}
257+
258+
export interface OrgEnvVar extends Omit<OrgEnvVarWithValue, "value"> {
259+
id: string;
260+
orgId: string;
261+
}
262+
253263
export interface UserEnvVarValue extends EnvVarWithValue {
254264
id?: string;
255265
repositoryPattern: string; // DEPRECATED: Use ProjectEnvVar instead of repositoryPattern - https://github.com/gitpod-com/gitpod/issues/5322

0 commit comments

Comments
 (0)