From 4b3889e4f95508ad90748fc9aee4d6fb3c31e287 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:16:08 +1100 Subject: [PATCH] fix(cli): allow site admins to use `coder create --org` for any organization (#21528) ## Problem Site-wide admins (e.g., Owners) could not use `coder create --org ` to create workspaces in organizations they are not members of. The error was: ``` $ coder create my-workspace -t docker --org data-science error: organization "data-science" not found, are you sure you are a member of this organization? ``` This was inconsistent with the web UI, where Owners can create workspaces in any organization. ## Root Cause The CLI's `OrganizationContext.Selected()` function only checked the user's membership list, ignoring site-wide RBAC permissions that grant Owners access to all organizations. ## Solution Added a fallback in `OrganizationContext.Selected()` that fetches the org directly via the API when not found in the membership list. This works because the API endpoint applies RBAC filtering, allowing Owners to read any org. ## Impact This fixes `coder create --org` and all other CLI commands that use `OrganizationContext.Selected()` (29+ commands), including: - `coder templates push --org ` - `coder organizations members add --org ` - `coder provisioner list --org ` ## Testing Added `TestEnterpriseCreate/OwnerCanCreateInNonMemberOrg` which: - Creates an Owner user who is NOT a member of a second org - Verifies they can create a workspace there using `--org` - Properly fails without the code fix, passes with it --- *This PR was generated by [mux](https://mux.coder.com) but reviewed by a human.* --- cli/root.go | 19 +++++++++++++++---- enterprise/cli/create_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/cli/root.go b/cli/root.go index 5a09cad853..cf7504f7b9 100644 --- a/cli/root.go +++ b/cli/root.go @@ -884,16 +884,27 @@ func (o *OrganizationContext) Selected(inv *serpent.Invocation, client *codersdk index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool { return org.Name == o.FlagSelect || org.ID.String() == o.FlagSelect }) + if index >= 0 { + return orgs[index], nil + } - if index < 0 { + // Not in membership list - try direct fetch. + // This allows site-wide admins (e.g., Owners) to use orgs they aren't + // members of. + org, err := client.OrganizationByName(inv.Context(), o.FlagSelect) + if err != nil { var names []string for _, org := range orgs { names = append(names, org.Name) } - return codersdk.Organization{}, xerrors.Errorf("organization %q not found, are you sure you are a member of this organization? "+ - "Valid options for '--org=' are [%s].", o.FlagSelect, strings.Join(names, ", ")) + var sdkErr *codersdk.Error + if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound { + return codersdk.Organization{}, xerrors.Errorf("organization %q not found, are you sure you are a member of this organization? "+ + "Valid options for '--org=' are [%s].", o.FlagSelect, strings.Join(names, ", ")) + } + return codersdk.Organization{}, xerrors.Errorf("get organization %q: %w", o.FlagSelect, err) } - return orgs[index], nil + return org, nil } if len(orgs) == 1 { diff --git a/enterprise/cli/create_test.go b/enterprise/cli/create_test.go index 9c1fa8bcf6..705d9ed71e 100644 --- a/enterprise/cli/create_test.go +++ b/enterprise/cli/create_test.go @@ -192,6 +192,41 @@ func TestEnterpriseCreate(t *testing.T) { } }) + // Site-wide admins (Owners) can create workspaces in organizations they + // are not a member of by using the --org flag. + t.Run("OwnerCanCreateInNonMemberOrg", func(t *testing.T) { + t.Parallel() + + const templateName = "ownertemplate" + setup := setupMultipleOrganizations(t, setupArgs{ + secondTemplates: []string{templateName}, + }) + + // Create a new Owner user who is NOT a member of the second org. + // The setup.owner created the second org and is auto-added as member, + // so we need a different Owner to test the RBAC-only path. + newOwner, _ := coderdtest.CreateAnotherUser(t, setup.owner, setup.firstResponse.OrganizationID, rbac.RoleOwner()) + + args := []string{ + "create", + "owner-workspace", + "-y", + "--template", templateName, + "--org", setup.second.Name, + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, newOwner, root) + _ = ptytest.New(t).Attach(inv) + err := inv.Run() + require.NoError(t, err) + + ws, err := newOwner.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "owner-workspace", codersdk.WorkspaceOptions{}) + if assert.NoError(t, err, "expected workspace to be created") { + assert.Equal(t, ws.TemplateName, templateName) + assert.Equal(t, ws.OrganizationName, setup.second.Name, "workspace in second organization") + } + }) + // If an organization is specified, but the template is not in that // organization, an error is thrown. t.Run("CreateIncorrectOrg", func(t *testing.T) {