Skip to content

Commit 667bd2f

Browse files
authored
[usage] add optional UserID to filter usage (#18449)
1 parent b2324bd commit 667bd2f

File tree

9 files changed

+302
-166
lines changed

9 files changed

+302
-166
lines changed

components/gitpod-db/go/usage.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ type WorkspaceInstanceUsageData struct {
9393
}
9494

9595
type CreditNoteMetaData struct {
96-
UserId string `json:userId`
96+
UserID string `json:"userId"`
9797
}
9898

9999
type FindUsageResult struct {
@@ -141,6 +141,7 @@ func FindAllDraftUsage(ctx context.Context, conn *gorm.DB) ([]Usage, error) {
141141

142142
type FindUsageParams struct {
143143
AttributionId AttributionID
144+
UserID uuid.UUID
144145
From, To time.Time
145146
ExcludeDrafts bool
146147
Order Order
@@ -152,8 +153,11 @@ func FindUsage(ctx context.Context, conn *gorm.DB, params *FindUsageParams) ([]U
152153
var usageRecordsBatch []Usage
153154

154155
db := conn.WithContext(ctx).
155-
Where("attributionId = ?", params.AttributionId).
156-
Where("effectiveTime >= ? AND effectiveTime < ?", TimeToISO8601(params.From), TimeToISO8601(params.To)).
156+
Where("attributionId = ?", params.AttributionId)
157+
if params.UserID != uuid.Nil {
158+
db = db.Where("metadata->>'$.userId' = ?", params.UserID.String())
159+
}
160+
db = db.Where("effectiveTime >= ? AND effectiveTime < ?", TimeToISO8601(params.From), TimeToISO8601(params.To)).
157161
Where("kind = ?", WorkspaceInstanceUsageKind)
158162
if params.ExcludeDrafts {
159163
db = db.Where("draft = ?", false)
@@ -179,6 +183,7 @@ func FindUsage(ctx context.Context, conn *gorm.DB, params *FindUsageParams) ([]U
179183

180184
type GetUsageSummaryParams struct {
181185
AttributionId AttributionID
186+
UserID uuid.UUID
182187
From, To time.Time
183188
ExcludeDrafts bool
184189
}
@@ -192,8 +197,11 @@ func GetUsageSummary(ctx context.Context, conn *gorm.DB, params GetUsageSummaryP
192197
db := conn.WithContext(ctx)
193198
query1 := db.Table((&Usage{}).TableName()).
194199
Select("sum(creditCents) as CreditCentsUsed, count(*) as NumberOfRecords").
195-
Where("attributionId = ?", params.AttributionId).
196-
Where("effectiveTime >= ? AND effectiveTime < ?", TimeToISO8601(params.From), TimeToISO8601(params.To)).
200+
Where("attributionId = ?", params.AttributionId)
201+
if params.UserID != uuid.Nil {
202+
query1 = query1.Where("metadata->>'$.userId' = ?", params.UserID.String())
203+
}
204+
query1 = query1.Where("effectiveTime >= ? AND effectiveTime < ?", TimeToISO8601(params.From), TimeToISO8601(params.To)).
197205
Where("kind = ?", WorkspaceInstanceUsageKind)
198206
if params.ExcludeDrafts {
199207
query1 = query1.Where("draft = ?", false)

components/gitpod-db/go/usage_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,60 @@ func TestFindUsageInRange(t *testing.T) {
5454
require.Equal(t, []db.Usage{entryInside}, listResult)
5555
}
5656

57+
func TestFindUsageInRangeByUser(t *testing.T) {
58+
conn := dbtest.ConnectForTests(t)
59+
60+
start := time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC)
61+
end := time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC)
62+
userID := uuid.New()
63+
64+
attributionID := db.NewTeamAttributionID(uuid.New().String())
65+
66+
entryBefore := dbtest.NewUsage(t, withUserId(userID, db.Usage{
67+
AttributionID: attributionID,
68+
EffectiveTime: db.NewVarCharTime(start.Add(-1 * 23 * time.Hour)),
69+
Draft: true,
70+
}))
71+
72+
entryInside := dbtest.NewUsage(t, withUserId(userID, db.Usage{
73+
AttributionID: attributionID,
74+
EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Minute)),
75+
}))
76+
77+
entryInsideOtherUser := dbtest.NewUsage(t, withUserId(uuid.New(), db.Usage{
78+
AttributionID: attributionID,
79+
EffectiveTime: db.NewVarCharTime(start.Add(2 * time.Minute)),
80+
}))
81+
82+
entryAfter := dbtest.NewUsage(t, withUserId(userID, db.Usage{
83+
AttributionID: attributionID,
84+
EffectiveTime: db.NewVarCharTime(end.Add(2 * time.Hour)),
85+
}))
86+
87+
usageEntries := []db.Usage{entryBefore, entryInside, entryInsideOtherUser, entryAfter}
88+
dbtest.CreateUsageRecords(t, conn, usageEntries...)
89+
listResult, err := db.FindUsage(context.Background(), conn, &db.FindUsageParams{
90+
AttributionId: attributionID,
91+
UserID: userID,
92+
From: start,
93+
To: end,
94+
})
95+
require.NoError(t, err)
96+
97+
require.Equal(t, 1, len(listResult))
98+
require.Equal(t, entryInside.ID, listResult[0].ID)
99+
100+
summary, err := db.GetUsageSummary(context.Background(), conn, db.GetUsageSummaryParams{
101+
AttributionId: attributionID,
102+
UserID: userID,
103+
From: start,
104+
To: end,
105+
})
106+
require.NoError(t, err)
107+
require.Equal(t, entryInside.CreditCents, summary.CreditCentsUsed)
108+
109+
}
110+
57111
func TestGetUsageSummary(t *testing.T) {
58112
conn := dbtest.ConnectForTests(t)
59113

@@ -335,6 +389,13 @@ func TestListBalance(t *testing.T) {
335389
})
336390
}
337391

392+
func withUserId(id uuid.UUID, usage db.Usage) db.Usage {
393+
usage.SetCreditNoteMetaData(db.CreditNoteMetaData{
394+
UserID: id.String(),
395+
})
396+
return usage
397+
}
398+
338399
func TestGetBalance(t *testing.T) {
339400
teamAttributionID := db.NewTeamAttributionID(uuid.New().String())
340401
teamAttributionID2 := db.NewTeamAttributionID(uuid.New().String())

components/gitpod-protocol/src/usage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66

77
import { WorkspaceType } from "./protocol";
88

9-
// types below are copied over from components/usage-api/typescript/src/usage/v1/usage_pb.d.ts
9+
// types below are manually kept in sycn with components/usage-api/typescript/src/usage/v1/usage_pb.d.ts
1010
export interface ListUsageRequest {
1111
attributionId: string;
12+
userId?: string;
1213
from?: number;
1314
to?: number;
1415
order: Ordering;

components/server/src/orgs/usage-service.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,26 @@ export class UsageService {
7070
});
7171
}
7272
const orgId = attributionId.teamId;
73-
await this.authorizer.checkPermissionOnOrganization(userId, "read_billing", orgId);
73+
// check if the user has access to why they requested
74+
let requestedUserId = req.userId;
75+
if (requestedUserId !== userId) {
76+
try {
77+
// asking for everybody's usage
78+
await this.authorizer.checkPermissionOnOrganization(userId, "read_billing", orgId);
79+
} catch (err) {
80+
if (ApplicationError.hasErrorCode(err) && err.code === ErrorCodes.PERMISSION_DENIED) {
81+
// downgrade to user's usage only
82+
requestedUserId = userId;
83+
} else {
84+
throw err;
85+
}
86+
}
87+
}
88+
await this.authorizer.checkPermissionOnOrganization(userId, "read_info", orgId);
7489

7590
const response = await this.usageService.listUsage({
7691
attributionId: AttributionId.render(attributionId),
92+
userId: requestedUserId,
7793
from: from ? new Date(from) : undefined,
7894
to: to ? new Date(to) : undefined,
7995
order: ListUsageRequest_Ordering.ORDERING_DESCENDING,

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4124,7 +4124,20 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
41244124
});
41254125
}
41264126
const user = await this.checkAndBlockUser("listUsage");
4127-
await this.guardCostCenterAccess(ctx, attributionId, "get", "not_implemented");
4127+
4128+
// we are adding this check here inline because we are moving to the new fine-grained permissions model but are not quite ready yet.
4129+
const members = await this.teamDB.findMembersByTeam(attributionId.teamId);
4130+
const member = members.find((m) => m.userId === user.id);
4131+
if (!member) {
4132+
throw new ApplicationError(ErrorCodes.NOT_FOUND, "Organization not found.");
4133+
}
4134+
const isMemberUsageEnabled = await getExperimentsClientForBackend().getValueAsync("member_usage", false, {
4135+
user: user,
4136+
teamId: attributionId.teamId,
4137+
});
4138+
if (isMemberUsageEnabled && member.role !== "owner") {
4139+
req.userId = user.id;
4140+
}
41284141
return this.usageService.listUsage(user.id, req);
41294142
}
41304143

0 commit comments

Comments
 (0)