Skip to content

Commit b95a468

Browse files
authored
[fga] Introduce EnvVarService (#18503)
1 parent 7d27150 commit b95a468

18 files changed

+964
-690
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ export interface ProjectDB extends TransactionalDB<ProjectDB> {
2323
storeProject(project: Project): Promise<Project>;
2424
updateProject(partialProject: PartialProject): Promise<void>;
2525
markDeleted(projectId: string): Promise<void>;
26-
setProjectEnvironmentVariable(projectId: string, name: string, value: string, censored: boolean): Promise<void>;
26+
findProjectEnvironmentVariable(
27+
projectId: string,
28+
envVar: ProjectEnvVarWithValue,
29+
): Promise<ProjectEnvVar | undefined>;
30+
addProjectEnvironmentVariable(projectId: string, envVar: ProjectEnvVarWithValue): Promise<void>;
31+
updateProjectEnvironmentVariable(projectId: string, envVar: Required<ProjectEnvVarWithValue>): Promise<void>;
2732
getProjectEnvironmentVariables(projectId: string): Promise<ProjectEnvVar[]>;
2833
getProjectEnvironmentVariableById(variableId: string): Promise<ProjectEnvVar | undefined>;
2934
deleteProjectEnvironmentVariable(variableId: string): Promise<void>;

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

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { DBProjectUsage } from "./entity/db-project-usage";
1717
import { TransactionalDBImpl } from "./transactional-db-impl";
1818
import { TypeORM } from "./typeorm";
1919

20-
function toProjectEnvVar(envVarWithValue: ProjectEnvVarWithValue): ProjectEnvVar {
20+
function toProjectEnvVar(envVarWithValue: DBProjectEnvVar): ProjectEnvVar {
2121
const envVar = { ...envVarWithValue };
2222
delete (envVar as any)["value"];
2323
return envVar;
@@ -160,40 +160,35 @@ export class ProjectDBImpl extends TransactionalDBImpl<ProjectDB> implements Pro
160160
}
161161
}
162162

163-
public async setProjectEnvironmentVariable(
163+
public async findProjectEnvironmentVariable(
164164
projectId: string,
165-
name: string,
166-
value: string,
167-
censored: boolean,
168-
): Promise<void> {
169-
if (!name) {
170-
throw new Error("Variable name cannot be empty");
171-
}
172-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
173-
throw new Error(
174-
"Please choose a variable name containing only letters, numbers, or _, and which doesn't start with a number",
175-
);
176-
}
165+
envVar: ProjectEnvVarWithValue,
166+
): Promise<ProjectEnvVar | undefined> {
167+
const envVarRepo = await this.getProjectEnvVarRepo();
168+
return envVarRepo.findOne({ projectId, name: envVar.name, deleted: false });
169+
}
170+
171+
public async addProjectEnvironmentVariable(projectId: string, envVar: ProjectEnvVarWithValue): Promise<void> {
177172
const envVarRepo = await this.getProjectEnvVarRepo();
178-
const envVarWithValue = await envVarRepo.findOne({ projectId, name, deleted: false });
179-
if (envVarWithValue) {
180-
await envVarRepo.update(
181-
{ id: envVarWithValue.id, projectId: envVarWithValue.projectId },
182-
{ value, censored },
183-
);
184-
return;
185-
}
186173
await envVarRepo.save({
187174
id: uuidv4(),
188175
projectId,
189-
name,
190-
value,
191-
censored,
176+
name: envVar.name,
177+
value: envVar.value,
178+
censored: envVar.censored,
192179
creationTime: new Date().toISOString(),
193180
deleted: false,
194181
});
195182
}
196183

184+
public async updateProjectEnvironmentVariable(
185+
projectId: string,
186+
envVar: Required<ProjectEnvVarWithValue>,
187+
): Promise<void> {
188+
const envVarRepo = await this.getProjectEnvVarRepo();
189+
await envVarRepo.update({ id: envVar.id, projectId }, { value: envVar.value, censored: envVar.censored });
190+
}
191+
197192
public async getProjectEnvironmentVariables(projectId: string): Promise<ProjectEnvVar[]> {
198193
const envVarRepo = await this.getProjectEnvVarRepo();
199194
const envVarsWithValue = await envVarRepo.find({ projectId, deleted: false });

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

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
TokenEntry,
1616
User,
1717
UserEnvVar,
18+
UserEnvVarValue,
1819
UserSSHPublicKey,
1920
} from "@gitpod/gitpod-protocol";
2021
import { EncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
@@ -28,7 +29,7 @@ import {
2829
OAuthUser,
2930
} from "@jmondi/oauth2-server";
3031
import { inject, injectable, optional } from "inversify";
31-
import { EntityManager, Repository } from "typeorm";
32+
import { EntityManager, Equal, Not, Repository } from "typeorm";
3233
import { v4 as uuidv4 } from "uuid";
3334
import {
3435
BUILTIN_WORKSPACE_PROBE_USER_ID,
@@ -396,9 +397,38 @@ export class TypeORMUserDBImpl extends TransactionalDBImpl<UserDB> implements Us
396397
return Number.parseInt(count);
397398
}
398399

399-
public async setEnvVar(envVar: UserEnvVar): Promise<void> {
400+
public async findEnvVar(userId: string, envVar: UserEnvVarValue): Promise<UserEnvVar | undefined> {
400401
const repo = await this.getUserEnvVarRepo();
401-
await repo.save(envVar);
402+
return repo.findOne({
403+
where: {
404+
userId,
405+
name: envVar.name,
406+
repositoryPattern: envVar.repositoryPattern,
407+
deleted: Not(Equal(true)),
408+
},
409+
});
410+
}
411+
412+
public async addEnvVar(userId: string, envVar: UserEnvVarValue): Promise<void> {
413+
const repo = await this.getUserEnvVarRepo();
414+
await repo.save({
415+
id: uuidv4(),
416+
userId,
417+
name: envVar.name,
418+
repositoryPattern: envVar.repositoryPattern,
419+
value: envVar.value,
420+
});
421+
}
422+
423+
public async updateEnvVar(userId: string, envVar: Required<UserEnvVarValue>): Promise<void> {
424+
const repo = await this.getUserEnvVarRepo();
425+
await repo.update(
426+
{
427+
id: envVar.id,
428+
userId: userId,
429+
},
430+
{ name: envVar.name, repositoryPattern: envVar.repositoryPattern, value: envVar.value },
431+
);
402432
}
403433

404434
public async getEnvVars(userId: string): Promise<UserEnvVar[]> {
@@ -408,9 +438,8 @@ export class TypeORMUserDBImpl extends TransactionalDBImpl<UserDB> implements Us
408438
}
409439

410440
public async deleteEnvVar(envVar: UserEnvVar): Promise<void> {
411-
envVar.deleted = true;
412441
const repo = await this.getUserEnvVarRepo();
413-
await repo.save(envVar);
442+
await repo.update({ userId: envVar.userId, id: envVar.id }, { deleted: true });
414443
}
415444

416445
public async hasSSHPublicKey(userId: string): Promise<boolean> {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
TokenEntry,
1515
User,
1616
UserEnvVar,
17+
UserEnvVarValue,
1718
UserSSHPublicKey,
1819
} from "@gitpod/gitpod-protocol";
1920
import { OAuthTokenRepository, OAuthUserRepository } from "@jmondi/oauth2-server";
@@ -112,7 +113,9 @@ export interface UserDB extends OAuthUserRepository, OAuthTokenRepository, Trans
112113
*/
113114
findUsersByEmail(email: string): Promise<User[]>;
114115

115-
setEnvVar(envVar: UserEnvVar): Promise<void>;
116+
findEnvVar(userId: string, envVar: UserEnvVarValue): Promise<UserEnvVar | undefined>;
117+
addEnvVar(userId: string, envVar: UserEnvVarValue): Promise<void>;
118+
updateEnvVar(userId: string, envVar: UserEnvVarValue): Promise<void>;
116119
deleteEnvVar(envVar: UserEnvVar): Promise<void>;
117120
getEnvVars(userId: string): Promise<UserEnvVar[]>;
118121

components/gitpod-protocol/src/protocol.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,14 @@ export interface EnvVarWithValue {
396396
}
397397

398398
export interface ProjectEnvVarWithValue extends EnvVarWithValue {
399-
id: string;
400-
projectId: string;
399+
id?: string;
401400
censored: boolean;
402401
}
403402

404-
export type ProjectEnvVar = Omit<ProjectEnvVarWithValue, "value">;
403+
export interface ProjectEnvVar extends Omit<ProjectEnvVarWithValue, "value"> {
404+
id: string;
405+
projectId: string;
406+
}
405407

406408
export interface UserEnvVarValue extends EnvVarWithValue {
407409
id?: string;

components/server/src/authorization/definitions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ export type UserPermission =
4040
| "read_ssh"
4141
| "write_ssh"
4242
| "read_tokens"
43-
| "write_tokens";
43+
| "write_tokens"
44+
| "read_env_var"
45+
| "write_env_var";
4446

4547
export type InstallationResourceType = "installation";
4648

@@ -75,7 +77,7 @@ export type ProjectResourceType = "project";
7577

7678
export type ProjectRelation = "org" | "editor" | "viewer";
7779

78-
export type ProjectPermission = "read_info" | "write_info" | "delete";
80+
export type ProjectPermission = "read_info" | "write_info" | "delete" | "read_env_var" | "write_env_var";
7981

8082
export type WorkspaceResourceType = "workspace";
8183

components/server/src/container-module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ import { WebsocketConnectionManager } from "./websocket/websocket-connection-man
104104
import { ConfigProvider } from "./workspace/config-provider";
105105
import { IContextParser, IPrefixContextParser } from "./workspace/context-parser";
106106
import { ContextParser } from "./workspace/context-parser-service";
107-
import { EnvVarService } from "./workspace/env-var-service";
108107
import { EnvvarPrefixParser } from "./workspace/envvar-prefix-context-parser";
109108
import { GitTokenScopeGuesser } from "./workspace/git-token-scope-guesser";
110109
import { GitTokenValidator } from "./workspace/git-token-validator";
@@ -131,6 +130,7 @@ import { RelationshipUpdater } from "./authorization/relationship-updater";
131130
import { WorkspaceService } from "./workspace/workspace-service";
132131
import { SSHKeyService } from "./user/sshkey-service";
133132
import { GitpodTokenService } from "./user/gitpod-token-service";
133+
import { EnvVarService } from "./user/env-var-service";
134134

135135
export const productionContainerModule = new ContainerModule(
136136
(bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => {
@@ -147,6 +147,7 @@ export const productionContainerModule = new ContainerModule(
147147

148148
bind(SSHKeyService).toSelf().inSingletonScope();
149149
bind(GitpodTokenService).toSelf().inSingletonScope();
150+
bind(EnvVarService).toSelf().inSingletonScope();
150151

151152
bind(TokenService).toSelf().inSingletonScope();
152153
bind(TokenProvider).toService(TokenService);
@@ -257,7 +258,6 @@ export const productionContainerModule = new ContainerModule(
257258

258259
bind(OrganizationService).toSelf().inSingletonScope();
259260
bind(ProjectsService).toSelf().inSingletonScope();
260-
bind(EnvVarService).toSelf().inSingletonScope();
261261

262262
bind(NewsletterSubscriptionController).toSelf().inSingletonScope();
263263

components/server/src/prebuilds/prebuild-manager.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ import { PrebuildRateLimiterConfig } from "../workspace/prebuild-rate-limiter";
3636
import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error";
3737
import { UserAuthentication } from "../user/user-authentication";
3838
import { EntitlementService, MayStartWorkspaceResult } from "../billing/entitlement-service";
39-
import { EnvVarService } from "../workspace/env-var-service";
4039
import { WorkspaceService } from "../workspace/workspace-service";
40+
import { EnvVarService } from "../user/env-var-service";
4141

4242
export class WorkspaceRunningError extends Error {
4343
constructor(msg: string, public instance: WorkspaceInstance) {
@@ -230,7 +230,12 @@ export class PrebuildManager {
230230
context.normalizedContextURL!,
231231
);
232232

233-
const envVarsPromise = this.envVarService.resolve(workspace);
233+
const envVarsPromise = this.envVarService.resolveEnvVariables(
234+
workspace.ownerId,
235+
workspace.projectId,
236+
workspace.type,
237+
workspace.context,
238+
);
234239

235240
const prebuild = await this.workspaceDB.trace({ span }).findPrebuildByWorkspaceID(workspace.id)!;
236241
if (!prebuild) {
@@ -334,7 +339,12 @@ export class PrebuildManager {
334339
if (!prebuild) {
335340
throw new Error("No prebuild found for workspace " + workspaceId);
336341
}
337-
const envVars = await this.envVarService.resolve(workspace);
342+
const envVars = await this.envVarService.resolveEnvVariables(
343+
workspace.ownerId,
344+
workspace.projectId,
345+
workspace.type,
346+
workspace.context,
347+
);
338348
await this.workspaceStarter.startWorkspace({ span }, workspace, user, project, envVars);
339349
return { prebuildId: prebuild.id, wsid: workspace.id, done: false };
340350
} catch (err) {

components/server/src/projects/projects-service.spec.db.ts

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -120,64 +120,6 @@ describe("ProjectsService", async () => {
120120
);
121121
});
122122

123-
it("should let owners create, delete and get project env vars", async () => {
124-
const ps = container.get(ProjectsService);
125-
const project = await createTestProject(ps, org, owner);
126-
await ps.setProjectEnvironmentVariable(owner.id, project.id, "FOO", "BAR", false);
127-
128-
const envVars = await ps.getProjectEnvironmentVariables(owner.id, project.id);
129-
expect(envVars[0].name).to.equal("FOO");
130-
131-
const envVarById = await ps.getProjectEnvironmentVariableById(owner.id, envVars[0].id);
132-
expect(envVarById?.name).to.equal("FOO");
133-
134-
await ps.deleteProjectEnvironmentVariable(owner.id, envVars[0].id);
135-
136-
await expectError(ErrorCodes.NOT_FOUND, () => ps.getProjectEnvironmentVariableById(owner.id, envVars[0].id));
137-
138-
const emptyEnvVars = await ps.getProjectEnvironmentVariables(owner.id, project.id);
139-
expect(emptyEnvVars.length).to.equal(0);
140-
});
141-
142-
it("should not let members create, delete but allow get project env vars", async () => {
143-
const ps = container.get(ProjectsService);
144-
const project = await createTestProject(ps, org, owner);
145-
await ps.setProjectEnvironmentVariable(owner.id, project.id, "FOO", "BAR", false);
146-
147-
const envVars = await ps.getProjectEnvironmentVariables(member.id, project.id);
148-
expect(envVars[0].name).to.equal("FOO");
149-
150-
const envVarById = await ps.getProjectEnvironmentVariableById(member.id, envVars[0].id);
151-
expect(envVarById?.name).to.equal("FOO");
152-
153-
await expectError(ErrorCodes.PERMISSION_DENIED, () =>
154-
ps.deleteProjectEnvironmentVariable(member.id, envVars[0].id),
155-
);
156-
157-
await expectError(ErrorCodes.PERMISSION_DENIED, () =>
158-
ps.setProjectEnvironmentVariable(member.id, project.id, "FOO", "BAR", false),
159-
);
160-
});
161-
162-
it("should not let strangers create, delete and get project env vars", async () => {
163-
const ps = container.get(ProjectsService);
164-
const project = await createTestProject(ps, org, owner);
165-
166-
await ps.setProjectEnvironmentVariable(owner.id, project.id, "FOO", "BAR", false);
167-
168-
const envVars = await ps.getProjectEnvironmentVariables(owner.id, project.id);
169-
expect(envVars[0].name).to.equal("FOO");
170-
171-
// let's try to get the env var as a stranger
172-
await expectError(ErrorCodes.NOT_FOUND, () => ps.getProjectEnvironmentVariableById(stranger.id, envVars[0].id));
173-
174-
// let's try to delete the env var as a stranger
175-
await expectError(ErrorCodes.NOT_FOUND, () => ps.deleteProjectEnvironmentVariable(stranger.id, envVars[0].id));
176-
177-
// let's try to get the env vars as a stranger
178-
await expectError(ErrorCodes.NOT_FOUND, () => ps.getProjectEnvironmentVariables(stranger.id, project.id));
179-
});
180-
181123
it("should findProjects", async () => {
182124
const ps = container.get(ProjectsService);
183125
const project = await createTestProject(ps, org, owner);

components/server/src/projects/projects-service.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
CreateProjectParams,
1313
FindPrebuildsParams,
1414
Project,
15-
ProjectEnvVar,
1615
User,
1716
PrebuildEvent,
1817
} from "@gitpod/gitpod-protocol";
@@ -386,41 +385,6 @@ export class ProjectsService {
386385
return this.projectDB.updateProject(partialProject);
387386
}
388387

389-
async setProjectEnvironmentVariable(
390-
userId: string,
391-
projectId: string,
392-
name: string,
393-
value: string,
394-
censored: boolean,
395-
): Promise<void> {
396-
await this.auth.checkPermissionOnProject(userId, "write_info", projectId);
397-
return this.projectDB.setProjectEnvironmentVariable(projectId, name, value, censored);
398-
}
399-
400-
async getProjectEnvironmentVariables(userId: string, projectId: string): Promise<ProjectEnvVar[]> {
401-
await this.auth.checkPermissionOnProject(userId, "read_info", projectId);
402-
return this.projectDB.getProjectEnvironmentVariables(projectId);
403-
}
404-
405-
async getProjectEnvironmentVariableById(userId: string, variableId: string): Promise<ProjectEnvVar> {
406-
const result = await this.projectDB.getProjectEnvironmentVariableById(variableId);
407-
if (!result) {
408-
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Environment Variable ${variableId} not found.`);
409-
}
410-
try {
411-
await this.auth.checkPermissionOnProject(userId, "read_info", result.projectId);
412-
} catch (err) {
413-
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Environment Variable ${variableId} not found.`);
414-
}
415-
return result;
416-
}
417-
418-
async deleteProjectEnvironmentVariable(userId: string, variableId: string): Promise<void> {
419-
const variable = await this.getProjectEnvironmentVariableById(userId, variableId);
420-
await this.auth.checkPermissionOnProject(userId, "write_info", variable.projectId);
421-
return this.projectDB.deleteProjectEnvironmentVariable(variableId);
422-
}
423-
424388
async isProjectConsideredInactive(userId: string, projectId: string): Promise<boolean> {
425389
await this.auth.checkPermissionOnProject(userId, "read_info", projectId);
426390
const usage = await this.projectDB.getProjectUsage(projectId);

0 commit comments

Comments
 (0)