@@ -2,14 +2,19 @@ package coderd_test
2
2
3
3
import (
4
4
"context"
5
+ "net/http"
6
+ "strings"
5
7
"testing"
6
8
7
- "go.uber.org/goleak "
8
-
9
+ "github.com/go-chi/chi/v5 "
10
+ "github.com/stretchr/testify/assert"
9
11
"github.com/stretchr/testify/require"
12
+ "go.uber.org/goleak"
13
+ "golang.org/x/xerrors"
10
14
11
15
"github.com/coder/coder/buildinfo"
12
16
"github.com/coder/coder/coderd/coderdtest"
17
+ "github.com/coder/coder/coderd/rbac"
13
18
)
14
19
15
20
func TestMain (m * testing.M ) {
@@ -24,3 +29,197 @@ func TestBuildInfo(t *testing.T) {
24
29
require .Equal (t , buildinfo .ExternalURL (), buildInfo .ExternalURL , "external URL" )
25
30
require .Equal (t , buildinfo .Version (), buildInfo .Version , "version" )
26
31
}
32
+
33
+ // TestAuthorizeAllEndpoints will check `authorize` is called on every endpoint registered.
34
+ func TestAuthorizeAllEndpoints (t * testing.T ) {
35
+ t .Parallel ()
36
+
37
+ authorizer := & fakeAuthorizer {}
38
+ srv , client := coderdtest .NewMemoryCoderd (t , & coderdtest.Options {
39
+ Authorizer : authorizer ,
40
+ })
41
+ admin := coderdtest .CreateFirstUser (t , client )
42
+ organization , err := client .Organization (context .Background (), admin .OrganizationID )
43
+ require .NoError (t , err , "fetch org" )
44
+
45
+ // Setup some data in the database.
46
+ coderdtest .NewProvisionerDaemon (t , client )
47
+ version := coderdtest .CreateTemplateVersion (t , client , admin .OrganizationID , nil )
48
+ coderdtest .AwaitTemplateVersionJob (t , client , version .ID )
49
+ template := coderdtest .CreateTemplate (t , client , admin .OrganizationID , version .ID )
50
+ workspace := coderdtest .CreateWorkspace (t , client , admin .OrganizationID , template .ID )
51
+
52
+ // Always fail auth from this point forward
53
+ authorizer .AlwaysReturn = rbac .ForbiddenWithInternal (xerrors .New ("fake implementation" ), nil , nil )
54
+
55
+ // skipRoutes allows skipping routes from being checked.
56
+ type routeCheck struct {
57
+ NoAuthorize bool
58
+ AssertObject rbac.Object
59
+ StatusCode int
60
+ }
61
+ assertRoute := map [string ]routeCheck {
62
+ // These endpoints do not require auth
63
+ "GET:/api/v2" : {NoAuthorize : true },
64
+ "GET:/api/v2/buildinfo" : {NoAuthorize : true },
65
+ "GET:/api/v2/users/first" : {NoAuthorize : true },
66
+ "POST:/api/v2/users/first" : {NoAuthorize : true },
67
+ "POST:/api/v2/users/login" : {NoAuthorize : true },
68
+ "POST:/api/v2/users/logout" : {NoAuthorize : true },
69
+ "GET:/api/v2/users/authmethods" : {NoAuthorize : true },
70
+
71
+ // All workspaceagents endpoints do not use rbac
72
+ "POST:/api/v2/workspaceagents/aws-instance-identity" : {NoAuthorize : true },
73
+ "POST:/api/v2/workspaceagents/azure-instance-identity" : {NoAuthorize : true },
74
+ "POST:/api/v2/workspaceagents/google-instance-identity" : {NoAuthorize : true },
75
+ "GET:/api/v2/workspaceagents/me/gitsshkey" : {NoAuthorize : true },
76
+ "GET:/api/v2/workspaceagents/me/iceservers" : {NoAuthorize : true },
77
+ "GET:/api/v2/workspaceagents/me/listen" : {NoAuthorize : true },
78
+ "GET:/api/v2/workspaceagents/me/metadata" : {NoAuthorize : true },
79
+ "GET:/api/v2/workspaceagents/me/turn" : {NoAuthorize : true },
80
+ "GET:/api/v2/workspaceagents/{workspaceagent}" : {NoAuthorize : true },
81
+ "GET:/api/v2/workspaceagents/{workspaceagent}/" : {NoAuthorize : true },
82
+ "GET:/api/v2/workspaceagents/{workspaceagent}/dial" : {NoAuthorize : true },
83
+ "GET:/api/v2/workspaceagents/{workspaceagent}/iceservers" : {NoAuthorize : true },
84
+ "GET:/api/v2/workspaceagents/{workspaceagent}/pty" : {NoAuthorize : true },
85
+ "GET:/api/v2/workspaceagents/{workspaceagent}/turn" : {NoAuthorize : true },
86
+
87
+ // 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 },
95
+
96
+ "GET:/api/v2/users/oauth2/github/callback" : {NoAuthorize : true },
97
+
98
+ "POST:/api/v2/users/{user}/organizations/" : {NoAuthorize : true },
99
+ "PUT:/api/v2/organizations/{organization}/members/{user}/roles" : {NoAuthorize : true },
100
+ "GET:/api/v2/organizations/{organization}/provisionerdaemons" : {NoAuthorize : true },
101
+ "POST:/api/v2/organizations/{organization}/templates" : {NoAuthorize : true },
102
+ "GET:/api/v2/organizations/{organization}/templates" : {NoAuthorize : true },
103
+ "GET:/api/v2/organizations/{organization}/templates/{templatename}" : {NoAuthorize : true },
104
+ "POST:/api/v2/organizations/{organization}/templateversions" : {NoAuthorize : true },
105
+ "POST:/api/v2/organizations/{organization}/workspaces" : {NoAuthorize : true },
106
+
107
+ "POST:/api/v2/parameters/{scope}/{id}" : {NoAuthorize : true },
108
+ "GET:/api/v2/parameters/{scope}/{id}" : {NoAuthorize : true },
109
+ "DELETE:/api/v2/parameters/{scope}/{id}/{name}" : {NoAuthorize : true },
110
+
111
+ "GET:/api/v2/provisionerdaemons/me/listen" : {NoAuthorize : true },
112
+
113
+ "DELETE:/api/v2/templates/{template}" : {NoAuthorize : true },
114
+ "GET:/api/v2/templates/{template}" : {NoAuthorize : true },
115
+ "GET:/api/v2/templates/{template}/versions" : {NoAuthorize : true },
116
+ "PATCH:/api/v2/templates/{template}/versions" : {NoAuthorize : true },
117
+ "GET:/api/v2/templates/{template}/versions/{templateversionname}" : {NoAuthorize : true },
118
+
119
+ "GET:/api/v2/templateversions/{templateversion}" : {NoAuthorize : true },
120
+ "PATCH:/api/v2/templateversions/{templateversion}/cancel" : {NoAuthorize : true },
121
+ "GET:/api/v2/templateversions/{templateversion}/logs" : {NoAuthorize : true },
122
+ "GET:/api/v2/templateversions/{templateversion}/parameters" : {NoAuthorize : true },
123
+ "GET:/api/v2/templateversions/{templateversion}/resources" : {NoAuthorize : true },
124
+ "GET:/api/v2/templateversions/{templateversion}/schema" : {NoAuthorize : true },
125
+
126
+ "POST:/api/v2/users/{user}/organizations" : {NoAuthorize : true },
127
+
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
+
134
+ "POST:/api/v2/files" : {NoAuthorize : true },
135
+ "GET:/api/v2/files/{hash}" : {NoAuthorize : true },
136
+
137
+ // These endpoints have more assertions. This is good, add more endpoints to assert if you can!
138
+ "GET:/api/v2/organizations/{organization}" : {AssertObject : rbac .ResourceOrganization .InOrg (admin .OrganizationID )},
139
+ "GET:/api/v2/users/{user}/organizations" : {StatusCode : http .StatusOK , AssertObject : rbac .ResourceOrganization },
140
+ "GET:/api/v2/users/{user}/workspaces" : {StatusCode : http .StatusOK , AssertObject : rbac .ResourceWorkspace },
141
+ "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 ()),
144
+ },
145
+ "GET:/api/v2/organizations/{organization}/workspaces" : {StatusCode : http .StatusOK , AssertObject : rbac .ResourceWorkspace },
146
+
147
+ // These endpoints need payloads to get to the auth part.
148
+ "PUT:/api/v2/users/{user}/roles" : {StatusCode : http .StatusBadRequest , NoAuthorize : true },
149
+ }
150
+
151
+ c , _ := srv .Config .Handler .(* chi.Mux )
152
+ err = chi .Walk (c , func (method string , route string , handler http.Handler , middlewares ... func (http.Handler ) http.Handler ) error {
153
+ name := method + ":" + route
154
+ t .Run (name , func (t * testing.T ) {
155
+ authorizer .reset ()
156
+ routeAssertions , ok := assertRoute [strings .TrimRight (name , "/" )]
157
+ if ! ok {
158
+ // By default, all omitted routes check for just "authorize" called
159
+ routeAssertions = routeCheck {}
160
+ }
161
+ if routeAssertions .StatusCode == 0 {
162
+ routeAssertions .StatusCode = http .StatusForbidden
163
+ }
164
+
165
+ // Replace all url params with known values
166
+ route = strings .ReplaceAll (route , "{organization}" , admin .OrganizationID .String ())
167
+ route = strings .ReplaceAll (route , "{user}" , admin .UserID .String ())
168
+ route = strings .ReplaceAll (route , "{organizationname}" , organization .Name )
169
+ route = strings .ReplaceAll (route , "{workspace}" , workspace .Name )
170
+
171
+ resp , err := client .Request (context .Background (), method , route , nil )
172
+ require .NoError (t , err , "do req" )
173
+ _ = resp .Body .Close ()
174
+
175
+ if ! routeAssertions .NoAuthorize {
176
+ assert .NotNil (t , authorizer .Called , "authorizer expected" )
177
+ assert .Equal (t , routeAssertions .StatusCode , resp .StatusCode , "expect unauthorized" )
178
+ if authorizer .Called != nil {
179
+ if routeAssertions .AssertObject .Type != "" {
180
+ assert .Equal (t , routeAssertions .AssertObject .Type , authorizer .Called .Object .Type , "resource type" )
181
+ }
182
+ if routeAssertions .AssertObject .Owner != "" {
183
+ assert .Equal (t , routeAssertions .AssertObject .Owner , authorizer .Called .Object .Owner , "resource owner" )
184
+ }
185
+ if routeAssertions .AssertObject .OrgID != "" {
186
+ assert .Equal (t , routeAssertions .AssertObject .OrgID , authorizer .Called .Object .OrgID , "resource org" )
187
+ }
188
+ if routeAssertions .AssertObject .ResourceID != "" {
189
+ assert .Equal (t , routeAssertions .AssertObject .ResourceID , authorizer .Called .Object .ResourceID , "resource ID" )
190
+ }
191
+ }
192
+ } else {
193
+ assert .Nil (t , authorizer .Called , "authorize not expected" )
194
+ }
195
+ })
196
+ return nil
197
+ })
198
+ require .NoError (t , err )
199
+ }
200
+
201
+ type authCall struct {
202
+ SubjectID string
203
+ Roles []string
204
+ Action rbac.Action
205
+ Object rbac.Object
206
+ }
207
+
208
+ type fakeAuthorizer struct {
209
+ Called * authCall
210
+ AlwaysReturn error
211
+ }
212
+
213
+ func (f * fakeAuthorizer ) ByRoleName (_ context.Context , subjectID string , roleNames []string , action rbac.Action , object rbac.Object ) error {
214
+ f .Called = & authCall {
215
+ SubjectID : subjectID ,
216
+ Roles : roleNames ,
217
+ Action : action ,
218
+ Object : object ,
219
+ }
220
+ return f .AlwaysReturn
221
+ }
222
+
223
+ func (f * fakeAuthorizer ) reset () {
224
+ f .Called = nil
225
+ }
0 commit comments