Skip to content

Commit 7f0eca5

Browse files
committed
chore: scope workspace quotas to organizations
Quotas are now a function of (user_id, organization_id). They are still sourced from groups. Deprecate the old api endpoint.
1 parent c3ef7dc commit 7f0eca5

File tree

8 files changed

+98
-35
lines changed

8 files changed

+98
-35
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,20 +1814,20 @@ func (q *querier) GetProvisionerLogsAfterID(ctx context.Context, arg database.Ge
18141814
return q.db.GetProvisionerLogsAfterID(ctx, arg)
18151815
}
18161816

1817-
func (q *querier) GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error) {
1818-
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUserObject(userID))
1817+
func (q *querier) GetQuotaAllowanceForUser(ctx context.Context, params database.GetQuotaAllowanceForUserParams) (int64, error) {
1818+
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUserObject(params.UserID))
18191819
if err != nil {
18201820
return -1, err
18211821
}
1822-
return q.db.GetQuotaAllowanceForUser(ctx, userID)
1822+
return q.db.GetQuotaAllowanceForUser(ctx, params)
18231823
}
18241824

1825-
func (q *querier) GetQuotaConsumedForUser(ctx context.Context, userID uuid.UUID) (int64, error) {
1826-
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUserObject(userID))
1825+
func (q *querier) GetQuotaConsumedForUser(ctx context.Context, params database.GetQuotaConsumedForUserParams) (int64, error) {
1826+
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUserObject(params.OwnerID))
18271827
if err != nil {
18281828
return -1, err
18291829
}
1830-
return q.db.GetQuotaConsumedForUser(ctx, userID)
1830+
return q.db.GetQuotaConsumedForUser(ctx, params)
18311831
}
18321832

18331833
func (q *querier) GetReplicaByID(ctx context.Context, id uuid.UUID) (database.Replica, error) {

coderd/database/dbmem/dbmem.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3304,13 +3304,13 @@ func (q *FakeQuerier) GetProvisionerLogsAfterID(_ context.Context, arg database.
33043304
return logs, nil
33053305
}
33063306

3307-
func (q *FakeQuerier) GetQuotaAllowanceForUser(_ context.Context, userID uuid.UUID) (int64, error) {
3307+
func (q *FakeQuerier) GetQuotaAllowanceForUser(_ context.Context, params database.GetQuotaAllowanceForUserParams) (int64, error) {
33083308
q.mutex.RLock()
33093309
defer q.mutex.RUnlock()
33103310

33113311
var sum int64
33123312
for _, member := range q.groupMembers {
3313-
if member.UserID != userID {
3313+
if member.UserID != params.UserID {
33143314
continue
33153315
}
33163316
if _, err := q.getOrganizationByIDNoLock(member.GroupID); err == nil {
@@ -3330,27 +3330,33 @@ func (q *FakeQuerier) GetQuotaAllowanceForUser(_ context.Context, userID uuid.UU
33303330
// Grab the quota for the Everyone group iff the user is a member of
33313331
// said organization.
33323332
for _, mem := range q.organizationMembers {
3333-
if mem.UserID != userID {
3333+
if mem.UserID != params.UserID {
33343334
continue
33353335
}
33363336

33373337
group, err := q.getGroupByIDNoLock(context.Background(), mem.OrganizationID)
33383338
if err != nil {
33393339
return -1, xerrors.Errorf("failed to get everyone group for org %q", mem.OrganizationID.String())
33403340
}
3341+
if group.OrganizationID != params.OrganizationID {
3342+
continue
3343+
}
33413344
sum += int64(group.QuotaAllowance)
33423345
}
33433346

33443347
return sum, nil
33453348
}
33463349

3347-
func (q *FakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUID) (int64, error) {
3350+
func (q *FakeQuerier) GetQuotaConsumedForUser(_ context.Context, params database.GetQuotaConsumedForUserParams) (int64, error) {
33483351
q.mutex.RLock()
33493352
defer q.mutex.RUnlock()
33503353

33513354
var sum int64
33523355
for _, workspace := range q.workspaces {
3353-
if workspace.OwnerID != userID {
3356+
if workspace.OwnerID != params.OwnerID {
3357+
continue
3358+
}
3359+
if workspace.OrganizationID != params.OrganizationID {
33543360
continue
33553361
}
33563362
if workspace.Deleted {

coderd/database/dbmetrics/dbmetrics.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 22 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/quotas.sql

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ FROM
55
(
66
-- Select all groups this user is a member of. This will also include
77
-- the "Everyone" group for organizations the user is a member of.
8-
SELECT * FROM group_members_expanded WHERE @user_id = user_id
8+
SELECT * FROM group_members_expanded
9+
WHERE
10+
@user_id = user_id AND
11+
@organization_id = group_members_expanded.organization_id
912
) AS members
1013
INNER JOIN groups ON
1114
members.group_id = groups.id
@@ -30,4 +33,8 @@ FROM
3033
workspaces
3134
JOIN latest_builds ON
3235
latest_builds.workspace_id = workspaces.id
33-
WHERE NOT deleted AND workspaces.owner_id = $1;
36+
WHERE NOT
37+
deleted AND
38+
workspaces.owner_id = @owner_id AND
39+
workspaces.organization_id = @organization_id
40+
;

enterprise/coderd/workspacequota.go

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import (
66
"errors"
77
"net/http"
88

9+
"github.com/go-chi/chi/v5"
910
"github.com/google/uuid"
1011

1112
"cdr.dev/slog"
1213

1314
"github.com/coder/coder/v2/coderd/database"
1415
"github.com/coder/coder/v2/coderd/httpapi"
1516
"github.com/coder/coder/v2/coderd/httpmw"
16-
"github.com/coder/coder/v2/coderd/rbac/policy"
1717
"github.com/coder/coder/v2/codersdk"
1818
"github.com/coder/coder/v2/provisionerd/proto"
1919
)
@@ -48,12 +48,18 @@ func (c *committer) CommitQuota(
4848
)
4949
err = c.Database.InTx(func(s database.Store) error {
5050
var err error
51-
consumed, err = s.GetQuotaConsumedForUser(ctx, workspace.OwnerID)
51+
consumed, err = s.GetQuotaConsumedForUser(ctx, database.GetQuotaConsumedForUserParams{
52+
OwnerID: workspace.OwnerID,
53+
OrganizationID: workspace.OrganizationID,
54+
})
5255
if err != nil {
5356
return err
5457
}
5558

56-
budget, err = s.GetQuotaAllowanceForUser(ctx, workspace.OwnerID)
59+
budget, err = s.GetQuotaAllowanceForUser(ctx, database.GetQuotaAllowanceForUserParams{
60+
UserID: workspace.OwnerID,
61+
OrganizationID: workspace.OrganizationID,
62+
})
5763
if err != nil {
5864
return err
5965
}
@@ -120,13 +126,35 @@ func (c *committer) CommitQuota(
120126
// @Param user path string true "User ID, name, or me"
121127
// @Success 200 {object} codersdk.WorkspaceQuota
122128
// @Router /workspace-quota/{user} [get]
123-
func (api *API) workspaceQuota(rw http.ResponseWriter, r *http.Request) {
124-
user := httpmw.UserParam(r)
125-
126-
if !api.AGPL.Authorize(r, policy.ActionRead, user) {
127-
httpapi.ResourceNotFound(rw)
129+
// @Deprecated this endpoint will be removed,
130+
// use /organizations/{organization}/members/{user}/workspace-quota instead
131+
func (api *API) workspaceQuotaByUser(rw http.ResponseWriter, r *http.Request) {
132+
defaultOrg, err := api.Database.GetDefaultOrganization(r.Context())
133+
if err != nil {
134+
httpapi.InternalServerError(rw, err)
128135
return
129136
}
137+
chi.RouteContext(r.Context()).URLParams.Add("organization", defaultOrg.ID.String())
138+
139+
// defer to the new endpoint
140+
api.workspaceQuota(rw, r)
141+
}
142+
143+
// @Summary Get workspace quota by user
144+
// @ID get-workspace-quota-by-user
145+
// @Security CoderSessionToken
146+
// @Produce json
147+
// @Tags Enterprise
148+
// @Param user path string true "User ID, name, or me"
149+
// @Success 200 {object} codersdk.WorkspaceQuota
150+
// @Router /organizations/{organization}/members/{user}/workspace-quota [get]
151+
func (api *API) workspaceQuota(rw http.ResponseWriter, r *http.Request) {
152+
var (
153+
organization = httpmw.OrganizationParam(r)
154+
member = httpmw.OrganizationMemberParam(r)
155+
)
156+
157+
//defaultOrg, _ := api.Database.GetDefaultOrganization(r.Context())
130158

131159
api.entitlementsMu.RLock()
132160
licensed := api.entitlements.Features[codersdk.FeatureTemplateRBAC].Enabled
@@ -136,7 +164,10 @@ func (api *API) workspaceQuota(rw http.ResponseWriter, r *http.Request) {
136164
var quotaAllowance int64 = -1
137165
if licensed {
138166
var err error
139-
quotaAllowance, err = api.Database.GetQuotaAllowanceForUser(r.Context(), user.ID)
167+
quotaAllowance, err = api.Database.GetQuotaAllowanceForUser(r.Context(), database.GetQuotaAllowanceForUserParams{
168+
UserID: member.UserID,
169+
OrganizationID: organization.ID,
170+
})
140171
if err != nil {
141172
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
142173
Message: "Failed to get allowance",
@@ -146,7 +177,10 @@ func (api *API) workspaceQuota(rw http.ResponseWriter, r *http.Request) {
146177
}
147178
}
148179

149-
quotaConsumed, err := api.Database.GetQuotaConsumedForUser(r.Context(), user.ID)
180+
quotaConsumed, err := api.Database.GetQuotaConsumedForUser(r.Context(), database.GetQuotaConsumedForUserParams{
181+
OwnerID: member.UserID,
182+
OrganizationID: organization.ID,
183+
})
150184
if err != nil {
151185
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
152186
Message: "Failed to get consumed",

0 commit comments

Comments
 (0)