Skip to content

[fga] Introduce EnvVarService #18503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion components/gitpod-db/src/project-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ export interface ProjectDB extends TransactionalDB<ProjectDB> {
storeProject(project: Project): Promise<Project>;
updateProject(partialProject: PartialProject): Promise<void>;
markDeleted(projectId: string): Promise<void>;
setProjectEnvironmentVariable(projectId: string, name: string, value: string, censored: boolean): Promise<void>;
findProjectEnvironmentVariable(
projectId: string,
envVar: ProjectEnvVarWithValue,
): Promise<ProjectEnvVar | undefined>;
addProjectEnvironmentVariable(projectId: string, envVar: ProjectEnvVarWithValue): Promise<void>;
updateProjectEnvironmentVariable(projectId: string, envVar: Required<ProjectEnvVarWithValue>): Promise<void>;
getProjectEnvironmentVariables(projectId: string): Promise<ProjectEnvVar[]>;
getProjectEnvironmentVariableById(variableId: string): Promise<ProjectEnvVar | undefined>;
deleteProjectEnvironmentVariable(variableId: string): Promise<void>;
Expand Down
45 changes: 20 additions & 25 deletions components/gitpod-db/src/typeorm/project-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { DBProjectUsage } from "./entity/db-project-usage";
import { TransactionalDBImpl } from "./transactional-db-impl";
import { TypeORM } from "./typeorm";

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

public async setProjectEnvironmentVariable(
public async findProjectEnvironmentVariable(
projectId: string,
name: string,
value: string,
censored: boolean,
): Promise<void> {
if (!name) {
throw new Error("Variable name cannot be empty");
}
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
throw new Error(
"Please choose a variable name containing only letters, numbers, or _, and which doesn't start with a number",
);
}
envVar: ProjectEnvVarWithValue,
): Promise<ProjectEnvVar | undefined> {
const envVarRepo = await this.getProjectEnvVarRepo();
return envVarRepo.findOne({ projectId, name: envVar.name, deleted: false });
}

public async addProjectEnvironmentVariable(projectId: string, envVar: ProjectEnvVarWithValue): Promise<void> {
const envVarRepo = await this.getProjectEnvVarRepo();
const envVarWithValue = await envVarRepo.findOne({ projectId, name, deleted: false });
if (envVarWithValue) {
await envVarRepo.update(
{ id: envVarWithValue.id, projectId: envVarWithValue.projectId },
{ value, censored },
);
return;
}
await envVarRepo.save({
id: uuidv4(),
projectId,
name,
value,
censored,
name: envVar.name,
value: envVar.value,
censored: envVar.censored,
creationTime: new Date().toISOString(),
deleted: false,
});
}

public async updateProjectEnvironmentVariable(
projectId: string,
envVar: Required<ProjectEnvVarWithValue>,
): Promise<void> {
const envVarRepo = await this.getProjectEnvVarRepo();
await envVarRepo.update({ id: envVar.id, projectId }, { value: envVar.value, censored: envVar.censored });
}

public async getProjectEnvironmentVariables(projectId: string): Promise<ProjectEnvVar[]> {
const envVarRepo = await this.getProjectEnvVarRepo();
const envVarsWithValue = await envVarRepo.find({ projectId, deleted: false });
Expand Down
39 changes: 34 additions & 5 deletions components/gitpod-db/src/typeorm/user-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
TokenEntry,
User,
UserEnvVar,
UserEnvVarValue,
UserSSHPublicKey,
} from "@gitpod/gitpod-protocol";
import { EncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
Expand All @@ -28,7 +29,7 @@ import {
OAuthUser,
} from "@jmondi/oauth2-server";
import { inject, injectable, optional } from "inversify";
import { EntityManager, Repository } from "typeorm";
import { EntityManager, Equal, Not, Repository } from "typeorm";
import { v4 as uuidv4 } from "uuid";
import {
BUILTIN_WORKSPACE_PROBE_USER_ID,
Expand Down Expand Up @@ -396,9 +397,38 @@ export class TypeORMUserDBImpl extends TransactionalDBImpl<UserDB> implements Us
return Number.parseInt(count);
}

public async setEnvVar(envVar: UserEnvVar): Promise<void> {
public async findEnvVar(userId: string, envVar: UserEnvVarValue): Promise<UserEnvVar | undefined> {
const repo = await this.getUserEnvVarRepo();
await repo.save(envVar);
return repo.findOne({
where: {
userId,
name: envVar.name,
repositoryPattern: envVar.repositoryPattern,
deleted: Not(Equal(true)),
},
});
}

public async addEnvVar(userId: string, envVar: UserEnvVarValue): Promise<void> {
const repo = await this.getUserEnvVarRepo();
await repo.save({
id: uuidv4(),
userId,
name: envVar.name,
repositoryPattern: envVar.repositoryPattern,
value: envVar.value,
});
}

public async updateEnvVar(userId: string, envVar: Required<UserEnvVarValue>): Promise<void> {
const repo = await this.getUserEnvVarRepo();
await repo.update(
{
id: envVar.id,
userId: userId,
},
{ name: envVar.name, repositoryPattern: envVar.repositoryPattern, value: envVar.value },
);
}

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

public async deleteEnvVar(envVar: UserEnvVar): Promise<void> {
envVar.deleted = true;
const repo = await this.getUserEnvVarRepo();
await repo.save(envVar);
await repo.update({ userId: envVar.userId, id: envVar.id }, { deleted: true });
}

public async hasSSHPublicKey(userId: string): Promise<boolean> {
Expand Down
5 changes: 4 additions & 1 deletion components/gitpod-db/src/user-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
TokenEntry,
User,
UserEnvVar,
UserEnvVarValue,
UserSSHPublicKey,
} from "@gitpod/gitpod-protocol";
import { OAuthTokenRepository, OAuthUserRepository } from "@jmondi/oauth2-server";
Expand Down Expand Up @@ -112,7 +113,9 @@ export interface UserDB extends OAuthUserRepository, OAuthTokenRepository, Trans
*/
findUsersByEmail(email: string): Promise<User[]>;

setEnvVar(envVar: UserEnvVar): Promise<void>;
findEnvVar(userId: string, envVar: UserEnvVarValue): Promise<UserEnvVar | undefined>;
addEnvVar(userId: string, envVar: UserEnvVarValue): Promise<void>;
updateEnvVar(userId: string, envVar: UserEnvVarValue): Promise<void>;
deleteEnvVar(envVar: UserEnvVar): Promise<void>;
getEnvVars(userId: string): Promise<UserEnvVar[]>;

Expand Down
8 changes: 5 additions & 3 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,12 +396,14 @@ export interface EnvVarWithValue {
}

export interface ProjectEnvVarWithValue extends EnvVarWithValue {
id: string;
projectId: string;
id?: string;
censored: boolean;
}

export type ProjectEnvVar = Omit<ProjectEnvVarWithValue, "value">;
export interface ProjectEnvVar extends Omit<ProjectEnvVarWithValue, "value"> {
id: string;
projectId: string;
}

export interface UserEnvVarValue extends EnvVarWithValue {
id?: string;
Expand Down
6 changes: 4 additions & 2 deletions components/server/src/authorization/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export type UserPermission =
| "read_ssh"
| "write_ssh"
| "read_tokens"
| "write_tokens";
| "write_tokens"
| "read_env_var"
| "write_env_var";

export type InstallationResourceType = "installation";

Expand Down Expand Up @@ -75,7 +77,7 @@ export type ProjectResourceType = "project";

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

export type ProjectPermission = "read_info" | "write_info" | "delete";
export type ProjectPermission = "read_info" | "write_info" | "delete" | "read_env_var" | "write_env_var";

export type WorkspaceResourceType = "workspace";

Expand Down
4 changes: 2 additions & 2 deletions components/server/src/container-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ import { WebsocketConnectionManager } from "./websocket/websocket-connection-man
import { ConfigProvider } from "./workspace/config-provider";
import { IContextParser, IPrefixContextParser } from "./workspace/context-parser";
import { ContextParser } from "./workspace/context-parser-service";
import { EnvVarService } from "./workspace/env-var-service";
import { EnvvarPrefixParser } from "./workspace/envvar-prefix-context-parser";
import { GitTokenScopeGuesser } from "./workspace/git-token-scope-guesser";
import { GitTokenValidator } from "./workspace/git-token-validator";
Expand All @@ -131,6 +130,7 @@ import { RelationshipUpdater } from "./authorization/relationship-updater";
import { WorkspaceService } from "./workspace/workspace-service";
import { SSHKeyService } from "./user/sshkey-service";
import { GitpodTokenService } from "./user/gitpod-token-service";
import { EnvVarService } from "./user/env-var-service";

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

bind(SSHKeyService).toSelf().inSingletonScope();
bind(GitpodTokenService).toSelf().inSingletonScope();
bind(EnvVarService).toSelf().inSingletonScope();

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

bind(OrganizationService).toSelf().inSingletonScope();
bind(ProjectsService).toSelf().inSingletonScope();
bind(EnvVarService).toSelf().inSingletonScope();

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

Expand Down
16 changes: 13 additions & 3 deletions components/server/src/prebuilds/prebuild-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ import { PrebuildRateLimiterConfig } from "../workspace/prebuild-rate-limiter";
import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { UserAuthentication } from "../user/user-authentication";
import { EntitlementService, MayStartWorkspaceResult } from "../billing/entitlement-service";
import { EnvVarService } from "../workspace/env-var-service";
import { WorkspaceService } from "../workspace/workspace-service";
import { EnvVarService } from "../user/env-var-service";

export class WorkspaceRunningError extends Error {
constructor(msg: string, public instance: WorkspaceInstance) {
Expand Down Expand Up @@ -230,7 +230,12 @@ export class PrebuildManager {
context.normalizedContextURL!,
);

const envVarsPromise = this.envVarService.resolve(workspace);
const envVarsPromise = this.envVarService.resolveEnvVariables(
workspace.ownerId,
workspace.projectId,
workspace.type,
workspace.context,
);

const prebuild = await this.workspaceDB.trace({ span }).findPrebuildByWorkspaceID(workspace.id)!;
if (!prebuild) {
Expand Down Expand Up @@ -334,7 +339,12 @@ export class PrebuildManager {
if (!prebuild) {
throw new Error("No prebuild found for workspace " + workspaceId);
}
const envVars = await this.envVarService.resolve(workspace);
const envVars = await this.envVarService.resolveEnvVariables(
workspace.ownerId,
workspace.projectId,
workspace.type,
workspace.context,
);
await this.workspaceStarter.startWorkspace({ span }, workspace, user, project, envVars);
return { prebuildId: prebuild.id, wsid: workspace.id, done: false };
} catch (err) {
Expand Down
58 changes: 0 additions & 58 deletions components/server/src/projects/projects-service.spec.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,64 +120,6 @@ describe("ProjectsService", async () => {
);
});

it("should let owners create, delete and get project env vars", async () => {
const ps = container.get(ProjectsService);
const project = await createTestProject(ps, org, owner);
await ps.setProjectEnvironmentVariable(owner.id, project.id, "FOO", "BAR", false);

const envVars = await ps.getProjectEnvironmentVariables(owner.id, project.id);
expect(envVars[0].name).to.equal("FOO");

const envVarById = await ps.getProjectEnvironmentVariableById(owner.id, envVars[0].id);
expect(envVarById?.name).to.equal("FOO");

await ps.deleteProjectEnvironmentVariable(owner.id, envVars[0].id);

await expectError(ErrorCodes.NOT_FOUND, () => ps.getProjectEnvironmentVariableById(owner.id, envVars[0].id));

const emptyEnvVars = await ps.getProjectEnvironmentVariables(owner.id, project.id);
expect(emptyEnvVars.length).to.equal(0);
});

it("should not let members create, delete but allow get project env vars", async () => {
const ps = container.get(ProjectsService);
const project = await createTestProject(ps, org, owner);
await ps.setProjectEnvironmentVariable(owner.id, project.id, "FOO", "BAR", false);

const envVars = await ps.getProjectEnvironmentVariables(member.id, project.id);
expect(envVars[0].name).to.equal("FOO");

const envVarById = await ps.getProjectEnvironmentVariableById(member.id, envVars[0].id);
expect(envVarById?.name).to.equal("FOO");

await expectError(ErrorCodes.PERMISSION_DENIED, () =>
ps.deleteProjectEnvironmentVariable(member.id, envVars[0].id),
);

await expectError(ErrorCodes.PERMISSION_DENIED, () =>
ps.setProjectEnvironmentVariable(member.id, project.id, "FOO", "BAR", false),
);
});

it("should not let strangers create, delete and get project env vars", async () => {
const ps = container.get(ProjectsService);
const project = await createTestProject(ps, org, owner);

await ps.setProjectEnvironmentVariable(owner.id, project.id, "FOO", "BAR", false);

const envVars = await ps.getProjectEnvironmentVariables(owner.id, project.id);
expect(envVars[0].name).to.equal("FOO");

// let's try to get the env var as a stranger
await expectError(ErrorCodes.NOT_FOUND, () => ps.getProjectEnvironmentVariableById(stranger.id, envVars[0].id));

// let's try to delete the env var as a stranger
await expectError(ErrorCodes.NOT_FOUND, () => ps.deleteProjectEnvironmentVariable(stranger.id, envVars[0].id));

// let's try to get the env vars as a stranger
await expectError(ErrorCodes.NOT_FOUND, () => ps.getProjectEnvironmentVariables(stranger.id, project.id));
});

it("should findProjects", async () => {
const ps = container.get(ProjectsService);
const project = await createTestProject(ps, org, owner);
Expand Down
36 changes: 0 additions & 36 deletions components/server/src/projects/projects-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
CreateProjectParams,
FindPrebuildsParams,
Project,
ProjectEnvVar,
User,
PrebuildEvent,
} from "@gitpod/gitpod-protocol";
Expand Down Expand Up @@ -386,41 +385,6 @@ export class ProjectsService {
return this.projectDB.updateProject(partialProject);
}

async setProjectEnvironmentVariable(
userId: string,
projectId: string,
name: string,
value: string,
censored: boolean,
): Promise<void> {
await this.auth.checkPermissionOnProject(userId, "write_info", projectId);
return this.projectDB.setProjectEnvironmentVariable(projectId, name, value, censored);
}

async getProjectEnvironmentVariables(userId: string, projectId: string): Promise<ProjectEnvVar[]> {
await this.auth.checkPermissionOnProject(userId, "read_info", projectId);
return this.projectDB.getProjectEnvironmentVariables(projectId);
}

async getProjectEnvironmentVariableById(userId: string, variableId: string): Promise<ProjectEnvVar> {
const result = await this.projectDB.getProjectEnvironmentVariableById(variableId);
if (!result) {
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Environment Variable ${variableId} not found.`);
}
try {
await this.auth.checkPermissionOnProject(userId, "read_info", result.projectId);
} catch (err) {
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Environment Variable ${variableId} not found.`);
}
return result;
}

async deleteProjectEnvironmentVariable(userId: string, variableId: string): Promise<void> {
const variable = await this.getProjectEnvironmentVariableById(userId, variableId);
await this.auth.checkPermissionOnProject(userId, "write_info", variable.projectId);
return this.projectDB.deleteProjectEnvironmentVariable(variableId);
}

async isProjectConsideredInactive(userId: string, projectId: string): Promise<boolean> {
await this.auth.checkPermissionOnProject(userId, "read_info", projectId);
const usage = await this.projectDB.getProjectUsage(projectId);
Expand Down
Loading