Skip to content

Commit 544ee9b

Browse files
Emyrkkylecarbs
authored andcommitted
feat: Add RBAC to /workspace endpoints (#1566)
* feat: Add RBAC to /workspace endpoints
1 parent 1abdb78 commit 544ee9b

File tree

7 files changed

+215
-35
lines changed

7 files changed

+215
-35
lines changed

coderd/coderd.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func New(options *Options) (http.Handler, func()) {
149149
r.Get("/", api.workspacesByOrganization)
150150
r.Route("/{user}", func(r chi.Router) {
151151
r.Use(httpmw.ExtractUserParam(options.Database))
152-
r.Get("/{workspace}", api.workspaceByOwnerAndName)
152+
r.Get("/{workspacename}", api.workspaceByOwnerAndName)
153153
r.Get("/", api.workspacesByOwner)
154154
})
155155
})
@@ -237,8 +237,6 @@ func New(options *Options) (http.Handler, func()) {
237237
r.Route("/password", func(r chi.Router) {
238238
r.Put("/", api.putUserPassword)
239239
})
240-
r.Get("/organizations", api.organizationsByUser)
241-
r.Post("/organizations", api.postOrganizationsByUser)
242240
// These roles apply to the site wide permissions.
243241
r.Put("/roles", api.putUserRoles)
244242
r.Get("/roles", api.userRoles)
@@ -316,6 +314,7 @@ func New(options *Options) (http.Handler, func()) {
316314
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
317315
r.Use(
318316
apiKeyMiddleware,
317+
authRolesMiddleware,
319318
httpmw.ExtractWorkspaceBuildParam(options.Database),
320319
httpmw.ExtractWorkspaceParam(options.Database),
321320
)

coderd/coderd_test.go

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package coderd_test
22

33
import (
44
"context"
5+
"io"
56
"net/http"
67
"strings"
78
"testing"
@@ -48,13 +49,18 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
4849
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
4950
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
5051
workspace := coderdtest.CreateWorkspace(t, client, admin.OrganizationID, template.ID)
52+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
5153

5254
// Always fail auth from this point forward
5355
authorizer.AlwaysReturn = rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil)
5456

57+
// Some quick reused objects
58+
workspaceRBACObj := rbac.ResourceWorkspace.InOrg(organization.ID).WithID(workspace.ID.String()).WithOwner(workspace.OwnerID.String())
59+
5560
// skipRoutes allows skipping routes from being checked.
5661
type routeCheck struct {
5762
NoAuthorize bool
63+
AssertAction rbac.Action
5864
AssertObject rbac.Object
5965
StatusCode int
6066
}
@@ -84,13 +90,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
8490
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
8591

8692
// TODO: @emyrk these need to be fixed by adding authorize calls
87-
"GET:/api/v2/workspaceresources/{workspaceresource}": {NoAuthorize: true},
88-
"GET:/api/v2/workspacebuilds/{workspacebuild}": {NoAuthorize: true},
89-
"GET:/api/v2/workspacebuilds/{workspacebuild}/logs": {NoAuthorize: true},
90-
"GET:/api/v2/workspacebuilds/{workspacebuild}/resources": {NoAuthorize: true},
91-
"GET:/api/v2/workspacebuilds/{workspacebuild}/state": {NoAuthorize: true},
92-
"PATCH:/api/v2/workspacebuilds/{workspacebuild}/cancel": {NoAuthorize: true},
93-
"GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": {NoAuthorize: true},
93+
"GET:/api/v2/workspaceresources/{workspaceresource}": {NoAuthorize: true},
9494

9595
"GET:/api/v2/users/oauth2/github/callback": {NoAuthorize: true},
9696

@@ -123,15 +123,9 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
123123

124124
"POST:/api/v2/users/{user}/organizations": {NoAuthorize: true},
125125

126-
"GET:/api/v2/workspaces/{workspace}": {NoAuthorize: true},
127-
"PUT:/api/v2/workspaces/{workspace}/autostart": {NoAuthorize: true},
128-
"PUT:/api/v2/workspaces/{workspace}/autostop": {NoAuthorize: true},
129-
"GET:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true},
130-
"POST:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true},
131-
"GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true},
132-
133-
"POST:/api/v2/files": {NoAuthorize: true},
134-
"GET:/api/v2/files/{hash}": {NoAuthorize: true},
126+
"POST:/api/v2/files": {NoAuthorize: true},
127+
"GET:/api/v2/files/{hash}": {NoAuthorize: true},
128+
"GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true},
135129

136130
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
137131
"GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.InOrg(admin.OrganizationID)},
@@ -141,11 +135,60 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
141135
"GET:/api/v2/organizations/{organization}/workspaces/{user}/{workspace}": {
142136
AssertObject: rbac.ResourceWorkspace.InOrg(organization.ID).WithID(workspace.ID.String()).WithOwner(workspace.OwnerID.String()),
143137
},
138+
"GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": {
139+
AssertAction: rbac.ActionRead,
140+
AssertObject: workspaceRBACObj,
141+
},
142+
"GET:/api/v2/organizations/{organization}/workspaces/{user}/{workspacename}": {
143+
AssertAction: rbac.ActionRead,
144+
AssertObject: workspaceRBACObj,
145+
},
144146
"GET:/api/v2/organizations/{organization}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
145-
"GET:/api/v2/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
147+
"GET:/api/v2/workspacebuilds/{workspacebuild}": {
148+
AssertAction: rbac.ActionRead,
149+
AssertObject: workspaceRBACObj,
150+
},
151+
"GET:/api/v2/workspacebuilds/{workspacebuild}/logs": {
152+
AssertAction: rbac.ActionRead,
153+
AssertObject: workspaceRBACObj,
154+
},
155+
"GET:/api/v2/workspaces/{workspace}/builds": {
156+
AssertAction: rbac.ActionRead,
157+
AssertObject: workspaceRBACObj,
158+
},
159+
"GET:/api/v2/workspaces/{workspace}": {
160+
AssertAction: rbac.ActionRead,
161+
AssertObject: workspaceRBACObj,
162+
},
163+
"PUT:/api/v2/workspaces/{workspace}/autostart": {
164+
AssertAction: rbac.ActionUpdate,
165+
AssertObject: workspaceRBACObj,
166+
},
167+
"PUT:/api/v2/workspaces/{workspace}/autostop": {
168+
AssertAction: rbac.ActionUpdate,
169+
AssertObject: workspaceRBACObj,
170+
},
171+
"PATCH:/api/v2/workspacebuilds/{workspacebuild}/cancel": {
172+
AssertAction: rbac.ActionUpdate,
173+
AssertObject: workspaceRBACObj,
174+
},
175+
"GET:/api/v2/workspacebuilds/{workspacebuild}/resources": {
176+
AssertAction: rbac.ActionRead,
177+
AssertObject: workspaceRBACObj,
178+
},
179+
"GET:/api/v2/workspacebuilds/{workspacebuild}/state": {
180+
AssertAction: rbac.ActionRead,
181+
AssertObject: workspaceRBACObj,
182+
},
183+
"GET:/api/v2/workspaces/": {
184+
StatusCode: http.StatusOK,
185+
AssertAction: rbac.ActionRead,
186+
AssertObject: workspaceRBACObj,
187+
},
146188

147-
// These endpoints need payloads to get to the auth part.
148-
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
189+
// These endpoints need payloads to get to the auth part. Payloads will be required
190+
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
191+
"POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
149192
}
150193

151194
for k, v := range assertRoute {
@@ -175,16 +218,24 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
175218
route = strings.ReplaceAll(route, "{organization}", admin.OrganizationID.String())
176219
route = strings.ReplaceAll(route, "{user}", admin.UserID.String())
177220
route = strings.ReplaceAll(route, "{organizationname}", organization.Name)
178-
route = strings.ReplaceAll(route, "{workspace}", workspace.Name)
221+
route = strings.ReplaceAll(route, "{workspace}", workspace.ID.String())
222+
route = strings.ReplaceAll(route, "{workspacebuild}", workspace.LatestBuild.ID.String())
223+
route = strings.ReplaceAll(route, "{workspacename}", workspace.Name)
224+
route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name)
179225

180226
resp, err := client.Request(context.Background(), method, route, nil)
181227
require.NoError(t, err, "do req")
228+
body, _ := io.ReadAll(resp.Body)
229+
t.Logf("Response Body: %q", string(body))
182230
_ = resp.Body.Close()
183231

184232
if !routeAssertions.NoAuthorize {
185233
assert.NotNil(t, authorizer.Called, "authorizer expected")
186234
assert.Equal(t, routeAssertions.StatusCode, resp.StatusCode, "expect unauthorized")
187235
if authorizer.Called != nil {
236+
if routeAssertions.AssertAction != "" {
237+
assert.Equal(t, routeAssertions.AssertAction, authorizer.Called.Action, "resource action")
238+
}
188239
if routeAssertions.AssertObject.Type != "" {
189240
assert.Equal(t, routeAssertions.AssertObject.Type, authorizer.Called.Object.Type, "resource type")
190241
}

coderd/users.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,14 @@ func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request)
568568
if !httpapi.Read(rw, r, &req) {
569569
return
570570
}
571+
572+
// Create organization uses the organization resource without an OrgID.
573+
// This means you need the site wide permission to make a new organization.
574+
if !api.Authorize(rw, r, rbac.ActionCreate,
575+
rbac.ResourceOrganization) {
576+
return
577+
}
578+
571579
_, err := api.Database.GetOrganizationByName(r.Context(), req.Name)
572580
if err == nil {
573581
httpapi.Write(rw, http.StatusConflict, httpapi.Response{

coderd/users_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func TestPostUsers(t *testing.T) {
173173
client := coderdtest.New(t, nil)
174174
first := coderdtest.CreateFirstUser(t, client)
175175
notInOrg := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
176-
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
176+
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleAdmin(), rbac.RoleMember())
177177
org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
178178
Name: "another",
179179
})

coderd/workspacebuilds.go

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,25 @@ import (
1515
"github.com/coder/coder/coderd/database"
1616
"github.com/coder/coder/coderd/httpapi"
1717
"github.com/coder/coder/coderd/httpmw"
18+
"github.com/coder/coder/coderd/rbac"
1819
"github.com/coder/coder/codersdk"
1920
)
2021

2122
func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
2223
workspaceBuild := httpmw.WorkspaceBuildParam(r)
24+
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
25+
if err != nil {
26+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
27+
Message: "no workspace exists for this job",
28+
})
29+
return
30+
}
31+
32+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
33+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
34+
return
35+
}
36+
2337
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
2438
if err != nil {
2539
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -34,6 +48,11 @@ func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
3448
func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
3549
workspace := httpmw.WorkspaceParam(r)
3650

51+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
52+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
53+
return
54+
}
55+
3756
paginationParams, ok := parsePagination(rw, r)
3857
if !ok {
3958
return
@@ -90,6 +109,11 @@ func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
90109

91110
func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) {
92111
workspace := httpmw.WorkspaceParam(r)
112+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
113+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
114+
return
115+
}
116+
93117
workspaceBuildName := chi.URLParam(r, "workspacebuildname")
94118
workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{
95119
WorkspaceID: workspace.ID,
@@ -125,6 +149,25 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
125149
if !httpapi.Read(rw, r, &createBuild) {
126150
return
127151
}
152+
153+
// Rbac action depends on the transition
154+
var action rbac.Action
155+
switch createBuild.Transition {
156+
case database.WorkspaceTransitionDelete:
157+
action = rbac.ActionDelete
158+
case database.WorkspaceTransitionStart, database.WorkspaceTransitionStop:
159+
action = rbac.ActionUpdate
160+
default:
161+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
162+
Message: fmt.Sprintf("transition not supported: %q", createBuild.Transition),
163+
})
164+
return
165+
}
166+
if !api.Authorize(rw, r, action, rbac.ResourceWorkspace.
167+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
168+
return
169+
}
170+
128171
if createBuild.TemplateVersionID == uuid.Nil {
129172
latestBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
130173
if err != nil {
@@ -269,6 +312,19 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
269312

270313
func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
271314
workspaceBuild := httpmw.WorkspaceBuildParam(r)
315+
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
316+
if err != nil {
317+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
318+
Message: "no workspace exists for this job",
319+
})
320+
return
321+
}
322+
323+
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace.
324+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
325+
return
326+
}
327+
272328
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
273329
if err != nil {
274330
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -308,6 +364,19 @@ func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
308364

309365
func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) {
310366
workspaceBuild := httpmw.WorkspaceBuildParam(r)
367+
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
368+
if err != nil {
369+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
370+
Message: "no workspace exists for this job",
371+
})
372+
return
373+
}
374+
375+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
376+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
377+
return
378+
}
379+
311380
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
312381
if err != nil {
313382
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -320,6 +389,19 @@ func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request)
320389

321390
func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
322391
workspaceBuild := httpmw.WorkspaceBuildParam(r)
392+
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
393+
if err != nil {
394+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
395+
Message: "no workspace exists for this job",
396+
})
397+
return
398+
}
399+
400+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
401+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
402+
return
403+
}
404+
323405
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
324406
if err != nil {
325407
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -330,8 +412,20 @@ func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
330412
api.provisionerJobLogs(rw, r, job)
331413
}
332414

333-
func (*api) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
415+
func (api *api) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
334416
workspaceBuild := httpmw.WorkspaceBuildParam(r)
417+
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
418+
if err != nil {
419+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
420+
Message: "no workspace exists for this job",
421+
})
422+
return
423+
}
424+
425+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
426+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
427+
return
428+
}
335429

336430
rw.Header().Set("Content-Type", "application/json")
337431
rw.WriteHeader(http.StatusOK)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy