Skip to content

Commit ef16d23

Browse files
committed
[server] Extend guardTeamResource with fine-grained ops
1 parent 08b7539 commit ef16d23

File tree

2 files changed

+71
-17
lines changed

2 files changed

+71
-17
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
export type OrganizationOperation =
8+
// Not yet implemented operation time. This exists such that we can be explicit about what
9+
// we have not yet migrated to fine-grained-permissions.
10+
| "not_implemented"
11+
12+
// Ability to perform read actions an Organization, and all sub-resources of the organization.
13+
| "org_read"
14+
// Ability to perform write actions an Organization, and all sub-resources of the organization.
15+
| "org_write"
16+
17+
// Ability to read Team metadata - name, ID, but no members.
18+
| "org_metadata_read"
19+
20+
// Ability to access information about team members.
21+
| "org_members_read"
22+
// Ability to add, or remove, team members.
23+
| "org_members_write"
24+
25+
// Ability to read projects in an Organization.
26+
| "org_project_read"
27+
// Ability to create/update/delete projects in an Organization.
28+
| "org_project_write";

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

+43-17
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,11 @@ import * as grpc from "@grpc/grpc-js";
180180
import { CachingBlobServiceClientProvider } from "../util/content-service-sugar";
181181
import { CostCenterJSON } from "@gitpod/gitpod-protocol/lib/usage";
182182
import { createCookielessId, maskIp } from "../analytics";
183-
import { ConfigCatClientFactory } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
183+
import {
184+
ConfigCatClientFactory,
185+
getExperimentsClientForBackend,
186+
} from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
187+
import { OrganizationOperation } from "../authorization/perms";
184188

185189
// shortcut
186190
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
@@ -2033,11 +2037,26 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
20332037
protected async guardTeamOperation(
20342038
teamId: string,
20352039
op: ResourceAccessOp,
2040+
fineGrainedOps: OrganizationOperation[],
20362041
): Promise<{ team: Team; members: TeamMemberInfo[] }> {
20372042
if (!uuidValidate(teamId)) {
20382043
throw new ResponseError(ErrorCodes.BAD_REQUEST, "organization ID must be a valid UUID");
20392044
}
20402045

2046+
const user = this.checkUser();
2047+
const centralizedPermissionsEnabled = await getExperimentsClientForBackend().getValueAsync(
2048+
"centralizedPermissions",
2049+
false,
2050+
{
2051+
user: user,
2052+
teamId: teamId,
2053+
},
2054+
);
2055+
2056+
if (centralizedPermissionsEnabled) {
2057+
log.info("[perms] Checking team operations.", { org: teamId, operations: fineGrainedOps, user: user.id });
2058+
}
2059+
20412060
const team = await this.teamDB.findTeamById(teamId);
20422061
if (!team) {
20432062
// We return Permission Denied because we don't want to leak the existence, or not of the Organization.
@@ -2060,15 +2079,15 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
20602079

20612080
this.checkAndBlockUser("getTeam");
20622081

2063-
const { team } = await this.guardTeamOperation(teamId, "get");
2082+
const { team } = await this.guardTeamOperation(teamId, "get", ["org_members_read"]);
20642083
return team;
20652084
}
20662085

20672086
public async updateTeam(ctx: TraceContext, teamId: string, team: Pick<Team, "name">): Promise<Team> {
20682087
traceAPIParams(ctx, { teamId });
20692088
this.checkUser("updateTeam");
20702089

2071-
await this.guardTeamOperation(teamId, "update");
2090+
await this.guardTeamOperation(teamId, "update", ["org_write"]);
20722091

20732092
const updatedTeam = await this.teamDB.updateTeam(teamId, team);
20742093
return updatedTeam;
@@ -2077,7 +2096,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
20772096
public async getTeamMembers(ctx: TraceContext, teamId: string): Promise<TeamMemberInfo[]> {
20782097
traceAPIParams(ctx, { teamId });
20792098

2080-
const { members } = await this.guardTeamOperation(teamId, "get");
2099+
const { members } = await this.guardTeamOperation(teamId, "get", ["org_members_read"]);
20812100

20822101
return members;
20832102
}
@@ -2148,7 +2167,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
21482167
}
21492168

21502169
this.checkAndBlockUser("setTeamMemberRole");
2151-
await this.guardTeamOperation(teamId, "update");
2170+
await this.guardTeamOperation(teamId, "update", ["org_members_write"]);
21522171

21532172
await this.teamDB.setTeamMemberRole(userId, teamId, role);
21542173
}
@@ -2161,8 +2180,15 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
21612180
}
21622181

21632182
const user = this.checkAndBlockUser("removeTeamMember");
2164-
// Users are free to leave any team themselves, but only owners can remove others from their teams.
2165-
await this.guardTeamOperation(teamId, user.id === userId ? "get" : "update");
2183+
// The user is leaving a team, if they are removing themselves from the team.
2184+
const userLeavingTeam = user.id === userId;
2185+
2186+
if (userLeavingTeam) {
2187+
await this.guardTeamOperation(teamId, "update", ["not_implemented"]);
2188+
} else {
2189+
await this.guardTeamOperation(teamId, "get", ["org_members_write"]);
2190+
}
2191+
21662192
const membership = await this.teamDB.findTeamMembership(userId, teamId);
21672193
if (!membership) {
21682194
throw new Error(`Could not find membership for user '${userId}' in organization '${teamId}'`);
@@ -2183,7 +2209,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
21832209
traceAPIParams(ctx, { teamId });
21842210

21852211
this.checkUser("getGenericInvite");
2186-
await this.guardTeamOperation(teamId, "get");
2212+
await this.guardTeamOperation(teamId, "get", ["org_members_write"]);
21872213

21882214
const invite = await this.teamDB.findGenericInviteByTeamId(teamId);
21892215
if (invite) {
@@ -2196,7 +2222,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
21962222
traceAPIParams(ctx, { teamId });
21972223

21982224
this.checkAndBlockUser("resetGenericInvite");
2199-
await this.guardTeamOperation(teamId, "update");
2225+
await this.guardTeamOperation(teamId, "update", ["org_members_write"]);
22002226
return this.teamDB.resetGenericInvite(teamId);
22012227
}
22022228

@@ -2213,7 +2239,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
22132239
}
22142240
} else {
22152241
// Anyone who can read a team's information (i.e. any team member) can manage team projects
2216-
await this.guardTeamOperation(project.teamId || "", "get");
2242+
await this.guardTeamOperation(project.teamId || "", "get", ["not_implemented"]);
22172243
}
22182244
}
22192245

@@ -2240,7 +2266,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
22402266
}
22412267
} else {
22422268
// Anyone who can read a team's information (i.e. any team member) can create a new project.
2243-
await this.guardTeamOperation(params.teamId || "", "get");
2269+
await this.guardTeamOperation(params.teamId || "", "get", ["not_implemented"]);
22442270
}
22452271

22462272
return this.projectsService.createProject(params, user);
@@ -2265,7 +2291,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
22652291
const user = this.checkAndBlockUser("deleteTeam");
22662292
traceAPIParams(ctx, { teamId, userId: user.id });
22672293

2268-
await this.guardTeamOperation(teamId, "delete");
2294+
await this.guardTeamOperation(teamId, "delete", ["org_write"]);
22692295

22702296
const teamProjects = await this.projectsService.getTeamProjects(teamId);
22712297
teamProjects.forEach((project) => {
@@ -2298,7 +2324,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
22982324

22992325
this.checkUser("getTeamProjects");
23002326

2301-
await this.guardTeamOperation(teamId, "get");
2327+
await this.guardTeamOperation(teamId, "get", ["not_implemented"]);
23022328
return this.projectsService.getTeamProjects(teamId);
23032329
}
23042330

@@ -2921,7 +2947,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
29212947
}
29222948

29232949
// Ensure user can perform this operation on this organization
2924-
await this.guardTeamOperation(newProvider.organizationId, "create");
2950+
await this.guardTeamOperation(newProvider.organizationId, "create", ["org_write"]);
29252951

29262952
try {
29272953
// on creating we're are checking for already existing runtime providers
@@ -2969,7 +2995,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
29692995

29702996
await this.guardWithFeatureFlag("orgGitAuthProviders", providerUpdate.organizationId);
29712997

2972-
await this.guardTeamOperation(providerUpdate.organizationId, "update");
2998+
await this.guardTeamOperation(providerUpdate.organizationId, "update", ["org_write"]);
29732999

29743000
try {
29753001
const result = await this.authProviderService.updateOrgAuthProvider(providerUpdate);
@@ -2990,7 +3016,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
29903016

29913017
await this.guardWithFeatureFlag("orgGitAuthProviders", params.organizationId);
29923018

2993-
await this.guardTeamOperation(params.organizationId, "get");
3019+
await this.guardTeamOperation(params.organizationId, "get", ["org_read"]);
29943020

29953021
try {
29963022
const result = await this.authProviderService.getAuthProvidersOfOrg(params.organizationId);
@@ -3021,7 +3047,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
30213047
throw new ResponseError(ErrorCodes.NOT_FOUND, "Provider resource not found.");
30223048
}
30233049

3024-
await this.guardTeamOperation(authProvider.organizationId || "", "delete");
3050+
await this.guardTeamOperation(authProvider.organizationId || "", "delete", ["org_write"]);
30253051

30263052
try {
30273053
await this.authProviderService.deleteAuthProvider(authProvider);

0 commit comments

Comments
 (0)