@@ -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 {
@@ -107,39 +118,27 @@ type OrganizationMember struct {
107118
108119// ExtractOrganizationMemberParam grabs a user membership from the "organization" and "user" URL parameter.
109120// 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 {
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 ,auth ,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,105 @@ 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+ 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 ,
156252})
157253next .ServeHTTP (rw ,r .WithContext (ctx ))
158254})