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

Skip to content

Commit a2fdb42

Browse files
committed
feat: Add RBAC to /workspace endpoints
1 parent 85a932b commit a2fdb42

File tree

5 files changed

+186
-30
lines changed

5 files changed

+186
-30
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)
@@ -311,6 +309,7 @@ func New(options *Options) (http.Handler, func()) {
311309
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
312310
r.Use(
313311
apiKeyMiddleware,
312+
authRolesMiddleware,
314313
httpmw.ExtractWorkspaceBuildParam(options.Database),
315314
httpmw.ExtractWorkspaceParam(options.Database),
316315
)

coderd/coderd_test.go

Lines changed: 62 additions & 18 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
}
@@ -85,13 +91,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
8591
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
8692

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

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

@@ -125,12 +125,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
125125

126126
"POST:/api/v2/users/{user}/organizations": {NoAuthorize: true},
127127

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

@@ -139,13 +133,55 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
139133
"GET:/api/v2/users/{user}/organizations": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceOrganization},
140134
"GET:/api/v2/users/{user}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
141135
"GET:/api/v2/organizations/{organization}/workspaces/{user}": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
142-
"GET:/api/v2/organizations/{organization}/workspaces/{user}/{workspace}": {
143-
AssertObject: rbac.ResourceWorkspace.InOrg(organization.ID).WithID(workspace.ID.String()).WithOwner(workspace.OwnerID.String()),
136+
"GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": {
137+
AssertAction: rbac.ActionRead,
138+
AssertObject: workspaceRBACObj,
139+
},
140+
"GET:/api/v2/organizations/{organization}/workspaces/{user}/{workspacename}": {
141+
AssertAction: rbac.ActionRead,
142+
AssertObject: workspaceRBACObj,
144143
},
145144
"GET:/api/v2/organizations/{organization}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
145+
"GET:/api/v2/workspacebuilds/{workspacebuild}": {
146+
AssertAction: rbac.ActionRead,
147+
AssertObject: workspaceRBACObj,
148+
},
149+
"GET:/api/v2/workspacebuilds/{workspacebuild}/logs": {
150+
AssertAction: rbac.ActionRead,
151+
AssertObject: workspaceRBACObj,
152+
},
153+
"GET:/api/v2/workspaces/{workspace}/builds": {
154+
AssertAction: rbac.ActionRead,
155+
AssertObject: workspaceRBACObj,
156+
},
157+
"GET:/api/v2/workspaces/{workspace}": {
158+
AssertAction: rbac.ActionRead,
159+
AssertObject: workspaceRBACObj,
160+
},
161+
"PUT:/api/v2/workspaces/{workspace}/autostart": {
162+
AssertAction: rbac.ActionUpdate,
163+
AssertObject: workspaceRBACObj,
164+
},
165+
"PUT:/api/v2/workspaces/{workspace}/autostop": {
166+
AssertAction: rbac.ActionUpdate,
167+
AssertObject: workspaceRBACObj,
168+
},
169+
"PATCH:/api/v2/workspacebuilds/{workspacebuild}/cancel": {
170+
AssertAction: rbac.ActionUpdate,
171+
AssertObject: workspaceRBACObj,
172+
},
173+
"GET:/api/v2/workspacebuilds/{workspacebuild}/resources": {
174+
AssertAction: rbac.ActionRead,
175+
AssertObject: workspaceRBACObj,
176+
},
177+
"GET:/api/v2/workspacebuilds/{workspacebuild}/state": {
178+
AssertAction: rbac.ActionRead,
179+
AssertObject: workspaceRBACObj,
180+
},
146181

147-
// These endpoints need payloads to get to the auth part.
148-
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
182+
// These endpoints need payloads to get to the auth part. Payloads will be required
183+
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
184+
"POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
149185
}
150186

151187
c, _ := srv.Config.Handler.(*chi.Mux)
@@ -166,16 +202,24 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
166202
route = strings.ReplaceAll(route, "{organization}", admin.OrganizationID.String())
167203
route = strings.ReplaceAll(route, "{user}", admin.UserID.String())
168204
route = strings.ReplaceAll(route, "{organizationname}", organization.Name)
169-
route = strings.ReplaceAll(route, "{workspace}", workspace.Name)
205+
route = strings.ReplaceAll(route, "{workspace}", workspace.ID.String())
206+
route = strings.ReplaceAll(route, "{workspacebuild}", workspace.LatestBuild.ID.String())
207+
route = strings.ReplaceAll(route, "{workspacename}", workspace.Name)
208+
route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name)
170209

171210
resp, err := client.Request(context.Background(), method, route, nil)
172211
require.NoError(t, err, "do req")
212+
body, _ := io.ReadAll(resp.Body)
213+
t.Logf("Response Body: %q", string(body))
173214
_ = resp.Body.Close()
174215

175216
if !routeAssertions.NoAuthorize {
176217
assert.NotNil(t, authorizer.Called, "authorizer expected")
177218
assert.Equal(t, routeAssertions.StatusCode, resp.StatusCode, "expect unauthorized")
178219
if authorizer.Called != nil {
220+
if routeAssertions.AssertAction != "" {
221+
assert.Equal(t, routeAssertions.AssertAction, authorizer.Called.Action, "resource action")
222+
}
179223
if routeAssertions.AssertObject.Type != "" {
180224
assert.Equal(t, routeAssertions.AssertObject.Type, authorizer.Called.Object.Type, "resource type")
181225
}

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/workspacebuilds.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"fmt"
88
"net/http"
99

10+
"github.com/coder/coder/coderd/rbac"
11+
1012
"github.com/go-chi/chi/v5"
1113
"github.com/google/uuid"
1214
"github.com/moby/moby/pkg/namesgenerator"
@@ -20,6 +22,19 @@ import (
2022

2123
func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
2224
workspaceBuild := httpmw.WorkspaceBuildParam(r)
25+
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
26+
if err != nil {
27+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
28+
Message: "no workspace exists for this job",
29+
})
30+
return
31+
}
32+
33+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
34+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
35+
return
36+
}
37+
2338
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
2439
if err != nil {
2540
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -34,6 +49,11 @@ func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
3449
func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
3550
workspace := httpmw.WorkspaceParam(r)
3651

52+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
53+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
54+
return
55+
}
56+
3757
builds, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
3858
if xerrors.Is(err, sql.ErrNoRows) {
3959
err = nil
@@ -80,6 +100,11 @@ func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
80100

81101
func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) {
82102
workspace := httpmw.WorkspaceParam(r)
103+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
104+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
105+
return
106+
}
107+
83108
workspaceBuildName := chi.URLParam(r, "workspacebuildname")
84109
workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{
85110
WorkspaceID: workspace.ID,
@@ -111,10 +136,30 @@ func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) {
111136
func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
112137
apiKey := httpmw.APIKey(r)
113138
workspace := httpmw.WorkspaceParam(r)
139+
114140
var createBuild codersdk.CreateWorkspaceBuildRequest
115141
if !httpapi.Read(rw, r, &createBuild) {
116142
return
117143
}
144+
145+
// Rbac action depends on the transition
146+
var action rbac.Action
147+
switch createBuild.Transition {
148+
case database.WorkspaceTransitionDelete:
149+
action = rbac.ActionDelete
150+
case database.WorkspaceTransitionStart, database.WorkspaceTransitionStop:
151+
action = rbac.ActionUpdate
152+
default:
153+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
154+
Message: fmt.Sprintf("transition not supported: %q", createBuild.Transition),
155+
})
156+
return
157+
}
158+
if !api.Authorize(rw, r, action, rbac.ResourceWorkspace.
159+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
160+
return
161+
}
162+
118163
if createBuild.TemplateVersionID == uuid.Nil {
119164
latestBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
120165
if err != nil {
@@ -278,6 +323,19 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
278323

279324
func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
280325
workspaceBuild := httpmw.WorkspaceBuildParam(r)
326+
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
327+
if err != nil {
328+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
329+
Message: "no workspace exists for this job",
330+
})
331+
return
332+
}
333+
334+
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace.
335+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
336+
return
337+
}
338+
281339
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
282340
if err != nil {
283341
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -317,6 +375,19 @@ func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
317375

318376
func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) {
319377
workspaceBuild := httpmw.WorkspaceBuildParam(r)
378+
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
379+
if err != nil {
380+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
381+
Message: "no workspace exists for this job",
382+
})
383+
return
384+
}
385+
386+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
387+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
388+
return
389+
}
390+
320391
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
321392
if err != nil {
322393
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -329,6 +400,19 @@ func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request)
329400

330401
func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
331402
workspaceBuild := httpmw.WorkspaceBuildParam(r)
403+
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
404+
if err != nil {
405+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
406+
Message: "no workspace exists for this job",
407+
})
408+
return
409+
}
410+
411+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
412+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
413+
return
414+
}
415+
332416
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
333417
if err != nil {
334418
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -339,8 +423,20 @@ func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
339423
api.provisionerJobLogs(rw, r, job)
340424
}
341425

342-
func (*api) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
426+
func (api *api) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
343427
workspaceBuild := httpmw.WorkspaceBuildParam(r)
428+
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
429+
if err != nil {
430+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
431+
Message: "no workspace exists for this job",
432+
})
433+
return
434+
}
435+
436+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
437+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
438+
return
439+
}
344440

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

0 commit comments

Comments
 (0)