@@ -11,12 +11,15 @@ import (
11
11
"github.com/coder/coder/v2/coderd/database"
12
12
"github.com/coder/coder/v2/coderd/database/dbauthz"
13
13
"github.com/coder/coder/v2/coderd/httpapi"
14
+ "github.com/coder/coder/v2/coderd/rbac"
15
+ "github.com/coder/coder/v2/coderd/rbac/policy"
14
16
"github.com/coder/coder/v2/codersdk"
15
17
)
16
18
17
19
type (
18
- organizationParamContextKey struct {}
19
- organizationMemberParamContextKey struct {}
20
+ organizationParamContextKey struct {}
21
+ organizationMemberParamContextKey struct {}
22
+ organizationMembersParamContextKey struct {}
20
23
)
21
24
22
25
// OrganizationParam returns the organization from the ExtractOrganizationParam handler.
@@ -38,6 +41,14 @@ func OrganizationMemberParam(r *http.Request) OrganizationMember {
38
41
return organizationMember
39
42
}
40
43
44
+ func OrganizationMembersParam (r * http.Request ) OrganizationMembers {
45
+ organizationMembers , ok := r .Context ().Value (organizationMembersParamContextKey {}).(OrganizationMembers )
46
+ if ! ok {
47
+ panic ("developer error: organization members param middleware not provided" )
48
+ }
49
+ return organizationMembers
50
+ }
51
+
41
52
// ExtractOrganizationParam grabs an organization from the "organization" URL parameter.
42
53
// This middleware requires the API key middleware higher in the call stack for authentication.
43
54
func ExtractOrganizationParam (db database.Store ) func (http.Handler ) http.Handler {
@@ -107,39 +118,27 @@ type OrganizationMember struct {
107
118
108
119
// ExtractOrganizationMemberParam grabs a user membership from the "organization" and "user" URL parameter.
109
120
// This middleware requires the ExtractUser and ExtractOrganization middleware higher in the stack
110
- func ExtractOrganizationMemberParam (db database.Store ) func (http.Handler ) http.Handler {
121
+ func ExtractOrganizationMemberParam (db database.Store , auth func ( r * http. Request , action policy. Action , object rbac. Objecter ) bool ) func (http.Handler ) http.Handler {
111
122
return func (next http.Handler ) http.Handler {
112
123
return http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
113
124
ctx := r .Context ()
114
- // We need to resolve the `{user}` URL parameter so that we can get the userID and
115
- // username. We do this as SystemRestricted since the caller might have permission
116
- // to access the OrganizationMember object, but *not* the User object. So, it is
117
- // very important that we do not add the User object to the request context or otherwise
118
- // leak it to the API handler.
119
- // nolint:gocritic
120
- user , ok := ExtractUserContext (dbauthz .AsSystemRestricted (ctx ), db , rw , r )
121
- if ! ok {
122
- return
123
- }
124
125
organization := OrganizationParam (r )
125
-
126
- organizationMember , err := database .ExpectOne (db .OrganizationMembers (ctx , database.OrganizationMembersParams {
127
- OrganizationID : organization .ID ,
128
- UserID : user .ID ,
129
- IncludeSystem : false ,
130
- }))
131
- if httpapi .Is404Error (err ) {
132
- httpapi .ResourceNotFound (rw )
126
+ _ , members , done := ExtractOrganizationMember (ctx , auth , rw , r , db , organization .ID )
127
+ if done {
133
128
return
134
129
}
135
- if err != nil {
130
+
131
+ if len (members ) != 1 {
136
132
httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
137
133
Message : "Internal error fetching organization member." ,
138
- Detail : err .Error (),
134
+ // This is a developer error and should never happen.
135
+ Detail : fmt .Sprintf ("Expected exactly one organization member, but got %d." , len (members )),
139
136
})
140
137
return
141
138
}
142
139
140
+ organizationMember := members [0 ]
141
+
143
142
ctx = context .WithValue (ctx , organizationMemberParamContextKey {}, OrganizationMember {
144
143
OrganizationMember : organizationMember .OrganizationMember ,
145
144
// Here we're making two exceptions to the rule about not leaking data about the user
@@ -151,8 +150,105 @@ func ExtractOrganizationMemberParam(db database.Store) func(http.Handler) http.H
151
150
// API handlers need this information for audit logging and returning the owner's
152
151
// username in response to creating a workspace. Additionally, the frontend consumes
153
152
// the Avatar URL and this allows the FE to avoid an extra request.
154
- Username : user .Username ,
155
- AvatarURL : user .AvatarURL ,
153
+ Username : organizationMember .Username ,
154
+ AvatarURL : organizationMember .AvatarURL ,
155
+ })
156
+
157
+ next .ServeHTTP (rw , r .WithContext (ctx ))
158
+ })
159
+ }
160
+ }
161
+
162
+ // ExtractOrganizationMember extracts all user memberships from the "user" URL
163
+ // parameter. If orgID is uuid.Nil, then it will return all memberships for the
164
+ // user, otherwise it will only return memberships to the org.
165
+ //
166
+ // If `user` is returned, that means the caller can use the data. This is returned because
167
+ // it is possible to have a user with 0 organizations. So the user != nil, with 0 memberships.
168
+ func ExtractOrganizationMember (ctx context.Context , auth func (r * http.Request , action policy.Action , object rbac.Objecter ) bool , rw http.ResponseWriter , r * http.Request , db database.Store , orgID uuid.UUID ) (* database.User , []database.OrganizationMembersRow , bool ) {
169
+ // We need to resolve the `{user}` URL parameter so that we can get the userID and
170
+ // username. We do this as SystemRestricted since the caller might have permission
171
+ // to access the OrganizationMember object, but *not* the User object. So, it is
172
+ // very important that we do not add the User object to the request context or otherwise
173
+ // leak it to the API handler.
174
+ // nolint:gocritic
175
+ user , ok := ExtractUserContext (dbauthz .AsSystemRestricted (ctx ), db , rw , r )
176
+ if ! ok {
177
+ return nil , nil , true
178
+ }
179
+
180
+ organizationMembers , err := db .OrganizationMembers (ctx , database.OrganizationMembersParams {
181
+ OrganizationID : orgID ,
182
+ UserID : user .ID ,
183
+ IncludeSystem : false ,
184
+ })
185
+ if httpapi .Is404Error (err ) {
186
+ httpapi .ResourceNotFound (rw )
187
+ return nil , nil , true
188
+ }
189
+ if err != nil {
190
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
191
+ Message : "Internal error fetching organization member." ,
192
+ Detail : err .Error (),
193
+ })
194
+ return nil , nil , true
195
+ }
196
+
197
+ if auth (r , policy .ActionRead , user ) {
198
+ return & user , organizationMembers , true
199
+ }
200
+
201
+ // If the user cannot be read and 0 memberships exist, throw a 404 to not
202
+ // leak the user existance.
203
+ if len (organizationMembers ) == 0 {
204
+ httpapi .ResourceNotFound (rw )
205
+ return nil , nil , true
206
+ }
207
+
208
+ return nil , organizationMembers , false
209
+ }
210
+
211
+ type OrganizationMembers struct {
212
+ User * database.User
213
+ Memberships []OrganizationMember
214
+ }
215
+
216
+ func (om OrganizationMembers ) UserID () uuid.UUID {
217
+ if om .User != nil {
218
+ return om .User .ID
219
+ }
220
+
221
+ if len (om .Memberships ) > 0 {
222
+ return om .Memberships [0 ].UserID
223
+ }
224
+ return uuid .Nil
225
+ }
226
+
227
+ // ExtractOrganizationMembersParam grabs all user organization memberships.
228
+ // Only requires the "user" URL parameter.
229
+ func ExtractOrganizationMembersParam (db database.Store , auth func (r * http.Request , action policy.Action , object rbac.Objecter ) bool ) func (http.Handler ) http.Handler {
230
+ return func (next http.Handler ) http.Handler {
231
+ return http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
232
+ ctx := r .Context ()
233
+
234
+ // Fetch all memberships
235
+ user , members , done := ExtractOrganizationMember (ctx , auth , rw , r , db , uuid .Nil )
236
+ if done {
237
+ return
238
+ }
239
+
240
+ orgMembers := make ([]OrganizationMember , 0 , len (members ))
241
+ for _ , organizationMember := range members {
242
+ orgMembers = append (orgMembers , OrganizationMember {
243
+ OrganizationMember : organizationMember .OrganizationMember ,
244
+ Username : organizationMember .Username ,
245
+ AvatarURL : organizationMember .AvatarURL ,
246
+ })
247
+ }
248
+
249
+ ctx = context .WithValue (ctx , organizationMembersParamContextKey {}, OrganizationMembers {
250
+ User : user ,
251
+ Memberships : orgMembers ,
156
252
})
157
253
next .ServeHTTP (rw , r .WithContext (ctx ))
158
254
})
0 commit comments