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

Commitc7ca86d

Browse files
authored
feat: Implement RBAC checks on /templates endpoints (#1678)
* feat: Generic Filter method for rbac objects
1 parentfcd610e commitc7ca86d

File tree

11 files changed

+221
-73
lines changed

11 files changed

+221
-73
lines changed

‎coderd/authorize.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ import (
1212
"github.com/coder/coder/coderd/rbac"
1313
)
1414

15-
func (api*api)Authorize(rw http.ResponseWriter,r*http.Request,action rbac.Action,object rbac.Object)bool {
15+
funcAuthorizeFilter[O rbac.Objecter](api*api,r*http.Request,action rbac.Action,objects []O) []O {
1616
roles:=httpmw.UserRoles(r)
17-
err:=api.Authorizer.ByRoleName(r.Context(),roles.ID.String(),roles.Roles,action,object)
17+
returnrbac.Filter(r.Context(),api.Authorizer,roles.ID.String(),roles.Roles,action,objects)
18+
}
19+
20+
func (api*api)Authorize(rw http.ResponseWriter,r*http.Request,action rbac.Action,object rbac.Objecter)bool {
21+
roles:=httpmw.UserRoles(r)
22+
err:=api.Authorizer.ByRoleName(r.Context(),roles.ID.String(),roles.Roles,action,object.RBACObject())
1823
iferr!=nil {
1924
httpapi.Write(rw,http.StatusForbidden, httpapi.Response{
2025
Message:err.Error(),

‎coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ func newRouter(options *Options, a *api) chi.Router {
186186
r.Route("/templates/{template}",func(r chi.Router) {
187187
r.Use(
188188
apiKeyMiddleware,
189+
authRolesMiddleware,
189190
httpmw.ExtractTemplateParam(options.Database),
190191
)
191192

‎coderd/coderd_test.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
100100

101101
"PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize:true},
102102
"GET:/api/v2/organizations/{organization}/provisionerdaemons": {NoAuthorize:true},
103-
"POST:/api/v2/organizations/{organization}/templates": {NoAuthorize:true},
104-
"GET:/api/v2/organizations/{organization}/templates": {NoAuthorize:true},
105103
"GET:/api/v2/organizations/{organization}/templates/{templatename}": {NoAuthorize:true},
106104
"POST:/api/v2/organizations/{organization}/templateversions": {NoAuthorize:true},
107105
"POST:/api/v2/organizations/{organization}/workspaces": {NoAuthorize:true},
@@ -110,8 +108,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
110108
"GET:/api/v2/parameters/{scope}/{id}": {NoAuthorize:true},
111109
"DELETE:/api/v2/parameters/{scope}/{id}/{name}": {NoAuthorize:true},
112110

113-
"DELETE:/api/v2/templates/{template}": {NoAuthorize:true},
114-
"GET:/api/v2/templates/{template}": {NoAuthorize:true},
115111
"GET:/api/v2/templates/{template}/versions": {NoAuthorize:true},
116112
"PATCH:/api/v2/templates/{template}/versions": {NoAuthorize:true},
117113
"GET:/api/v2/templates/{template}/versions/{templateversionname}": {NoAuthorize:true},
@@ -185,7 +181,23 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
185181
AssertAction:rbac.ActionRead,
186182
AssertObject:workspaceRBACObj,
187183
},
188-
184+
"GET:/api/v2/organizations/{organization}/templates": {
185+
StatusCode:http.StatusOK,
186+
AssertAction:rbac.ActionRead,
187+
AssertObject:rbac.ResourceTemplate.InOrg(template.OrganizationID).WithID(template.ID.String()),
188+
},
189+
"POST:/api/v2/organizations/{organization}/templates": {
190+
AssertAction:rbac.ActionCreate,
191+
AssertObject:rbac.ResourceTemplate.InOrg(organization.ID),
192+
},
193+
"DELETE:/api/v2/templates/{template}": {
194+
AssertAction:rbac.ActionDelete,
195+
AssertObject:rbac.ResourceTemplate.InOrg(template.OrganizationID).WithID(template.ID.String()),
196+
},
197+
"GET:/api/v2/templates/{template}": {
198+
AssertAction:rbac.ActionRead,
199+
AssertObject:rbac.ResourceTemplate.InOrg(template.OrganizationID).WithID(template.ID.String()),
200+
},
189201
"POST:/api/v2/files": {AssertAction:rbac.ActionCreate,AssertObject:rbac.ResourceFile},
190202
"GET:/api/v2/files/{fileHash}": {AssertAction:rbac.ActionRead,
191203
AssertObject:rbac.ResourceFile.WithOwner(admin.UserID.String()).WithID(file.Hash)},
@@ -226,6 +238,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
226238
route=strings.ReplaceAll(route,"{workspacebuild}",workspace.LatestBuild.ID.String())
227239
route=strings.ReplaceAll(route,"{workspacename}",workspace.Name)
228240
route=strings.ReplaceAll(route,"{workspacebuildname}",workspace.LatestBuild.Name)
241+
route=strings.ReplaceAll(route,"{template}",template.ID.String())
229242
route=strings.ReplaceAll(route,"{hash}",file.Hash)
230243

231244
resp,err:=client.Request(context.Background(),method,route,nil)

‎coderd/database/modelmethods.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package database
2+
3+
import"github.com/coder/coder/coderd/rbac"
4+
5+
func (tTemplate)RBACObject() rbac.Object {
6+
returnrbac.ResourceTemplate.InOrg(t.OrganizationID).WithID(t.ID.String())
7+
}
8+
9+
func (wWorkspace)RBACObject() rbac.Object {
10+
returnrbac.ResourceWorkspace.InOrg(w.OrganizationID).WithID(w.ID.String()).WithOwner(w.OwnerID.String())
11+
}
12+
13+
func (mOrganizationMember)RBACObject() rbac.Object {
14+
returnrbac.ResourceOrganizationMember.InOrg(m.OrganizationID).WithID(m.UserID.String())
15+
}
16+
17+
func (oOrganization)RBACObject() rbac.Object {
18+
returnrbac.ResourceOrganization.InOrg(o.ID).WithID(o.ID.String())
19+
}

‎coderd/rbac/authz.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package rbac
33
import (
44
"context"
55
_"embed"
6-
76
"golang.org/x/xerrors"
87

98
"github.com/open-policy-agent/opa/rego"
@@ -13,6 +12,24 @@ type Authorizer interface {
1312
ByRoleName(ctx context.Context,subjectIDstring,roleNames []string,actionAction,objectObject)error
1413
}
1514

15+
// Filter takes in a list of objects, and will filter the list removing all
16+
// the elements the subject does not have permission for.
17+
// Filter does not allocate a new slice, and will use the existing one
18+
// passed in. This can cause memory leaks if the slice is held for a prolonged
19+
// period of time.
20+
funcFilter[OObjecter](ctx context.Context,authAuthorizer,subjIDstring,subjRoles []string,actionAction,objects []O) []O {
21+
filtered:=make([]O,0)
22+
23+
fori:=rangeobjects {
24+
object:=objects[i]
25+
err:=auth.ByRoleName(ctx,subjID,subjRoles,action,object.RBACObject())
26+
iferr==nil {
27+
filtered=append(filtered,object)
28+
}
29+
}
30+
returnfiltered
31+
}
32+
1633
// RegoAuthorizer will use a prepared rego query for performing authorize()
1734
typeRegoAuthorizerstruct {
1835
query rego.PreparedEvalQuery

‎coderd/rbac/authz_test.go

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"strconv"
78
"testing"
89

910
"github.com/google/uuid"
10-
11-
"golang.org/x/xerrors"
12-
1311
"github.com/stretchr/testify/require"
12+
"golang.org/x/xerrors"
1413

1514
"github.com/coder/coder/coderd/rbac"
1615
)
@@ -24,6 +23,94 @@ type subject struct {
2423
Roles []rbac.Role`json:"roles"`
2524
}
2625

26+
funcTestFilter(t*testing.T) {
27+
t.Parallel()
28+
29+
objectList:=make([]rbac.Object,0)
30+
workspaceList:=make([]rbac.Object,0)
31+
fileList:=make([]rbac.Object,0)
32+
fori:=0;i<10;i++ {
33+
idxStr:=strconv.Itoa(i)
34+
workspace:=rbac.ResourceWorkspace.WithID(idxStr).WithOwner("me")
35+
file:=rbac.ResourceFile.WithID(idxStr).WithOwner("me")
36+
37+
workspaceList=append(workspaceList,workspace)
38+
fileList=append(fileList,file)
39+
40+
objectList=append(objectList,workspace)
41+
objectList=append(objectList,file)
42+
}
43+
44+
// copyList is to prevent tests from sharing the same slice
45+
copyList:=func(list []rbac.Object) []rbac.Object {
46+
tmp:=make([]rbac.Object,len(list))
47+
copy(tmp,list)
48+
returntmp
49+
}
50+
51+
testCases:= []struct {
52+
Namestring
53+
List []rbac.Object
54+
Expected []rbac.Object
55+
Authfunc(o rbac.Object)error
56+
}{
57+
{
58+
Name:"FilterWorkspaceType",
59+
List:copyList(objectList),
60+
Expected:copyList(workspaceList),
61+
Auth:func(o rbac.Object)error {
62+
ifo.Type!=rbac.ResourceWorkspace.Type {
63+
returnxerrors.New("only workspace")
64+
}
65+
returnnil
66+
},
67+
},
68+
{
69+
Name:"FilterFileType",
70+
List:copyList(objectList),
71+
Expected:copyList(fileList),
72+
Auth:func(o rbac.Object)error {
73+
ifo.Type!=rbac.ResourceFile.Type {
74+
returnxerrors.New("only file")
75+
}
76+
returnnil
77+
},
78+
},
79+
{
80+
Name:"FilterAll",
81+
List:copyList(objectList),
82+
Expected: []rbac.Object{},
83+
Auth:func(o rbac.Object)error {
84+
returnxerrors.New("always fail")
85+
},
86+
},
87+
{
88+
Name:"FilterNone",
89+
List:copyList(objectList),
90+
Expected:copyList(objectList),
91+
Auth:func(o rbac.Object)error {
92+
returnnil
93+
},
94+
},
95+
}
96+
97+
for_,c:=rangetestCases {
98+
c:=c
99+
t.Run(c.Name,func(t*testing.T) {
100+
t.Parallel()
101+
authorizer:=fakeAuthorizer{
102+
AuthFunc:func(_ context.Context,_string,_ []string,_ rbac.Action,object rbac.Object)error {
103+
returnc.Auth(object)
104+
},
105+
}
106+
107+
filtered:=rbac.Filter(context.Background(),authorizer,"me", []string{},rbac.ActionRead,c.List)
108+
require.ElementsMatch(t,c.Expected,filtered,"expect same list")
109+
require.Equal(t,len(c.Expected),len(filtered),"same length list")
110+
})
111+
}
112+
}
113+
27114
// TestAuthorizeDomain test the very basic roles that are commonly used.
28115
funcTestAuthorizeDomain(t*testing.T) {
29116
t.Parallel()

‎coderd/rbac/fake_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package rbac_test
2+
3+
import (
4+
"context"
5+
6+
"github.com/coder/coder/coderd/rbac"
7+
)
8+
9+
typefakeAuthorizerstruct {
10+
AuthFuncfunc(ctx context.Context,subjectIDstring,roleNames []string,action rbac.Action,object rbac.Object)error
11+
}
12+
13+
func (ffakeAuthorizer)ByRoleName(ctx context.Context,subjectIDstring,roleNames []string,action rbac.Action,object rbac.Object)error {
14+
returnf.AuthFunc(ctx,subjectID,roleNames,action,object)
15+
}

‎coderd/rbac/object.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import (
66

77
constWildcardSymbol="*"
88

9+
// Objecter returns the RBAC object for itself.
10+
typeObjecterinterface {
11+
RBACObject()Object
12+
}
13+
914
// Resources are just typed objects. Making resources this way allows directly
1015
// passing them into an Authorize function and use the chaining api.
1116
var (
@@ -99,6 +104,10 @@ type Object struct {
99104
// TODO: SharedUsers?
100105
}
101106

107+
func (zObject)RBACObject()Object {
108+
returnz
109+
}
110+
102111
// All returns an object matching all resources of the same type.
103112
func (zObject)All()Object {
104113
returnObject{

‎coderd/templates.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/coder/coder/coderd/database"
1414
"github.com/coder/coder/coderd/httpapi"
1515
"github.com/coder/coder/coderd/httpmw"
16+
"github.com/coder/coder/coderd/rbac"
1617
"github.com/coder/coder/codersdk"
1718
)
1819

@@ -30,6 +31,11 @@ func (api *api) template(rw http.ResponseWriter, r *http.Request) {
3031
})
3132
return
3233
}
34+
35+
if!api.Authorize(rw,r,rbac.ActionRead,template) {
36+
return
37+
}
38+
3339
count:=uint32(0)
3440
iflen(workspaceCounts)>0 {
3541
count=uint32(workspaceCounts[0].Count)
@@ -40,6 +46,9 @@ func (api *api) template(rw http.ResponseWriter, r *http.Request) {
4046

4147
func (api*api)deleteTemplate(rw http.ResponseWriter,r*http.Request) {
4248
template:=httpmw.TemplateParam(r)
49+
if!api.Authorize(rw,r,rbac.ActionDelete,template) {
50+
return
51+
}
4352

4453
workspaces,err:=api.Database.GetWorkspacesByTemplateID(r.Context(), database.GetWorkspacesByTemplateIDParams{
4554
TemplateID:template.ID,
@@ -77,10 +86,14 @@ func (api *api) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
7786
// Create a new template in an organization.
7887
func (api*api)postTemplateByOrganization(rw http.ResponseWriter,r*http.Request) {
7988
varcreateTemplate codersdk.CreateTemplateRequest
89+
organization:=httpmw.OrganizationParam(r)
90+
if!api.Authorize(rw,r,rbac.ActionCreate,rbac.ResourceTemplate.InOrg(organization.ID)) {
91+
return
92+
}
93+
8094
if!httpapi.Read(rw,r,&createTemplate) {
8195
return
8296
}
83-
organization:=httpmw.OrganizationParam(r)
8497
_,err:=api.Database.GetTemplateByOrganizationAndName(r.Context(), database.GetTemplateByOrganizationAndNameParams{
8598
OrganizationID:organization.ID,
8699
Name:createTemplate.Name,
@@ -194,7 +207,12 @@ func (api *api) templatesByOrganization(rw http.ResponseWriter, r *http.Request)
194207
})
195208
return
196209
}
210+
211+
// Filter templates based on rbac permissions
212+
templates=AuthorizeFilter(api,r,rbac.ActionRead,templates)
213+
197214
templateIDs:=make([]uuid.UUID,0,len(templates))
215+
198216
for_,template:=rangetemplates {
199217
templateIDs=append(templateIDs,template.ID)
200218
}
@@ -233,6 +251,10 @@ func (api *api) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Re
233251
return
234252
}
235253

254+
if!api.Authorize(rw,r,rbac.ActionRead,template) {
255+
return
256+
}
257+
236258
workspaceCounts,err:=api.Database.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID})
237259
iferrors.Is(err,sql.ErrNoRows) {
238260
err=nil

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp