diff --git a/coderd/coderd.go b/coderd/coderd.go index cc411ef71e255..fedecf252c79f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -149,7 +149,7 @@ func New(options *Options) (http.Handler, func()) { r.Get("/", api.workspacesByOrganization) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) - r.Get("/{workspace}", api.workspaceByOwnerAndName) + r.Get("/{workspacename}", api.workspaceByOwnerAndName) r.Get("/", api.workspacesByOwner) }) }) @@ -237,8 +237,6 @@ func New(options *Options) (http.Handler, func()) { r.Route("/password", func(r chi.Router) { r.Put("/", api.putUserPassword) }) - r.Get("/organizations", api.organizationsByUser) - r.Post("/organizations", api.postOrganizationsByUser) // These roles apply to the site wide permissions. r.Put("/roles", api.putUserRoles) r.Get("/roles", api.userRoles) @@ -316,6 +314,7 @@ func New(options *Options) (http.Handler, func()) { r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { r.Use( apiKeyMiddleware, + authRolesMiddleware, httpmw.ExtractWorkspaceBuildParam(options.Database), httpmw.ExtractWorkspaceParam(options.Database), ) diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index 08bd168837804..d462263dcd0ae 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -2,6 +2,7 @@ package coderd_test import ( "context" + "io" "net/http" "strings" "testing" @@ -48,13 +49,18 @@ func TestAuthorizeAllEndpoints(t *testing.T) { coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, admin.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) // Always fail auth from this point forward authorizer.AlwaysReturn = rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil) + // Some quick reused objects + workspaceRBACObj := rbac.ResourceWorkspace.InOrg(organization.ID).WithID(workspace.ID.String()).WithOwner(workspace.OwnerID.String()) + // skipRoutes allows skipping routes from being checked. type routeCheck struct { NoAuthorize bool + AssertAction rbac.Action AssertObject rbac.Object StatusCode int } @@ -84,13 +90,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) { "GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true}, // TODO: @emyrk these need to be fixed by adding authorize calls - "GET:/api/v2/workspaceresources/{workspaceresource}": {NoAuthorize: true}, - "GET:/api/v2/workspacebuilds/{workspacebuild}": {NoAuthorize: true}, - "GET:/api/v2/workspacebuilds/{workspacebuild}/logs": {NoAuthorize: true}, - "GET:/api/v2/workspacebuilds/{workspacebuild}/resources": {NoAuthorize: true}, - "GET:/api/v2/workspacebuilds/{workspacebuild}/state": {NoAuthorize: true}, - "PATCH:/api/v2/workspacebuilds/{workspacebuild}/cancel": {NoAuthorize: true}, - "GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": {NoAuthorize: true}, + "GET:/api/v2/workspaceresources/{workspaceresource}": {NoAuthorize: true}, "GET:/api/v2/users/oauth2/github/callback": {NoAuthorize: true}, @@ -123,15 +123,9 @@ func TestAuthorizeAllEndpoints(t *testing.T) { "POST:/api/v2/users/{user}/organizations": {NoAuthorize: true}, - "GET:/api/v2/workspaces/{workspace}": {NoAuthorize: true}, - "PUT:/api/v2/workspaces/{workspace}/autostart": {NoAuthorize: true}, - "PUT:/api/v2/workspaces/{workspace}/autostop": {NoAuthorize: true}, - "GET:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true}, - "POST:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true}, - "GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true}, - - "POST:/api/v2/files": {NoAuthorize: true}, - "GET:/api/v2/files/{hash}": {NoAuthorize: true}, + "POST:/api/v2/files": {NoAuthorize: true}, + "GET:/api/v2/files/{hash}": {NoAuthorize: true}, + "GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true}, // These endpoints have more assertions. This is good, add more endpoints to assert if you can! "GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.InOrg(admin.OrganizationID)}, @@ -141,11 +135,60 @@ func TestAuthorizeAllEndpoints(t *testing.T) { "GET:/api/v2/organizations/{organization}/workspaces/{user}/{workspace}": { AssertObject: rbac.ResourceWorkspace.InOrg(organization.ID).WithID(workspace.ID.String()).WithOwner(workspace.OwnerID.String()), }, + "GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + "GET:/api/v2/organizations/{organization}/workspaces/{user}/{workspacename}": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, "GET:/api/v2/organizations/{organization}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace}, - "GET:/api/v2/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace}, + "GET:/api/v2/workspacebuilds/{workspacebuild}": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + "GET:/api/v2/workspacebuilds/{workspacebuild}/logs": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + "GET:/api/v2/workspaces/{workspace}/builds": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + "GET:/api/v2/workspaces/{workspace}": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + "PUT:/api/v2/workspaces/{workspace}/autostart": { + AssertAction: rbac.ActionUpdate, + AssertObject: workspaceRBACObj, + }, + "PUT:/api/v2/workspaces/{workspace}/autostop": { + AssertAction: rbac.ActionUpdate, + AssertObject: workspaceRBACObj, + }, + "PATCH:/api/v2/workspacebuilds/{workspacebuild}/cancel": { + AssertAction: rbac.ActionUpdate, + AssertObject: workspaceRBACObj, + }, + "GET:/api/v2/workspacebuilds/{workspacebuild}/resources": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + "GET:/api/v2/workspacebuilds/{workspacebuild}/state": { + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, + "GET:/api/v2/workspaces/": { + StatusCode: http.StatusOK, + AssertAction: rbac.ActionRead, + AssertObject: workspaceRBACObj, + }, - // These endpoints need payloads to get to the auth part. - "PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, + // These endpoints need payloads to get to the auth part. Payloads will be required + "PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, + "POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, } for k, v := range assertRoute { @@ -175,16 +218,24 @@ func TestAuthorizeAllEndpoints(t *testing.T) { route = strings.ReplaceAll(route, "{organization}", admin.OrganizationID.String()) route = strings.ReplaceAll(route, "{user}", admin.UserID.String()) route = strings.ReplaceAll(route, "{organizationname}", organization.Name) - route = strings.ReplaceAll(route, "{workspace}", workspace.Name) + route = strings.ReplaceAll(route, "{workspace}", workspace.ID.String()) + route = strings.ReplaceAll(route, "{workspacebuild}", workspace.LatestBuild.ID.String()) + route = strings.ReplaceAll(route, "{workspacename}", workspace.Name) + route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name) resp, err := client.Request(context.Background(), method, route, nil) require.NoError(t, err, "do req") + body, _ := io.ReadAll(resp.Body) + t.Logf("Response Body: %q", string(body)) _ = resp.Body.Close() if !routeAssertions.NoAuthorize { assert.NotNil(t, authorizer.Called, "authorizer expected") assert.Equal(t, routeAssertions.StatusCode, resp.StatusCode, "expect unauthorized") if authorizer.Called != nil { + if routeAssertions.AssertAction != "" { + assert.Equal(t, routeAssertions.AssertAction, authorizer.Called.Action, "resource action") + } if routeAssertions.AssertObject.Type != "" { assert.Equal(t, routeAssertions.AssertObject.Type, authorizer.Called.Object.Type, "resource type") } diff --git a/coderd/users.go b/coderd/users.go index 92e2135b2fd37..66ffde5202dac 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -568,6 +568,14 @@ func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) if !httpapi.Read(rw, r, &req) { return } + + // Create organization uses the organization resource without an OrgID. + // This means you need the site wide permission to make a new organization. + if !api.Authorize(rw, r, rbac.ActionCreate, + rbac.ResourceOrganization) { + return + } + _, err := api.Database.GetOrganizationByName(r.Context(), req.Name) if err == nil { httpapi.Write(rw, http.StatusConflict, httpapi.Response{ diff --git a/coderd/users_test.go b/coderd/users_test.go index e0e1033819526..2ac25b47fa0d1 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -173,7 +173,7 @@ func TestPostUsers(t *testing.T) { client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) notInOrg := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) - other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleAdmin(), rbac.RoleMember()) org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ Name: "another", }) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index b3cf97462d969..3ee251c2c424e 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -15,11 +15,25 @@ import ( "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/codersdk" ) func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) + workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "no workspace exists for this job", + }) + return + } + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -34,6 +48,11 @@ func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) { func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + paginationParams, ok := parsePagination(rw, r) if !ok { return @@ -90,6 +109,11 @@ func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + workspaceBuildName := chi.URLParam(r, "workspacebuildname") workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{ WorkspaceID: workspace.ID, @@ -125,6 +149,25 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { if !httpapi.Read(rw, r, &createBuild) { return } + + // Rbac action depends on the transition + var action rbac.Action + switch createBuild.Transition { + case database.WorkspaceTransitionDelete: + action = rbac.ActionDelete + case database.WorkspaceTransitionStart, database.WorkspaceTransitionStop: + action = rbac.ActionUpdate + default: + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("transition not supported: %q", createBuild.Transition), + }) + return + } + if !api.Authorize(rw, r, action, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + if createBuild.TemplateVersionID == uuid.Nil { latestBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) if err != nil { @@ -269,6 +312,19 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) + workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "no workspace exists for this job", + }) + return + } + + if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -308,6 +364,19 @@ func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) + workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "no workspace exists for this job", + }) + return + } + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -320,6 +389,19 @@ func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) + workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "no workspace exists for this job", + }) + return + } + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -330,8 +412,20 @@ func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) { api.provisionerJobLogs(rw, r, job) } -func (*api) workspaceBuildState(rw http.ResponseWriter, r *http.Request) { +func (api *api) workspaceBuildState(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) + workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "no workspace exists for this job", + }) + return + } + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } rw.Header().Set("Content-Type", "application/json") rw.WriteHeader(http.StatusOK) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 1e567dfe77c40..577d8158be0cf 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -29,6 +29,10 @@ import ( func (api *api) workspace(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) + if !api.Authorize(rw, r, rbac.ActionRead, + rbac.ResourceWorkspace.InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) if err != nil { @@ -63,11 +67,6 @@ func (api *api) workspace(rw http.ResponseWriter, r *http.Request) { return } - if !api.Authorize(rw, r, rbac.ActionRead, - rbac.ResourceWorkspace.InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { - return - } - httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner)) } @@ -219,7 +218,7 @@ func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) { func (api *api) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) { owner := httpmw.UserParam(r) organization := httpmw.OrganizationParam(r) - workspaceName := chi.URLParam(r, "workspace") + workspaceName := chi.URLParam(r, "workspacename") workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ OwnerID: owner.ID, @@ -477,6 +476,12 @@ func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req } func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { + workspace := httpmw.WorkspaceParam(r) + if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + var req codersdk.UpdateWorkspaceAutostartRequest if !httpapi.Read(rw, r, &req) { return @@ -495,7 +500,6 @@ func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { dbSched.Valid = true } - workspace := httpmw.WorkspaceParam(r) err := api.Database.UpdateWorkspaceAutostart(r.Context(), database.UpdateWorkspaceAutostartParams{ ID: workspace.ID, AutostartSchedule: dbSched, @@ -509,6 +513,12 @@ func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { } func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) { + workspace := httpmw.WorkspaceParam(r) + if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + var req codersdk.UpdateWorkspaceAutostopRequest if !httpapi.Read(rw, r, &req) { return @@ -527,7 +537,6 @@ func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) { dbSched.Valid = true } - workspace := httpmw.WorkspaceParam(r) err := api.Database.UpdateWorkspaceAutostop(r.Context(), database.UpdateWorkspaceAutostopParams{ ID: workspace.ID, AutostopSchedule: dbSched, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 6658aa8481940..a5f118c76542d 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "github.com/coder/coder/coderd/rbac" + "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -18,7 +20,7 @@ import ( "github.com/coder/coder/provisionersdk/proto" ) -func TestWorkspace(t *testing.T) { +func TestAdminViewAllWorkspaces(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) @@ -27,8 +29,25 @@ func TestWorkspace(t *testing.T) { coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) _, err := client.Workspace(context.Background(), workspace.ID) require.NoError(t, err) + + otherOrg, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ + Name: "default-test", + }) + require.NoError(t, err, "create other org") + + // This other user is not in the first user's org. Since other is an admin, they can + // still see the "first" user's workspace. + other := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.RoleAdmin(), rbac.RoleMember()) + otherWorkspaces, err := other.Workspaces(context.Background(), codersdk.WorkspaceFilter{}) + require.NoError(t, err, "(other) fetch workspaces") + + firstWorkspaces, err := other.Workspaces(context.Background(), codersdk.WorkspaceFilter{}) + require.NoError(t, err, "(first) fetch workspaces") + + require.ElementsMatch(t, otherWorkspaces, firstWorkspaces) } func TestPostWorkspacesByOrganization(t *testing.T) { @@ -52,7 +71,7 @@ func TestPostWorkspacesByOrganization(t *testing.T) { client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) - other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleMember(), rbac.RoleAdmin()) org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ Name: "another", })