@@ -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 {
@@ -111,35 +122,23 @@ func ExtractOrganizationMemberParam(db database.Store) func(http.Handler) http.H
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 , nil , 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,113 @@ 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
+ // Only return the user data if the caller can read the user object.
198
+ if auth != nil && auth (r , policy .ActionRead , user ) {
199
+ return & user , organizationMembers , false
200
+ }
201
+
202
+ // If the user cannot be read and 0 memberships exist, throw a 404 to not
203
+ // leak the user existence.
204
+ if len (organizationMembers ) == 0 {
205
+ httpapi .ResourceNotFound (rw )
206
+ return nil , nil , true
207
+ }
208
+
209
+ return nil , organizationMembers , false
210
+ }
211
+
212
+ type OrganizationMembers struct {
213
+ // User is `nil` if the caller is not allowed access to the site wide
214
+ // user object.
215
+ User * database.User
216
+ // Memberships can only be length 0 if `user != nil`. If `user == nil`, then
217
+ // memberships will be at least length 1.
218
+ Memberships []OrganizationMember
219
+ }
220
+
221
+ func (om OrganizationMembers ) UserID () uuid.UUID {
222
+ if om .User != nil {
223
+ return om .User .ID
224
+ }
225
+
226
+ if len (om .Memberships ) > 0 {
227
+ return om .Memberships [0 ].UserID
228
+ }
229
+ return uuid .Nil
230
+ }
231
+
232
+ // ExtractOrganizationMembersParam grabs all user organization memberships.
233
+ // Only requires the "user" URL parameter.
234
+ //
235
+ // Use this if you want to grab as much information for a user as you can.
236
+ // From an organization context, site wide user information might not available.
237
+ func ExtractOrganizationMembersParam (db database.Store , auth func (r * http.Request , action policy.Action , object rbac.Objecter ) bool ) func (http.Handler ) http.Handler {
238
+ return func (next http.Handler ) http.Handler {
239
+ return http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
240
+ ctx := r .Context ()
241
+
242
+ // Fetch all memberships
243
+ user , members , done := ExtractOrganizationMember (ctx , auth , rw , r , db , uuid .Nil )
244
+ if done {
245
+ return
246
+ }
247
+
248
+ orgMembers := make ([]OrganizationMember , 0 , len (members ))
249
+ for _ , organizationMember := range members {
250
+ orgMembers = append (orgMembers , OrganizationMember {
251
+ OrganizationMember : organizationMember .OrganizationMember ,
252
+ Username : organizationMember .Username ,
253
+ AvatarURL : organizationMember .AvatarURL ,
254
+ })
255
+ }
256
+
257
+ ctx = context .WithValue (ctx , organizationMembersParamContextKey {}, OrganizationMembers {
258
+ User : user ,
259
+ Memberships : orgMembers ,
156
260
})
157
261
next .ServeHTTP (rw , r .WithContext (ctx ))
158
262
})
0 commit comments