Skip to content

Commit 3556cf2

Browse files
committed
[fga] Introduce EnvVarService
1 parent 1829dbe commit 3556cf2

File tree

4 files changed

+224
-85
lines changed

4 files changed

+224
-85
lines changed

components/server/src/container-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ import { UserService } from "./user/user-service";
130130
import { RelationshipUpdater } from "./authorization/relationship-updater";
131131
import { WorkspaceService } from "./workspace/workspace-service";
132132
import { SSHKeyService } from "./user/sshkey-service";
133+
import { EnvVarService } from "./user/env-var-service";
133134

134135
export const productionContainerModule = new ContainerModule(
135136
(bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => {
@@ -145,6 +146,7 @@ export const productionContainerModule = new ContainerModule(
145146
bind(AuthorizationService).to(AuthorizationServiceImpl).inSingletonScope();
146147

147148
bind(SSHKeyService).toSelf().inSingletonScope();
149+
bind(EnvVarService).toSelf().inSingletonScope();
148150

149151
bind(TokenService).toSelf().inSingletonScope();
150152
bind(TokenProvider).toService(TokenService);
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright (c) 2023 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 { BUILTIN_INSTLLATION_ADMIN_USER_ID, TypeORM } from "@gitpod/gitpod-db/lib";
8+
import { Organization, User } from "@gitpod/gitpod-protocol";
9+
import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
10+
import * as chai from "chai";
11+
import { Container } from "inversify";
12+
import "mocha";
13+
import { createTestContainer } from "../test/service-testing-container-module";
14+
import { resetDB } from "@gitpod/gitpod-db/lib/test/reset-db";
15+
import { OrganizationService } from "../orgs/organization-service";
16+
import { UserService } from "./user-service";
17+
import { expectError } from "../test/expect-utils";
18+
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
19+
import { EnvVarService } from "./env-var-service";
20+
21+
const expect = chai.expect;
22+
23+
describe("EnvVarService", async () => {
24+
let container: Container;
25+
let es: EnvVarService;
26+
27+
let member: User;
28+
let stranger: User;
29+
let org: Organization;
30+
31+
beforeEach(async () => {
32+
container = createTestContainer();
33+
Experiments.configureTestingClient({
34+
centralizedPermissions: true,
35+
});
36+
37+
const orgService = container.get<OrganizationService>(OrganizationService);
38+
org = await orgService.createOrganization(BUILTIN_INSTLLATION_ADMIN_USER_ID, "myOrg");
39+
const invite = await orgService.getOrCreateInvite(BUILTIN_INSTLLATION_ADMIN_USER_ID, org.id);
40+
41+
const userService = container.get<UserService>(UserService);
42+
member = await userService.createUser({
43+
organizationId: org.id,
44+
identity: {
45+
authId: "foo",
46+
authName: "bar",
47+
authProviderId: "github",
48+
primaryEmail: "[email protected]",
49+
},
50+
});
51+
await orgService.joinOrganization(member.id, invite.id);
52+
stranger = await userService.createUser({
53+
identity: {
54+
authId: "foo2",
55+
authName: "bar2",
56+
authProviderId: "github",
57+
},
58+
});
59+
60+
es = container.get(EnvVarService);
61+
});
62+
63+
afterEach(async () => {
64+
// Clean-up database
65+
await resetDB(container.get(TypeORM));
66+
});
67+
68+
it("should add env variable", async () => {
69+
const resp1 = await es.getAllEnvVars(member.id, member.id);
70+
expect(resp1.length).to.equal(0);
71+
72+
await es.setEnvVar(member.id, member.id, { name: "var1", value: "foo", repositoryPattern: "*/*" });
73+
74+
const resp2 = await es.getAllEnvVars(member.id, member.id);
75+
expect(resp2.length).to.equal(1);
76+
77+
await expectError(ErrorCodes.NOT_FOUND, es.getAllEnvVars(stranger.id, member.id));
78+
await expectError(
79+
ErrorCodes.NOT_FOUND,
80+
es.setEnvVar(stranger.id, member.id, { name: "var2", value: "bar", repositoryPattern: "*/*" }),
81+
);
82+
});
83+
84+
it("should list all env vars", async () => {
85+
await es.setEnvVar(member.id, member.id, { name: "var1", value: "foo", repositoryPattern: "*/*" });
86+
await es.setEnvVar(member.id, member.id, { name: "var2", value: "bar", repositoryPattern: "*/*" });
87+
88+
const envVars = await es.getAllEnvVars(member.id, member.id);
89+
expect(envVars.length).to.equal(2);
90+
expect(envVars.some((e) => e.name === "var1" && e.value === "foo")).to.be.true;
91+
expect(envVars.some((e) => e.name === "var2" && e.value === "bar")).to.be.true;
92+
93+
await expectError(ErrorCodes.NOT_FOUND, es.getAllEnvVars(stranger.id, member.id));
94+
});
95+
96+
it("should delete env vars", async () => {
97+
await es.setEnvVar(member.id, member.id, { name: "var1", value: "foo", repositoryPattern: "*/*" });
98+
await es.setEnvVar(member.id, member.id, { name: "var2", value: "bar", repositoryPattern: "*/*" });
99+
100+
const envVars = await es.getAllEnvVars(member.id, member.id);
101+
expect(envVars.length).to.equal(2);
102+
103+
await es.deleteEnvVar(member.id, member.id, envVars[0]);
104+
105+
const envVars2 = await es.getAllEnvVars(member.id, member.id);
106+
expect(envVars2.length).to.equal(1);
107+
108+
await expectError(ErrorCodes.NOT_FOUND, es.deleteEnvVar(stranger.id, member.id, envVars2[0]));
109+
});
110+
});
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* Copyright (c) 2023 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 { UserDB } from "@gitpod/gitpod-db/lib";
8+
import { UserEnvVar, UserEnvVarValue } from "@gitpod/gitpod-protocol";
9+
import { inject, injectable } from "inversify";
10+
import { Authorizer } from "../authorization/authorizer";
11+
import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
12+
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
13+
import { v4 as uuidv4 } from "uuid";
14+
import { Config } from "../config";
15+
16+
@injectable()
17+
export class EnvVarService {
18+
constructor(
19+
@inject(Config) private readonly config: Config,
20+
@inject(UserDB) private readonly userDB: UserDB,
21+
@inject(Authorizer) private readonly auth: Authorizer,
22+
@inject(IAnalyticsWriter) private readonly analytics: IAnalyticsWriter,
23+
) {}
24+
25+
async getAllEnvVars(requestorId: string, userId: string): Promise<UserEnvVarValue[]> {
26+
await this.auth.checkPermissionOnUser(requestorId, "read_info", userId);
27+
const result: UserEnvVarValue[] = [];
28+
for (const value of await this.userDB.getEnvVars(userId)) {
29+
result.push({
30+
id: value.id,
31+
name: value.name,
32+
value: value.value,
33+
repositoryPattern: value.repositoryPattern,
34+
});
35+
}
36+
return result;
37+
}
38+
39+
async setEnvVar(requestorId: string, userId: string, variable: UserEnvVarValue): Promise<void> {
40+
await this.auth.checkPermissionOnUser(requestorId, "write_info", userId);
41+
const validationError = UserEnvVar.validate(variable);
42+
if (validationError) {
43+
throw new ApplicationError(ErrorCodes.BAD_REQUEST, validationError);
44+
}
45+
46+
variable.repositoryPattern = UserEnvVar.normalizeRepoPattern(variable.repositoryPattern);
47+
const existingVars = (await this.userDB.getEnvVars(userId)).filter((v) => !v.deleted);
48+
49+
const existingVar = existingVars.find(
50+
(v) => v.name == variable.name && v.repositoryPattern == variable.repositoryPattern,
51+
);
52+
if (!!existingVar) {
53+
// overwrite existing variable rather than introduce a duplicate
54+
variable.id = existingVar.id;
55+
}
56+
57+
if (!variable.id) {
58+
// this is a new variable - make sure the user does not have too many (don't DOS our database using gp env)
59+
const varCount = existingVars.length;
60+
if (varCount > this.config.maxEnvvarPerUserCount) {
61+
throw new ApplicationError(
62+
ErrorCodes.PERMISSION_DENIED,
63+
`cannot have more than ${this.config.maxEnvvarPerUserCount} environment variables`,
64+
);
65+
}
66+
}
67+
68+
const envvar: UserEnvVar = {
69+
id: variable.id || uuidv4(),
70+
name: variable.name,
71+
repositoryPattern: variable.repositoryPattern,
72+
value: variable.value,
73+
userId,
74+
};
75+
this.analytics.track({ event: "envvar-set", userId });
76+
77+
await this.userDB.setEnvVar(envvar);
78+
}
79+
80+
async deleteEnvVar(requestorId: string, userId: string, variable: UserEnvVarValue): Promise<void> {
81+
await this.auth.checkPermissionOnUser(requestorId, "write_info", userId);
82+
if (!variable.id && variable.name && variable.repositoryPattern) {
83+
variable.repositoryPattern = UserEnvVar.normalizeRepoPattern(variable.repositoryPattern);
84+
const existingVars = (await this.userDB.getEnvVars(userId)).filter((v) => !v.deleted);
85+
const existingVar = existingVars.find(
86+
(v) => v.name == variable.name && v.repositoryPattern == variable.repositoryPattern,
87+
);
88+
variable.id = existingVar?.id;
89+
}
90+
91+
if (!variable.id) {
92+
throw new ApplicationError(
93+
ErrorCodes.NOT_FOUND,
94+
`cannot delete '${variable.name}' in scope '${variable.repositoryPattern}'`,
95+
);
96+
}
97+
98+
const envvar: UserEnvVar = {
99+
...variable,
100+
id: variable.id!,
101+
userId,
102+
};
103+
this.analytics.track({ event: "envvar-deleted", userId });
104+
105+
await this.userDB.deleteEnvVar(envvar);
106+
}
107+
}

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 5 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import {
3636
StartWorkspaceResult,
3737
Token,
3838
User,
39-
UserEnvVar,
4039
UserEnvVarValue,
4140
UserInfo,
4241
WhitelistedRepository,
@@ -192,6 +191,7 @@ import { UsageService } from "../orgs/usage-service";
192191
import { UserService } from "../user/user-service";
193192
import { SSHKeyService } from "../user/sshkey-service";
194193
import { StartWorkspaceOptions, WorkspaceService } from "./workspace-service";
194+
import { EnvVarService } from "../user/env-var-service";
195195

196196
// shortcut
197197
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
@@ -234,6 +234,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
234234
@inject(IAnalyticsWriter) private readonly analytics: IAnalyticsWriter,
235235
@inject(AuthorizationService) private readonly authorizationService: AuthorizationService,
236236
@inject(SSHKeyService) private readonly sshKeyservice: SSHKeyService,
237+
@inject(EnvVarService) private readonly envVarService: EnvVarService,
237238

238239
@inject(TeamDB) private readonly teamDB: TeamDB,
239240
@inject(OrganizationService) private readonly organizationService: OrganizationService,
@@ -2278,104 +2279,23 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
22782279
// Get all environment variables (unfiltered)
22792280
async getAllEnvVars(ctx: TraceContext): Promise<UserEnvVarValue[]> {
22802281
const user = await this.checkUser("getAllEnvVars");
2281-
const result: UserEnvVarValue[] = [];
2282-
for (const value of await this.userDB.getEnvVars(user.id)) {
2283-
if (!(await this.resourceAccessGuard.canAccess({ kind: "envVar", subject: value }, "get"))) {
2284-
continue;
2285-
}
2286-
result.push({
2287-
id: value.id,
2288-
name: value.name,
2289-
value: value.value,
2290-
repositoryPattern: value.repositoryPattern,
2291-
});
2292-
}
2293-
return result;
2282+
return this.envVarService.getAllEnvVars(user.id, user.id);
22942283
}
22952284

22962285
async setEnvVar(ctx: TraceContext, variable: UserEnvVarValue): Promise<void> {
22972286
traceAPIParams(ctx, { variable: censor(variable, "value") }); // filter content because of PII
22982287

22992288
// Note: this operation is per-user only, hence needs no resource guard
23002289
const user = await this.checkAndBlockUser("setEnvVar");
2301-
const userId = user.id;
2302-
2303-
// validate input
2304-
const validationError = UserEnvVar.validate(variable);
2305-
if (validationError) {
2306-
throw new ApplicationError(ErrorCodes.BAD_REQUEST, validationError);
2307-
}
2308-
2309-
variable.repositoryPattern = UserEnvVar.normalizeRepoPattern(variable.repositoryPattern);
2310-
const existingVars = (await this.userDB.getEnvVars(user.id)).filter((v) => !v.deleted);
2311-
2312-
const existingVar = existingVars.find(
2313-
(v) => v.name == variable.name && v.repositoryPattern == variable.repositoryPattern,
2314-
);
2315-
if (!!existingVar) {
2316-
// overwrite existing variable rather than introduce a duplicate
2317-
variable.id = existingVar.id;
2318-
}
2319-
2320-
if (!variable.id) {
2321-
// this is a new variable - make sure the user does not have too many (don't DOS our database using gp env)
2322-
const varCount = existingVars.length;
2323-
if (varCount > this.config.maxEnvvarPerUserCount) {
2324-
throw new ApplicationError(
2325-
ErrorCodes.PERMISSION_DENIED,
2326-
`cannot have more than ${this.config.maxEnvvarPerUserCount} environment variables`,
2327-
);
2328-
}
2329-
}
2330-
2331-
const envvar: UserEnvVar = {
2332-
id: variable.id || uuidv4(),
2333-
name: variable.name,
2334-
repositoryPattern: variable.repositoryPattern,
2335-
value: variable.value,
2336-
userId,
2337-
};
2338-
await this.guardAccess(
2339-
{ kind: "envVar", subject: envvar },
2340-
typeof variable.id === "string" ? "update" : "create",
2341-
);
2342-
this.analytics.track({ event: "envvar-set", userId });
2343-
2344-
await this.userDB.setEnvVar(envvar);
2290+
return this.envVarService.setEnvVar(user.id, user.id, variable);
23452291
}
23462292

23472293
async deleteEnvVar(ctx: TraceContext, variable: UserEnvVarValue): Promise<void> {
23482294
traceAPIParams(ctx, { variable: censor(variable, "value") });
23492295

23502296
// Note: this operation is per-user only, hence needs no resource guard
23512297
const user = await this.checkAndBlockUser("deleteEnvVar");
2352-
const userId = user.id;
2353-
2354-
if (!variable.id && variable.name && variable.repositoryPattern) {
2355-
variable.repositoryPattern = UserEnvVar.normalizeRepoPattern(variable.repositoryPattern);
2356-
const existingVars = (await this.userDB.getEnvVars(user.id)).filter((v) => !v.deleted);
2357-
const existingVar = existingVars.find(
2358-
(v) => v.name == variable.name && v.repositoryPattern == variable.repositoryPattern,
2359-
);
2360-
variable.id = existingVar?.id;
2361-
}
2362-
2363-
if (!variable.id) {
2364-
throw new ApplicationError(
2365-
ErrorCodes.NOT_FOUND,
2366-
`cannot delete '${variable.name}' in scope '${variable.repositoryPattern}'`,
2367-
);
2368-
}
2369-
2370-
const envvar: UserEnvVar = {
2371-
...variable,
2372-
id: variable.id!,
2373-
userId,
2374-
};
2375-
await this.guardAccess({ kind: "envVar", subject: envvar }, "delete");
2376-
this.analytics.track({ event: "envvar-deleted", userId });
2377-
2378-
await this.userDB.deleteEnvVar(envvar);
2298+
return this.envVarService.deleteEnvVar(user.id, user.id, variable);
23792299
}
23802300

23812301
async hasSSHPublicKey(ctx: TraceContext): Promise<boolean> {

0 commit comments

Comments
 (0)