Skip to content

Commit 40b579a

Browse files
committed
[db][server][dashboard] Allow censoring Project environment variables out of Workspaces
1 parent 91f3f92 commit 40b579a

File tree

9 files changed

+44
-32
lines changed

9 files changed

+44
-32
lines changed

components/dashboard/src/projects/ProjectVariables.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import { Project, ProjectEnvVar } from "@gitpod/gitpod-protocol";
88
import { useContext, useEffect, useState } from "react";
99
import AlertBox from "../components/AlertBox";
10+
import CheckBox from "../components/CheckBox";
11+
import InfoBox from "../components/InfoBox";
1012
import { Item, ItemField, ItemFieldContextMenu, ItemsList } from "../components/ItemsList";
1113
import Modal from "../components/Modal";
1214
import { getGitpodService } from "../service/service";
@@ -54,15 +56,17 @@ export default function () {
5456
</div>
5557
: <>
5658
<ItemsList>
57-
<Item header={true} className="grid grid-cols-3 items-center">
59+
<Item header={true} className="grid grid-cols-4 items-center">
5860
<ItemField>Name</ItemField>
5961
<ItemField>Value</ItemField>
62+
<ItemField>Visible in Workspaces?</ItemField>
6063
<ItemField></ItemField>
6164
</Item>
6265
{envVars.map(variable => {
63-
return <Item className="grid grid-cols-3 items-center">
66+
return <Item className="grid grid-cols-4 items-center">
6467
<ItemField>{variable.name}</ItemField>
6568
<ItemField>****</ItemField>
69+
<ItemField>{variable.censored ? 'Hidden' : 'Visible'}</ItemField>
6670
<ItemField className="flex justify-end">
6771
<ItemFieldContextMenu menuEntries={[
6872
{
@@ -83,14 +87,15 @@ export default function () {
8387
function AddVariableModal(props: { project?: Project, onClose: () => void }) {
8488
const [ name, setName ] = useState<string>("");
8589
const [ value, setValue ] = useState<string>("");
90+
const [ censored, setCensored ] = useState<boolean>(true);
8691
const [ error, setError ] = useState<Error | undefined>();
8792

8893
const addVariable = async () => {
8994
if (!props.project) {
9095
return;
9196
}
9297
try {
93-
await getGitpodService().server.setProjectEnvironmentVariable(props.project.id, name, value);
98+
await getGitpodService().server.setProjectEnvironmentVariable(props.project.id, name, value, censored);
9499
props.onClose();
95100
} catch (err) {
96101
setError(err);
@@ -104,18 +109,24 @@ function AddVariableModal(props: { project?: Project, onClose: () => void }) {
104109
{error && <div className="bg-gitpod-kumquat-light rounded-md p-3 text-gitpod-red text-sm mb-2">
105110
{error}
106111
</div>}
107-
<div className="mt-4">
112+
<div className="mt-8">
108113
<h4>Name</h4>
109114
<input autoFocus className="w-full" type="text" name="name" value={name} onChange={e => setName(e.target.value)} />
110115
</div>
111116
<div className="mt-4">
112117
<h4>Value</h4>
113118
<input className="w-full" type="text" name="value" value={value} onChange={e => setValue(e.target.value)} />
114119
</div>
120+
<div className="mt-4">
121+
<CheckBox title="Hide in Workspaces" desc="Project variables are visible during prebuilds. Choose whether this variable should be visible in workspaces as well." checked={censored} onChange={() => setCensored(!censored)} />
122+
</div>
123+
{!censored && <div className="mt-4">
124+
<InfoBox>This value will be directly visible to anyone who can open your repository in Gitpod.</InfoBox>
125+
</div>}
115126
</div>
116127
<div className="flex justify-end mt-6">
117128
<button className="secondary" onClick={props.onClose}>Cancel</button>
118-
<button className="ml-2" onClick={addVariable} >Add Variable</button>
129+
<button className="ml-2" onClick={addVariable}>Add Variable</button>
119130
</div>
120131
</Modal>;
121132
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface ProjectDB {
1616
storeProject(project: Project): Promise<Project>;
1717
updateProject(partialProject: PartialProject): Promise<void>;
1818
markDeleted(projectId: string): Promise<void>;
19-
setProjectEnvironmentVariable(projectId: string, name: string, value: string): Promise<void>;
19+
setProjectEnvironmentVariable(projectId: string, name: string, value: string, censored: boolean): Promise<void>;
2020
getProjectEnvironmentVariables(projectId: string): Promise<ProjectEnvVar[]>;
2121
getProjectEnvironmentVariableById(variableId: string): Promise<ProjectEnvVar | undefined>;
2222
deleteProjectEnvironmentVariable(variableId: string): Promise<void>;

components/gitpod-db/src/typeorm/entity/db-project-env-vars.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export class DBProjectEnvVar implements ProjectEnvVarWithValue {
3434
})
3535
value: string;
3636

37+
@Column()
38+
censored: boolean;
39+
3740
@Column("varchar")
3841
creationTime: string;
3942

components/gitpod-db/src/typeorm/migration/1639735838107-ProjectEnvVars.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { MigrationInterface, QueryRunner } from "typeorm";
99
export class ProjectEnvVars1639735838107 implements MigrationInterface {
1010

1111
public async up(queryRunner: QueryRunner): Promise<void> {
12-
await queryRunner.query("CREATE TABLE IF NOT EXISTS `d_b_project_env_var` (`id` char(36) NOT NULL, `projectId` char(36) NOT NULL, `name` varchar(255) NOT NULL, `value` text NOT NULL, `creationTime` varchar(255) NOT NULL, `deleted` tinyint(4) NOT NULL DEFAULT '0', `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`, `projectId`), KEY `ind_projectid` (projectId), KEY `ind_dbsync` (`_lastModified`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
12+
await queryRunner.query("CREATE TABLE IF NOT EXISTS `d_b_project_env_var` (`id` char(36) NOT NULL, `projectId` char(36) NOT NULL, `name` varchar(255) NOT NULL, `value` text NOT NULL, `censored` tinyint(4) NOT NULL, `creationTime` varchar(255) NOT NULL, `deleted` tinyint(4) NOT NULL DEFAULT '0', `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`, `projectId`), KEY `ind_projectid` (projectId), KEY `ind_dbsync` (`_lastModified`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
1313
}
1414

1515
public async down(queryRunner: QueryRunner): Promise<void> {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,19 @@ export class ProjectDBImpl implements ProjectDB {
108108
}
109109
}
110110

111-
public async setProjectEnvironmentVariable(projectId: string, name: string, value: string): Promise<void>{
111+
public async setProjectEnvironmentVariable(projectId: string, name: string, value: string, censored: boolean): Promise<void>{
112112
const envVarRepo = await this.getProjectEnvVarRepo();
113113
const envVarWithValue = await envVarRepo.findOne({ projectId, name, deleted: false });
114114
if (envVarWithValue) {
115-
await envVarRepo.update({ id: envVarWithValue.id, projectId: envVarWithValue.projectId }, { value });
115+
await envVarRepo.update({ id: envVarWithValue.id, projectId: envVarWithValue.projectId }, { value, censored });
116116
return;
117117
}
118118
await envVarRepo.save({
119119
id: uuidv4(),
120120
projectId,
121121
name,
122122
value,
123+
censored,
123124
creationTime: new Date().toISOString(),
124125
deleted: false,
125126
});

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
141141
guessRepositoryConfiguration(cloneUrl: string): Promise<string | undefined>;
142142
setProjectConfiguration(projectId: string, configString: string): Promise<void>;
143143
updateProjectPartial(partialProject: PartialProject): Promise<void>;
144-
setProjectEnvironmentVariable(projectId: string, name: string, value: string): Promise<void>;
144+
setProjectEnvironmentVariable(projectId: string, name: string, value: string, censored: boolean): Promise<void>;
145145
getProjectEnvironmentVariables(projectId: string): Promise<ProjectEnvVar[]>;
146146
deleteProjectEnvironmentVariable(variableId: string): Promise<void>;
147147

components/gitpod-protocol/src/protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export interface EnvVarWithValue {
162162
export interface ProjectEnvVarWithValue extends EnvVarWithValue {
163163
id: string;
164164
projectId: string;
165+
censored: boolean;
165166
}
166167

167168
export type ProjectEnvVar = Omit<ProjectEnvVarWithValue, 'value'>;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,8 @@ export class ProjectsService {
181181
return this.projectDB.updateProject(partialProject);
182182
}
183183

184-
async setProjectEnvironmentVariable(projectId: string, name: string, value: string): Promise<void> {
185-
return this.projectDB.setProjectEnvironmentVariable(projectId, name, value);
184+
async setProjectEnvironmentVariable(projectId: string, name: string, value: string, censored: boolean): Promise<void> {
185+
return this.projectDB.setProjectEnvironmentVariable(projectId, name, value, censored);
186186
}
187187

188188
async getProjectEnvironmentVariables(projectId: string): Promise<ProjectEnvVar[]> {

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

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -484,15 +484,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
484484
throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "Cannot (re-)start a deleted workspace.");
485485
}
486486
const userEnvVars = this.userDB.getEnvVars(user.id);
487-
let projectEnvVarsPromise: Promise<ProjectEnvVar[]> = Promise.resolve([]);
488-
if (workspace.projectId) {
489-
try {
490-
await this.guardProjectOperation(user, workspace.projectId, "get");
491-
projectEnvVarsPromise = this.projectsService.getProjectEnvironmentVariables(workspace.projectId);
492-
} catch (error) {
493-
log.debug({ userId: user.id }, "User doesn't have access to the Project, thus not loading Project environment variables:", error);
494-
}
495-
}
487+
let projectEnvVarsPromise = this.internalGetPublicProjectEnvVars(workspace.projectId);
496488

497489
await mayStartPromise;
498490

@@ -850,15 +842,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
850842
throw err;
851843
}
852844

853-
let projectEnvVarsPromise: Promise<ProjectEnvVar[]> = Promise.resolve([]);
854-
if (workspace.projectId) {
855-
try {
856-
await this.guardProjectOperation(user, workspace.projectId, "get");
857-
projectEnvVarsPromise = this.projectsService.getProjectEnvironmentVariables(workspace.projectId);
858-
} catch (error) {
859-
log.warn(logContext, "User doesn't have access to the Project, thus not loading Project environment variables:", error);
860-
}
861-
}
845+
let projectEnvVarsPromise = this.internalGetPublicProjectEnvVars(workspace.projectId);
862846

863847
logContext.workspaceId = workspace.id;
864848
traceWI(ctx, { workspaceId: workspace.id });
@@ -1374,11 +1358,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
13741358
await this.userDB.deleteEnvVar(envvar);
13751359
}
13761360

1377-
async setProjectEnvironmentVariable(ctx: TraceContext, projectId: string, name: string, value: string): Promise<void> {
1361+
async setProjectEnvironmentVariable(ctx: TraceContext, projectId: string, name: string, value: string, censored: boolean): Promise<void> {
13781362
traceAPIParams(ctx, { projectId, name }); // value may contain secrets
13791363
const user = this.checkAndBlockUser("setProjectEnvironmentVariable");
13801364
await this.guardProjectOperation(user, projectId, "update");
1381-
return this.projectsService.setProjectEnvironmentVariable(projectId, name, value);
1365+
return this.projectsService.setProjectEnvironmentVariable(projectId, name, value, censored);
13821366
}
13831367

13841368
async getProjectEnvironmentVariables(ctx: TraceContext, projectId: string): Promise<ProjectEnvVar[]> {
@@ -1399,6 +1383,18 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
13991383
return this.projectsService.deleteProjectEnvironmentVariable(envVar.id);
14001384
}
14011385

1386+
protected async internalGetPublicProjectEnvVars(projectId?: string): Promise<ProjectEnvVar[]> {
1387+
if (!projectId) {
1388+
return [];
1389+
}
1390+
const projectEnvVars = await this.projectsService.getProjectEnvironmentVariables(projectId);
1391+
// Instead of using an access guard for Project environment variables, we let Project owners decide whether
1392+
// a variable should be:
1393+
// - exposed in all workspaces (even for non-Project members when the repository is public), or
1394+
// - censored from all workspaces (even for Project members)
1395+
return projectEnvVars.filter(variable => !variable.censored);
1396+
}
1397+
14021398
protected async guardTeamOperation(teamId: string | undefined, op: ResourceAccessOp): Promise<void> {
14031399
const team = await this.teamDB.findTeamById(teamId || "");
14041400
if (!team) {

0 commit comments

Comments
 (0)