@@ -11,12 +11,15 @@ import (
1111"github.com/coder/coder/v2/coderd/database"
1212"github.com/coder/coder/v2/coderd/database/dbauthz"
1313"github.com/coder/coder/v2/coderd/httpapi"
14+ "github.com/coder/coder/v2/coderd/rbac"
15+ "github.com/coder/coder/v2/coderd/rbac/policy"
1416"github.com/coder/coder/v2/codersdk"
1517)
1618
1719type (
18- organizationParamContextKey struct {}
19- organizationMemberParamContextKey struct {}
20+ organizationParamContextKey struct {}
21+ organizationMemberParamContextKey struct {}
22+ organizationMembersParamContextKey struct {}
2023)
2124
2225// OrganizationParam returns the organization from the ExtractOrganizationParam handler.
@@ -38,6 +41,14 @@ func OrganizationMemberParam(r *http.Request) OrganizationMember {
3841return organizationMember
3942}
4043
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+
4152// ExtractOrganizationParam grabs an organization from the "organization" URL parameter.
4253// This middleware requires the API key middleware higher in the call stack for authentication.
4354func ExtractOrganizationParam (db database.Store )func (http.Handler ) http.Handler {
@@ -111,35 +122,23 @@ func ExtractOrganizationMemberParam(db database.Store) func(http.Handler) http.H
111122return func (next http.Handler ) http.Handler {
112123return http .HandlerFunc (func (rw http.ResponseWriter ,r * http.Request ) {
113124ctx := 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- }
124125organization := 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 {
133128return
134129}
135- if err != nil {
130+
131+ if len (members )!= 1 {
136132httpapi .Write (ctx ,rw ,http .StatusInternalServerError , codersdk.Response {
137133Message :"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 )),
139136})
140137return
141138}
142139
140+ organizationMember := members [0 ]
141+
143142ctx = context .WithValue (ctx ,organizationMemberParamContextKey {},OrganizationMember {
144143OrganizationMember :organizationMember .OrganizationMember ,
145144// 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
151150// API handlers need this information for audit logging and returning the owner's
152151// username in response to creating a workspace. Additionally, the frontend consumes
153152// 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 ,
156260})
157261next .ServeHTTP (rw ,r .WithContext (ctx ))
158262})