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 <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 <any-org>`
- `coder organizations members add --org <any-org>`
- `coder provisioner list --org <any-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.*
This commit is contained in:
Ethan
2026-02-16 12:16:08 +11:00
committed by GitHub
parent 7224977fa6
commit 4b3889e4f9
2 changed files with 50 additions and 4 deletions
+15 -4
View File
@@ -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 {
+35
View File
@@ -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) {