chore: fetch workspaces by username with organization permissions (#17707)

Closes https://github.com/coder/coder/issues/17691

`ExtractOrganizationMembersParam` will allow fetching a user with only
organization permissions. If the user belongs to 0 orgs, then the user "does not exist" 
from an org perspective. But if you are a site-wide admin, then the user does exist.
This commit is contained in:
Steven Masley
2025-05-08 14:41:17 -05:00
committed by GitHub
parent d93a9cfde2
commit d5360a6da0
6 changed files with 185 additions and 74 deletions

View File

@ -253,7 +253,8 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
// @Router /users/{user}/workspace/{workspacename} [get]
func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
owner := httpmw.UserParam(r)
mems := httpmw.OrganizationMembersParam(r)
workspaceName := chi.URLParam(r, "workspacename")
apiKey := httpmw.APIKey(r)
@ -273,12 +274,12 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
}
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(ctx, database.GetWorkspaceByOwnerIDAndNameParams{
OwnerID: owner.ID,
OwnerID: mems.UserID(),
Name: workspaceName,
})
if includeDeleted && errors.Is(err, sql.ErrNoRows) {
workspace, err = api.Database.GetWorkspaceByOwnerIDAndName(ctx, database.GetWorkspaceByOwnerIDAndNameParams{
OwnerID: owner.ID,
OwnerID: mems.UserID(),
Name: workspaceName,
Deleted: includeDeleted,
})
@ -408,6 +409,7 @@ func (api *API) postUserWorkspaces(rw http.ResponseWriter, r *http.Request) {
ctx = r.Context()
apiKey = httpmw.APIKey(r)
auditor = api.Auditor.Load()
mems = httpmw.OrganizationMembersParam(r)
)
var req codersdk.CreateWorkspaceRequest
@ -416,17 +418,16 @@ func (api *API) postUserWorkspaces(rw http.ResponseWriter, r *http.Request) {
}
var owner workspaceOwner
// This user fetch is an optimization path for the most common case of creating a
// workspace for 'Me'.
//
// This is also required to allow `owners` to create workspaces for users
// that are not in an organization.
user, ok := httpmw.UserParamOptional(r)
if ok {
if mems.User != nil {
// This user fetch is an optimization path for the most common case of creating a
// workspace for 'Me'.
//
// This is also required to allow `owners` to create workspaces for users
// that are not in an organization.
owner = workspaceOwner{
ID: user.ID,
Username: user.Username,
AvatarURL: user.AvatarURL,
ID: mems.User.ID,
Username: mems.User.Username,
AvatarURL: mems.User.AvatarURL,
}
} else {
// A workspace can still be created if the caller can read the organization
@ -443,35 +444,21 @@ func (api *API) postUserWorkspaces(rw http.ResponseWriter, r *http.Request) {
return
}
// We need to fetch the original user as a system user to fetch the
// user_id. 'ExtractUserContext' handles all cases like usernames,
// 'Me', etc.
// nolint:gocritic // The user_id needs to be fetched. This handles all those cases.
user, ok := httpmw.ExtractUserContext(dbauthz.AsSystemRestricted(ctx), api.Database, rw, r)
if !ok {
return
}
organizationMember, err := database.ExpectOne(api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{
OrganizationID: template.OrganizationID,
UserID: user.ID,
IncludeSystem: false,
}))
if httpapi.Is404Error(err) {
// If the caller can find the organization membership in the same org
// as the template, then they can continue.
orgIndex := slices.IndexFunc(mems.Memberships, func(mem httpmw.OrganizationMember) bool {
return mem.OrganizationID == template.OrganizationID
})
if orgIndex == -1 {
httpapi.ResourceNotFound(rw)
return
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching organization member.",
Detail: err.Error(),
})
return
}
member := mems.Memberships[orgIndex]
owner = workspaceOwner{
ID: organizationMember.OrganizationMember.UserID,
Username: organizationMember.Username,
AvatarURL: organizationMember.AvatarURL,
ID: member.UserID,
Username: member.Username,
AvatarURL: member.AvatarURL,
}
}