Skip to content

Commit 4f0ea03

Browse files
committed
dev 2
1 parent dfde0b6 commit 4f0ea03

File tree

5 files changed

+179
-6
lines changed

5 files changed

+179
-6
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright (c) 2022 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 AttributionId = UserAttributionId | TeamAttributionId;
8+
export type AttributionTarget = "user" | "team";
9+
10+
export interface UserAttributionId {
11+
kind: "user";
12+
userId: string;
13+
}
14+
export interface TeamAttributionId {
15+
kind: "team";
16+
teamId: string;
17+
}
18+
19+
export namespace AttributionId {
20+
const SEPARATOR = ":";
21+
22+
export function parse(s: string): UserAttributionId | TeamAttributionId | undefined {
23+
if (!s) {
24+
return undefined;
25+
}
26+
const parts = s.split(":");
27+
if (parts.length !== 2) {
28+
return undefined;
29+
}
30+
switch (parts[0]) {
31+
case "user":
32+
return { kind: "user", userId: parts[1] };
33+
case "team":
34+
return { kind: "team", teamId: parts[1] };
35+
default:
36+
return undefined;
37+
}
38+
}
39+
40+
export function render(id: AttributionId): string {
41+
switch (id.kind) {
42+
case "user":
43+
return `user${SEPARATOR}${id.userId}`;
44+
case "team":
45+
return `team${SEPARATOR}${id.teamId}`;
46+
}
47+
}
48+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2069,7 +2069,13 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
20692069

20702070
// TODO(gpl) We need a GuardedCostCenter to authorize access to reports
20712071
// const costCenter = await this.costCenterDB.findByAttributionId(attributionId);
2072-
// await this.guardAccess({ kind: "GuardedCostCenter", subject: costCenter }, "get");
2072+
Attribution;
2073+
const team = await this.teamDB.findTeamById(teamId);
2074+
if (!team) {
2075+
throw new ResponseError(ErrorCodes.NOT_FOUND, "Team not found");
2076+
}
2077+
const members = await this.teamDB.findMembersByTeam(team.id);
2078+
await this.guardAccess({ kind: "costCenter", /*subject: costCenter,*/ team, members }, "get");
20732079

20742080
const usageClient = this.usageServiceClientProvider.getDefault();
20752081
const response = await usageClient.getBilledUsage(ctx, attributionId);

components/server/src/auth/resource-access.spec.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
SharedWorkspaceAccessGuard,
2424
} from "./resource-access";
2525
import { PrebuiltWorkspace, User, UserEnvVar, Workspace, WorkspaceType } from "@gitpod/gitpod-protocol/lib/protocol";
26-
import { TeamMemberInfo, TeamMemberRole, WorkspaceInstance } from "@gitpod/gitpod-protocol";
26+
import { Team, TeamMemberInfo, TeamMemberRole, WorkspaceInstance } from "@gitpod/gitpod-protocol";
2727
import { HostContextProvider } from "./host-context-provider";
2828

2929
class MockedRepositoryResourceGuard extends RepositoryResourceGuard {
@@ -233,6 +233,108 @@ class TestResourceAccess {
233233
}
234234
}
235235

236+
@test public async costCenterResourceGuard() {
237+
const createUser = (): User => {
238+
return {
239+
id: "123",
240+
name: "testuser",
241+
creationDate: new Date(2000, 1, 1).toISOString(),
242+
identities: [
243+
{
244+
authId: "123",
245+
authName: "testuser",
246+
authProviderId: "github.com",
247+
},
248+
],
249+
};
250+
};
251+
252+
const tests: {
253+
name: string;
254+
teamRole: TeamMemberRole | undefined;
255+
operation: ResourceAccessOp;
256+
expectation: boolean;
257+
}[] = [
258+
{
259+
name: "member - get",
260+
teamRole: "member",
261+
operation: "get",
262+
expectation: false,
263+
},
264+
{
265+
name: "member - update",
266+
teamRole: "member",
267+
operation: "update",
268+
expectation: false,
269+
},
270+
{
271+
name: "member - update",
272+
teamRole: "member",
273+
operation: "create",
274+
expectation: false,
275+
},
276+
{
277+
name: "member - delete",
278+
teamRole: "member",
279+
operation: "delete",
280+
expectation: false,
281+
},
282+
{
283+
name: "team owner - get",
284+
teamRole: "owner",
285+
operation: "get",
286+
expectation: true,
287+
},
288+
{
289+
name: "team owner - update",
290+
teamRole: "owner",
291+
operation: "update",
292+
expectation: true,
293+
},
294+
{
295+
name: "team owner - update",
296+
teamRole: "owner",
297+
operation: "create",
298+
expectation: true,
299+
},
300+
{
301+
name: "team owner - delete",
302+
teamRole: "owner",
303+
operation: "delete",
304+
expectation: true,
305+
},
306+
];
307+
308+
for (const t of tests) {
309+
const user = createUser();
310+
const team: Team = {
311+
id: "team-123",
312+
name: "test-team",
313+
creationTime: user.creationDate,
314+
slug: "test-team",
315+
};
316+
const resourceGuard = new CompositeResourceAccessGuard([
317+
new OwnerResourceGuard(user.id),
318+
new TeamMemberResourceGuard(user.id),
319+
new SharedWorkspaceAccessGuard(),
320+
new MockedRepositoryResourceGuard(true),
321+
]);
322+
const teamMembers: TeamMemberInfo[] = [];
323+
if (!!t.teamRole) {
324+
teamMembers.push({
325+
userId: user.id,
326+
role: t.teamRole,
327+
memberSince: user.creationDate,
328+
});
329+
}
330+
const actual = await resourceGuard.canAccess({ kind: "costCenter", team, members: teamMembers }, "get");
331+
expect(actual).to.be.eq(
332+
t.expectation,
333+
`"${t.name}" expected canAccess(resource, "${t.operation}") === ${t.expectation}, but was ${actual}`,
334+
);
335+
}
336+
}
337+
236338
@test public async tokenResourceGuardCanAccess() {
237339
const workspaceResource: GuardedResource = {
238340
kind: "workspace",

components/server/src/auth/resource-access.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type GuardedResource =
3737
| GuardedContentBlob
3838
| GuardEnvVar
3939
| GuardedTeam
40+
| GuardedCostCenter
4041
| GuardedWorkspaceLog
4142
| GuardedPrebuild;
4243

@@ -51,6 +52,7 @@ const ALL_GUARDED_RESOURCE_KINDS = new Set<GuardedResourceKind>([
5152
"contentBlob",
5253
"envVar",
5354
"team",
55+
"costCenter",
5456
"workspaceLog",
5557
]);
5658
export function isGuardedResourceKind(kind: any): kind is GuardedResourceKind {
@@ -104,6 +106,13 @@ export interface GuardedTeam {
104106
members: TeamMemberInfo[];
105107
}
106108

109+
export interface GuardedCostCenter {
110+
kind: "costCenter";
111+
//subject: CostCenter;
112+
team: Team;
113+
members: TeamMemberInfo[];
114+
}
115+
107116
export interface GuardedGitpodToken {
108117
kind: "gitpodToken";
109118
subject: GitpodToken;
@@ -165,6 +174,10 @@ export class TeamMemberResourceGuard implements ResourceAccessGuard {
165174
return await this.hasAccessToWorkspace(resource.subject, resource.teamMembers);
166175
case "prebuild":
167176
return !!resource.teamMembers?.some((m) => m.userId === this.userId);
177+
case "costCenter":
178+
// TODO(gpl) We should check whether we're looking at the right team for the right CostCenter here!
179+
// Only team "owners" are allowed to do anything with CostCenters
180+
return resource.members.filter((m) => m.role === "owner").some((m) => m.userId === this.userId);
168181
}
169182
return false;
170183
}
@@ -218,6 +231,9 @@ export class OwnerResourceGuard implements ResourceAccessGuard {
218231
// Only owners can update or delete a team.
219232
return resource.members.some((m) => m.userId === this.userId && m.role === "owner");
220233
}
234+
case "costCenter":
235+
// CostCenter does not have a single "owner" but is owned by a team
236+
return false;
221237
case "workspaceLog":
222238
return resource.subject.ownerId === this.userId;
223239
case "prebuild":

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { TermsProvider } from "../terms/terms-provider";
2727
import { TokenService } from "./token-service";
2828
import { EmailAddressAlreadyTakenException, SelectAccountException } from "../auth/errors";
2929
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
30+
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
3031

3132
export interface FindUserByIdentityStrResult {
3233
user: User;
@@ -226,7 +227,7 @@ export class UserService {
226227
if (this.config.enablePayment) {
227228
if (!user.additionalData?.usageAttributionId) {
228229
// No explicit user attribution ID yet -- attribute all usage to the user by default (regardless of project/team).
229-
return `user:${user.id}`;
230+
return AttributionId.render({ kind: "user", userId: user.id });
230231
}
231232
// Return the user's explicit attribution ID.
232233
return user.additionalData.usageAttributionId;
@@ -235,15 +236,15 @@ export class UserService {
235236
// B. Project-based attribution
236237
if (!projectId) {
237238
// No project -- attribute to the user.
238-
return `user:${user.id}`;
239+
return AttributionId.render({ kind: "user", userId: user.id });
239240
}
240241
const project = await this.projectDb.findProjectById(projectId);
241242
if (!project?.teamId) {
242243
// The project doesn't exist, or it isn't owned by a team -- attribute to the user.
243-
return `user:${user.id}`;
244+
return AttributionId.render({ kind: "user", userId: user.id });
244245
}
245246
// Attribute workspace usage to the team that currently owns this project.
246-
return `team:${project.teamId}`;
247+
return AttributionId.render({ kind: "team", teamId: project.teamId });
247248
}
248249

249250
/**

0 commit comments

Comments
 (0)