Skip to content

[server] Extend guardTeamResource with fine-grained ops #16194

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
Feb 3, 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
14 changes: 7 additions & 7 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1596,7 +1596,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
// Team Subscriptions 2
async getTeamSubscription(ctx: TraceContext, teamId: string): Promise<TeamSubscription2 | undefined> {
this.checkUser("getTeamSubscription");
await this.guardTeamOperation(teamId, "get");
await this.guardTeamOperation(teamId, "get", ["not_implemented"]);
return this.teamSubscription2DB.findForTeam(teamId, new Date().toISOString());
}

Expand Down Expand Up @@ -2098,7 +2098,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {

try {
if (attrId.kind == "team") {
await this.guardTeamOperation(attrId.teamId, "get");
await this.guardTeamOperation(attrId.teamId, "get", ["not_implemented"]);
}
const subscriptionId = await this.stripeService.findUncancelledSubscriptionByAttributionId(attributionId);
return subscriptionId;
Expand All @@ -2119,7 +2119,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
}
let team: Team | undefined;
if (attrId.kind === "team") {
team = (await this.guardTeamOperation(attrId.teamId, "update")).team;
team = (await this.guardTeamOperation(attrId.teamId, "update", ["not_implemented"])).team;
await this.ensureStripeApiIsAllowed({ team });
} else {
if (attrId.userId !== user.id) {
Expand All @@ -2141,7 +2141,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
}
let team: Team | undefined;
if (attrId.kind === "team") {
team = (await this.guardTeamOperation(attrId.teamId, "update")).team;
team = (await this.guardTeamOperation(attrId.teamId, "update", ["not_implemented"])).team;
await this.ensureStripeApiIsAllowed({ team });
} else {
if (attrId.userId !== user.id) {
Expand Down Expand Up @@ -2211,7 +2211,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
let team: Team | undefined;
try {
if (attrId.kind === "team") {
team = (await this.guardTeamOperation(attrId.teamId, "update")).team;
team = (await this.guardTeamOperation(attrId.teamId, "update", ["not_implemented"])).team;
await this.ensureStripeApiIsAllowed({ team });
} else {
await this.ensureStripeApiIsAllowed({ user });
Expand Down Expand Up @@ -2257,7 +2257,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
if (attrId.kind === "user") {
await this.ensureStripeApiIsAllowed({ user });
} else if (attrId.kind === "team") {
const team = (await this.guardTeamOperation(attrId.teamId, "update")).team;
const team = (await this.guardTeamOperation(attrId.teamId, "update", ["not_implemented"])).team;
await this.ensureStripeApiIsAllowed({ team });
returnUrl = this.config.hostUrl
.with(() => ({ pathname: `/org-billing`, search: `org=${team.id}` }))
Expand Down Expand Up @@ -2493,7 +2493,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
traceAPIParams(ctx, { teamId });

this.checkAndBlockUser("getBillingModeForTeam");
const { team } = await this.guardTeamOperation(teamId, "get");
const { team } = await this.guardTeamOperation(teamId, "get", ["not_implemented"]);

return this.billingModes.getBillingModeForTeam(team, new Date());
}
Expand Down
33 changes: 33 additions & 0 deletions components/server/src/authorization/perms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

export type OrganizationOperation =
// A not yet implemented operation at this time. This exists such that we can be explicit about what
// we have not yet migrated to fine-grained-permissions.
| "not_implemented"

// Ability to perform write actions an Organization, and all sub-resources of the organization.
| "org_write"

// Ability to update Organization metadata - name, and other general info.
| "org_metadata_write"
// Ability to read Organization metadata - name, and other general info.
| "org_metadata_read"

// Ability to access information about team members.
| "org_members_read"
// Ability to add, or remove, team members.
| "org_members_write"

// Ability to read projects in an Organization.
| "org_project_read"
// Ability to create/update/delete projects in an Organization.
| "org_project_write"

// Ability to create/update/delete Organization Auth Providers.
| "org_authprovider_write"
// Ability to read Organization Auth Providers.
| "org_authprovider_read";
60 changes: 43 additions & 17 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,11 @@ import * as grpc from "@grpc/grpc-js";
import { CachingBlobServiceClientProvider } from "../util/content-service-sugar";
import { CostCenterJSON } from "@gitpod/gitpod-protocol/lib/usage";
import { createCookielessId, maskIp } from "../analytics";
import { ConfigCatClientFactory } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
import {
ConfigCatClientFactory,
getExperimentsClientForBackend,
} from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
import { OrganizationOperation } from "../authorization/perms";

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

const user = this.checkUser();
const centralizedPermissionsEnabled = await getExperimentsClientForBackend().getValueAsync(
"centralizedPermissions",
false,
{
user: user,
teamId: teamId,
},
);

if (centralizedPermissionsEnabled) {
log.info("[perms] Checking team operations.", { org: teamId, operations: fineGrainedOps, user: user.id });
}

const team = await this.teamDB.findTeamById(teamId);
if (!team) {
// We return Permission Denied because we don't want to leak the existence, or not of the Organization.
Expand All @@ -2060,15 +2079,15 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {

this.checkAndBlockUser("getTeam");

const { team } = await this.guardTeamOperation(teamId, "get");
const { team } = await this.guardTeamOperation(teamId, "get", ["org_members_read"]);
return team;
}

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

await this.guardTeamOperation(teamId, "update");
await this.guardTeamOperation(teamId, "update", ["org_metadata_write"]);

const updatedTeam = await this.teamDB.updateTeam(teamId, team);
return updatedTeam;
Expand All @@ -2078,7 +2097,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
traceAPIParams(ctx, { teamId });

this.checkUser("getTeamMembers");
const { members } = await this.guardTeamOperation(teamId, "get");
const { members } = await this.guardTeamOperation(teamId, "get", ["org_members_read"]);

return members;
}
Expand Down Expand Up @@ -2149,7 +2168,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
}

this.checkAndBlockUser("setTeamMemberRole");
await this.guardTeamOperation(teamId, "update");
await this.guardTeamOperation(teamId, "update", ["org_members_write"]);

await this.teamDB.setTeamMemberRole(userId, teamId, role);
}
Expand All @@ -2162,8 +2181,15 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
}

const user = this.checkAndBlockUser("removeTeamMember");
// Users are free to leave any team themselves, but only owners can remove others from their teams.
await this.guardTeamOperation(teamId, user.id === userId ? "get" : "update");
// The user is leaving a team, if they are removing themselves from the team.
const userLeavingTeam = user.id === userId;

if (userLeavingTeam) {
await this.guardTeamOperation(teamId, "update", ["not_implemented"]);
} else {
await this.guardTeamOperation(teamId, "get", ["org_members_write"]);
}

const membership = await this.teamDB.findTeamMembership(userId, teamId);
if (!membership) {
throw new Error(`Could not find membership for user '${userId}' in organization '${teamId}'`);
Expand All @@ -2184,7 +2210,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
traceAPIParams(ctx, { teamId });

this.checkUser("getGenericInvite");
await this.guardTeamOperation(teamId, "get");
await this.guardTeamOperation(teamId, "get", ["org_members_write"]);

const invite = await this.teamDB.findGenericInviteByTeamId(teamId);
if (invite) {
Expand All @@ -2197,7 +2223,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
traceAPIParams(ctx, { teamId });

this.checkAndBlockUser("resetGenericInvite");
await this.guardTeamOperation(teamId, "update");
await this.guardTeamOperation(teamId, "update", ["org_members_write"]);
return this.teamDB.resetGenericInvite(teamId);
}

Expand All @@ -2214,7 +2240,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
}
} else {
// Anyone who can read a team's information (i.e. any team member) can manage team projects
await this.guardTeamOperation(project.teamId || "", "get");
await this.guardTeamOperation(project.teamId || "", "get", ["not_implemented"]);
}
}

Expand All @@ -2241,7 +2267,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
}
} else {
// Anyone who can read a team's information (i.e. any team member) can create a new project.
await this.guardTeamOperation(params.teamId || "", "get");
await this.guardTeamOperation(params.teamId || "", "get", ["not_implemented"]);
}

return this.projectsService.createProject(params, user);
Expand All @@ -2266,7 +2292,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
const user = this.checkAndBlockUser("deleteTeam");
traceAPIParams(ctx, { teamId, userId: user.id });

await this.guardTeamOperation(teamId, "delete");
await this.guardTeamOperation(teamId, "delete", ["org_write"]);

const teamProjects = await this.projectsService.getTeamProjects(teamId);
teamProjects.forEach((project) => {
Expand Down Expand Up @@ -2299,7 +2325,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {

this.checkUser("getTeamProjects");

await this.guardTeamOperation(teamId, "get");
await this.guardTeamOperation(teamId, "get", ["not_implemented"]);
return this.projectsService.getTeamProjects(teamId);
}

Expand Down Expand Up @@ -2922,7 +2948,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
}

// Ensure user can perform this operation on this organization
await this.guardTeamOperation(newProvider.organizationId, "create");
await this.guardTeamOperation(newProvider.organizationId, "create", ["org_authprovider_write"]);

try {
// on creating we're are checking for already existing runtime providers
Expand Down Expand Up @@ -2970,7 +2996,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {

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

await this.guardTeamOperation(providerUpdate.organizationId, "update");
await this.guardTeamOperation(providerUpdate.organizationId, "update", ["org_authprovider_write"]);

try {
const result = await this.authProviderService.updateOrgAuthProvider(providerUpdate);
Expand All @@ -2991,7 +3017,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {

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

await this.guardTeamOperation(params.organizationId, "get");
await this.guardTeamOperation(params.organizationId, "get", ["org_authprovider_read"]);

try {
const result = await this.authProviderService.getAuthProvidersOfOrg(params.organizationId);
Expand Down Expand Up @@ -3022,7 +3048,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
throw new ResponseError(ErrorCodes.NOT_FOUND, "Provider resource not found.");
}

await this.guardTeamOperation(authProvider.organizationId || "", "delete");
await this.guardTeamOperation(authProvider.organizationId || "", "delete", ["org_authprovider_write"]);

try {
await this.authProviderService.deleteAuthProvider(authProvider);
Expand Down