Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitc034e83

Browse files
authored
feat: Add RBAC to /workspace endpoints (#1566)
* feat: Add RBAC to /workspace endpoints
1 parentf3fe2a0 commitc034e83

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
typerouteCheckstruct {
5762
NoAuthorizebool
63+
AssertAction rbac.Action
5864
AssertObject rbac.Object
5965
StatusCodeint
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
fork,v:=rangeassertRoute {
@@ -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
ifauthorizer.Called!=nil {
236+
ifrouteAssertions.AssertAction!="" {
237+
assert.Equal(t,routeAssertions.AssertAction,authorizer.Called.Action,"resource action")
238+
}
188239
ifrouteAssertions.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
iferr==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+
iferr!=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
iferr!=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+
varaction rbac.Action
155+
switchcreateBuild.Transition {
156+
casedatabase.WorkspaceTransitionDelete:
157+
action=rbac.ActionDelete
158+
casedatabase.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
ifcreateBuild.TemplateVersionID==uuid.Nil {
129172
latestBuild,err:=api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(),workspace.ID)
130173
iferr!=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+
iferr!=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
iferr!=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+
iferr!=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
iferr!=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+
iferr!=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
iferr!=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+
iferr!=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)

[8]ページ先頭

©2009-2025 Movatter.jp