Thanks to visit codestin.com
Credit goes to github.com

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)