@@ -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
})