diff --git a/cli/organization_test.go b/cli/organization_test.go index 160f4b37b6..2347ca6e79 100644 --- a/cli/organization_test.go +++ b/cli/organization_test.go @@ -12,11 +12,8 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/v2/cli/clitest" - "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/pty/ptytest" - "github.com/coder/coder/v2/testutil" ) func TestCurrentOrganization(t *testing.T) { @@ -55,64 +52,6 @@ func TestCurrentOrganization(t *testing.T) { require.NoError(t, <-errC) pty.ExpectMatch(orgID.String()) }) - - t.Run("OnlyID", func(t *testing.T) { - t.Parallel() - ownerClient := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, ownerClient) - // Owner is required to make orgs - client, _ := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID, rbac.RoleOwner()) - - ctx := testutil.Context(t, testutil.WaitMedium) - orgs := []string{"foo", "bar"} - for _, orgName := range orgs { - _, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: orgName, - }) - require.NoError(t, err) - } - - inv, root := clitest.New(t, "organizations", "show", "--only-id", "--org="+first.OrganizationID.String()) - clitest.SetupConfig(t, client, root) - pty := ptytest.New(t).Attach(inv) - errC := make(chan error) - go func() { - errC <- inv.Run() - }() - require.NoError(t, <-errC) - pty.ExpectMatch(first.OrganizationID.String()) - }) - - t.Run("UsingFlag", func(t *testing.T) { - t.Parallel() - ownerClient := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, ownerClient) - // Owner is required to make orgs - client, _ := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID, rbac.RoleOwner()) - - ctx := testutil.Context(t, testutil.WaitMedium) - orgs := map[string]codersdk.Organization{ - "foo": {}, - "bar": {}, - } - for orgName := range orgs { - org, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: orgName, - }) - require.NoError(t, err) - orgs[orgName] = org - } - - inv, root := clitest.New(t, "organizations", "show", "selected", "--only-id", "-O=bar") - clitest.SetupConfig(t, client, root) - pty := ptytest.New(t).Attach(inv) - errC := make(chan error) - go func() { - errC <- inv.Run() - }() - require.NoError(t, <-errC) - pty.ExpectMatch(orgs["bar"].ID.String()) - }) } func must[V any](v V, err error) V { diff --git a/cli/organizationmembers_test.go b/cli/organizationmembers_test.go index 55b3071a55..e17b268ea7 100644 --- a/cli/organizationmembers_test.go +++ b/cli/organizationmembers_test.go @@ -9,7 +9,6 @@ import ( "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/rbac" - "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -36,43 +35,6 @@ func TestListOrganizationMembers(t *testing.T) { }) } -func TestAddOrganizationMembers(t *testing.T) { - t.Parallel() - - t.Run("OK", func(t *testing.T) { - t.Parallel() - - ownerClient := coderdtest.New(t, &coderdtest.Options{}) - owner := coderdtest.CreateFirstUser(t, ownerClient) - _, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) - - ctx := testutil.Context(t, testutil.WaitMedium) - //nolint:gocritic // must be an owner, only owners can create orgs - otherOrg, err := ownerClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "Other", - DisplayName: "", - Description: "", - Icon: "", - }) - require.NoError(t, err, "create another organization") - - inv, root := clitest.New(t, "organization", "members", "add", "-O", otherOrg.ID.String(), user.Username) - //nolint:gocritic // must be an owner - clitest.SetupConfig(t, ownerClient, root) - - buf := new(bytes.Buffer) - inv.Stdout = buf - err = inv.WithContext(ctx).Run() - require.NoError(t, err) - - //nolint:gocritic // must be an owner - members, err := ownerClient.OrganizationMembers(ctx, otherOrg.ID) - require.NoError(t, err) - - require.Len(t, members, 2) - }) -} - func TestRemoveOrganizationMembers(t *testing.T) { t.Parallel() diff --git a/cli/templatecreate_test.go b/cli/templatecreate_test.go index 42ef60946b..093ca6e0cc 100644 --- a/cli/templatecreate_test.go +++ b/cli/templatecreate_test.go @@ -18,7 +18,7 @@ import ( "github.com/coder/coder/v2/testutil" ) -func TestTemplateCreate(t *testing.T) { +func TestCliTemplateCreate(t *testing.T) { t.Parallel() t.Run("Create", func(t *testing.T) { t.Parallel() diff --git a/cli/templatelist_test.go b/cli/templatelist_test.go index 5b7962e1c4..06cb75ea4a 100644 --- a/cli/templatelist_test.go +++ b/cli/templatelist_test.go @@ -110,41 +110,4 @@ func TestTemplateList(t *testing.T) { pty.ExpectMatch("No templates found") pty.ExpectMatch("Create one:") }) - - t.Run("MultiOrg", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - owner := coderdtest.CreateFirstUser(t, client) - - // Template in the first organization - firstVersion := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) - _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, firstVersion.ID) - _ = coderdtest.CreateTemplate(t, client, owner.OrganizationID, firstVersion.ID) - - secondOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{ - // Listing templates does not require the template actually completes. - // We cannot provision an external provisioner in AGPL tests. - IncludeProvisionerDaemon: false, - }) - secondVersion := coderdtest.CreateTemplateVersion(t, client, secondOrg.ID, nil) - _ = coderdtest.CreateTemplate(t, client, secondOrg.ID, secondVersion.ID) - - // Create a site wide template admin - templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) - - inv, root := clitest.New(t, "templates", "list", "--output=json") - clitest.SetupConfig(t, templateAdmin, root) - - ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancelFunc() - - out := bytes.NewBuffer(nil) - inv.Stdout = out - err := inv.WithContext(ctx).Run() - require.NoError(t, err) - - var templates []codersdk.Template - require.NoError(t, json.Unmarshal(out.Bytes(), &templates)) - require.Len(t, templates, 2) - }) } diff --git a/coderd/audit_test.go b/coderd/audit_test.go index 509744ecbf..6c21a1363c 100644 --- a/coderd/audit_test.go +++ b/coderd/audit_test.go @@ -95,92 +95,6 @@ func TestAuditLogs(t *testing.T) { require.Equal(t, foundUser, *alogs.AuditLogs[0].User) }) - t.Run("IncludeOrganization", func(t *testing.T) { - t.Parallel() - - ctx := context.Background() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "new-org", - DisplayName: "New organization", - Description: "A new organization to love and cherish until the test is over.", - Icon: "/emojis/1f48f-1f3ff.png", - }) - require.NoError(t, err) - - err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - OrganizationID: o.ID, - ResourceID: user.UserID, - }) - require.NoError(t, err) - - alogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{ - Pagination: codersdk.Pagination{ - Limit: 1, - }, - }) - require.NoError(t, err) - require.Equal(t, int64(1), alogs.Count) - require.Len(t, alogs.AuditLogs, 1) - - // Make sure the organization is fully populated. - require.Equal(t, &codersdk.MinimalOrganization{ - ID: o.ID, - Name: o.Name, - DisplayName: o.DisplayName, - Icon: o.Icon, - }, alogs.AuditLogs[0].Organization) - - // OrganizationID is deprecated, but make sure it is set. - require.Equal(t, o.ID, alogs.AuditLogs[0].OrganizationID) - - // Delete the org and try again, should be mostly empty. - err = client.DeleteOrganization(ctx, o.ID.String()) - require.NoError(t, err) - - alogs, err = client.AuditLogs(ctx, codersdk.AuditLogsRequest{ - Pagination: codersdk.Pagination{ - Limit: 1, - }, - }) - require.NoError(t, err) - require.Equal(t, int64(1), alogs.Count) - require.Len(t, alogs.AuditLogs, 1) - - require.Equal(t, &codersdk.MinimalOrganization{ - ID: o.ID, - }, alogs.AuditLogs[0].Organization) - - // OrganizationID is deprecated, but make sure it is set. - require.Equal(t, o.ID, alogs.AuditLogs[0].OrganizationID) - - // Some audit entries do not have an organization at all, in which case the - // response omits the organization. - err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - ResourceType: codersdk.ResourceTypeAPIKey, - ResourceID: user.UserID, - }) - require.NoError(t, err) - - alogs, err = client.AuditLogs(ctx, codersdk.AuditLogsRequest{ - SearchQuery: "resource_type:api_key", - Pagination: codersdk.Pagination{ - Limit: 1, - }, - }) - require.NoError(t, err) - require.Equal(t, int64(1), alogs.Count) - require.Len(t, alogs.AuditLogs, 1) - - // The other will have no organization. - require.Equal(t, (*codersdk.MinimalOrganization)(nil), alogs.AuditLogs[0].Organization) - - // OrganizationID is deprecated, but make sure it is empty. - require.Equal(t, uuid.Nil, alogs.AuditLogs[0].OrganizationID) - }) - t.Run("WorkspaceBuildAuditLink", func(t *testing.T) { t.Parallel() diff --git a/coderd/coderd.go b/coderd/coderd.go index 26f2c66bb4..a62cdae08c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -864,15 +864,12 @@ func New(options *Options) *API { r.Use( apiKeyMiddleware, ) - r.Post("/", api.postOrganizations) r.Get("/", api.organizations) r.Route("/{organization}", func(r chi.Router) { r.Use( httpmw.ExtractOrganizationParam(options.Database), ) r.Get("/", api.organization) - r.Patch("/", api.patchOrganization) - r.Delete("/", api.deleteOrganization) r.Post("/templateversions", api.postTemplateVersionsByOrganization) r.Route("/templates", func(r chi.Router) { r.Post("/", api.postTemplateByOrganization) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index d27b392c14..883742aa5f 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -538,14 +538,18 @@ func NewWithAPI(t testing.TB, options *Options) (*codersdk.Client, io.Closer, *c return client, provisionerCloser, coderAPI } -// provisionerdCloser wraps a provisioner daemon as an io.Closer that can be called multiple times -type provisionerdCloser struct { +// ProvisionerdCloser wraps a provisioner daemon as an io.Closer that can be called multiple times +type ProvisionerdCloser struct { mu sync.Mutex closed bool d *provisionerd.Server } -func (c *provisionerdCloser) Close() error { +func NewProvisionerDaemonCloser(d *provisionerd.Server) *ProvisionerdCloser { + return &ProvisionerdCloser{d: d} +} + +func (c *ProvisionerdCloser) Close() error { c.mu.Lock() defer c.mu.Unlock() if c.closed { @@ -605,74 +609,13 @@ func NewTaggedProvisionerDaemon(t testing.TB, coderAPI *coderd.API, name string, string(database.ProvisionerTypeEcho): sdkproto.NewDRPCProvisionerClient(echoClient), }, }) - closer := &provisionerdCloser{d: daemon} + closer := NewProvisionerDaemonCloser(daemon) t.Cleanup(func() { _ = closer.Close() }) return closer } -func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string) io.Closer { - t.Helper() - - // Without this check, the provisioner will silently fail. - entitlements, err := client.Entitlements(context.Background()) - if err != nil { - // AGPL instances will throw this error. They cannot use external - // provisioners. - t.Errorf("external provisioners requires a license with entitlements. The client failed to fetch the entitlements, is this an enterprise instance of coderd?") - t.FailNow() - return nil - } - - feature := entitlements.Features[codersdk.FeatureExternalProvisionerDaemons] - if !feature.Enabled || feature.Entitlement != codersdk.EntitlementEntitled { - require.NoError(t, xerrors.Errorf("external provisioner daemons require an entitled license")) - return nil - } - - echoClient, echoServer := drpc.MemTransportPipe() - ctx, cancelFunc := context.WithCancel(context.Background()) - serveDone := make(chan struct{}) - t.Cleanup(func() { - _ = echoClient.Close() - _ = echoServer.Close() - cancelFunc() - <-serveDone - }) - go func() { - defer close(serveDone) - err := echo.Serve(ctx, &provisionersdk.ServeOptions{ - Listener: echoServer, - WorkDirectory: t.TempDir(), - }) - assert.NoError(t, err) - }() - - daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { - return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ - ID: uuid.New(), - Name: t.Name(), - Organization: org, - Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho}, - Tags: tags, - }) - }, &provisionerd.Options{ - Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug), - UpdateInterval: 250 * time.Millisecond, - ForceCancelInterval: 5 * time.Second, - Connector: provisionerd.LocalProvisioners{ - string(database.ProvisionerTypeEcho): sdkproto.NewDRPCProvisionerClient(echoClient), - }, - }) - closer := &provisionerdCloser{d: daemon} - t.Cleanup(func() { - _ = closer.Close() - }) - - return closer -} - var FirstUserParams = codersdk.CreateFirstUserRequest{ Email: "testuser@coder.com", Username: "testuser", @@ -841,37 +784,6 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI return other, user } -type CreateOrganizationOptions struct { - // IncludeProvisionerDaemon will spin up an external provisioner for the organization. - // This requires enterprise and the feature 'codersdk.FeatureExternalProvisionerDaemons' - IncludeProvisionerDaemon bool -} - -func CreateOrganization(t *testing.T, client *codersdk.Client, opts CreateOrganizationOptions, mutators ...func(*codersdk.CreateOrganizationRequest)) codersdk.Organization { - ctx := testutil.Context(t, testutil.WaitMedium) - req := codersdk.CreateOrganizationRequest{ - Name: strings.ReplaceAll(strings.ToLower(namesgenerator.GetRandomName(0)), "_", "-"), - DisplayName: namesgenerator.GetRandomName(1), - Description: namesgenerator.GetRandomName(1), - Icon: "", - } - for _, mutator := range mutators { - mutator(&req) - } - - org, err := client.CreateOrganization(ctx, req) - require.NoError(t, err) - - if opts.IncludeProvisionerDaemon { - closer := NewExternalProvisionerDaemon(t, client, org.ID, map[string]string{}) - t.Cleanup(func() { - _ = closer.Close() - }) - } - - return org -} - // CreateTemplateVersion creates a template import provisioner job // with the responses provided. It uses the "echo" provisioner for compatibility // with testing. diff --git a/coderd/coderdtest/coderdtest_test.go b/coderd/coderdtest/coderdtest_test.go index 4d03f21ef8..455a03dc11 100644 --- a/coderd/coderdtest/coderdtest_test.go +++ b/coderd/coderdtest/coderdtest_test.go @@ -3,12 +3,9 @@ package coderdtest_test import ( "testing" - "github.com/google/uuid" - "github.com/stretchr/testify/require" "go.uber.org/goleak" "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/rbac" ) func TestMain(m *testing.M) { @@ -30,20 +27,3 @@ func TestNew(t *testing.T) { _, _ = coderdtest.NewGoogleInstanceIdentity(t, "example", false) _, _ = coderdtest.NewAWSInstanceIdentity(t, "an-instance") } - -// TestOrganizationMember checks the coderdtest helper can add organization members -// to multiple orgs. -func TestOrganizationMember(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, &coderdtest.Options{}) - owner := coderdtest.CreateFirstUser(t, client) - - second := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) - third := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) - - // Assign the user to 3 orgs in this 1 statement - _, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgMember(second.ID), rbac.ScopedRoleOrgMember(third.ID)) - require.Len(t, user.OrganizationIDs, 3) - require.ElementsMatch(t, user.OrganizationIDs, []uuid.UUID{owner.OrganizationID, second.ID, third.ID}) -} diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 10149dac44..818793182e 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -586,3 +586,18 @@ func RBACPermission(permission rbac.Permission) codersdk.Permission { Action: codersdk.RBACAction(permission.Action), } } + +func Organization(organization database.Organization) codersdk.Organization { + return codersdk.Organization{ + MinimalOrganization: codersdk.MinimalOrganization{ + ID: organization.ID, + Name: organization.Name, + DisplayName: organization.DisplayName, + Icon: organization.Icon, + }, + Description: organization.Description, + CreatedAt: organization.CreatedAt, + UpdatedAt: organization.UpdatedAt, + IsDefault: organization.IsDefault, + } +} diff --git a/coderd/members_test.go b/coderd/members_test.go index af8196e6da..8ca655590c 100644 --- a/coderd/members_test.go +++ b/coderd/members_test.go @@ -1,7 +1,6 @@ package coderd_test import ( - "net/http" "testing" "github.com/google/uuid" @@ -17,42 +16,6 @@ import ( func TestAddMember(t *testing.T) { t.Parallel() - t.Run("OK", func(t *testing.T) { - t.Parallel() - owner := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, owner) - ctx := testutil.Context(t, testutil.WaitMedium) - org, err := owner.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "other", - DisplayName: "", - Description: "", - Icon: "", - }) - require.NoError(t, err) - - // Make a user not in the second organization - _, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID) - - // Use scoped user admin in org to add the user - client, userAdmin := coderdtest.CreateAnotherUser(t, owner, org.ID, rbac.ScopedRoleOrgUserAdmin(org.ID)) - - members, err := client.OrganizationMembers(ctx, org.ID) - require.NoError(t, err) - require.Len(t, members, 2) // Verify the 2 members at the start - - // Add user to org - _, err = client.PostOrganizationMember(ctx, org.ID, user.Username) - require.NoError(t, err) - - members, err = client.OrganizationMembers(ctx, org.ID) - require.NoError(t, err) - // Owner + user admin + new member - require.Len(t, members, 3) - require.ElementsMatch(t, - []uuid.UUID{first.UserID, user.ID, userAdmin.ID}, - db2sdk.List(members, onlyIDs)) - }) - t.Run("AlreadyMember", func(t *testing.T) { t.Parallel() owner := coderdtest.New(t, nil) @@ -65,28 +28,6 @@ func TestAddMember(t *testing.T) { _, err := owner.PostOrganizationMember(ctx, first.OrganizationID, user.Username) require.ErrorContains(t, err, "already exists") }) - - t.Run("UserNotExists", func(t *testing.T) { - t.Parallel() - owner := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, owner) - ctx := testutil.Context(t, testutil.WaitMedium) - - org, err := owner.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "other", - DisplayName: "", - Description: "", - Icon: "", - }) - require.NoError(t, err) - - // Add user to org - _, err = owner.PostOrganizationMember(ctx, org.ID, uuid.NewString()) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Contains(t, apiErr.Message, "must be an existing") - }) } func TestListMembers(t *testing.T) { @@ -107,28 +48,6 @@ func TestListMembers(t *testing.T) { []uuid.UUID{first.UserID, user.ID}, db2sdk.List(members, onlyIDs)) }) - - // Calling it from a user without the org access. - t.Run("NotInOrg", func(t *testing.T) { - t.Parallel() - owner := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, owner) - - client, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID)) - - ctx := testutil.Context(t, testutil.WaitShort) - org, err := owner.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "test", - DisplayName: "", - Description: "", - }) - require.NoError(t, err, "create organization") - - // 404 error is expected instead of a 403/401 to not leak existence of - // an organization. - _, err = client.OrganizationMembers(ctx, org.ID) - require.ErrorContains(t, err, "404") - }) } func TestRemoveMember(t *testing.T) { @@ -161,31 +80,6 @@ func TestRemoveMember(t *testing.T) { []uuid.UUID{first.UserID, orgAdmin.ID}, db2sdk.List(members, onlyIDs)) }) - - t.Run("MemberNotInOrg", func(t *testing.T) { - t.Parallel() - owner := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, owner) - orgAdminClient, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID)) - - ctx := testutil.Context(t, testutil.WaitMedium) - // nolint:gocritic // requires owner to make a new org - org, _ := owner.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "other", - DisplayName: "", - Description: "", - Icon: "", - }) - - _, user := coderdtest.CreateAnotherUser(t, owner, org.ID) - - // Delete a user that is not in the organization - err := orgAdminClient.DeleteOrganizationMember(ctx, first.OrganizationID, user.Username) - require.Error(t, err) - var apiError *codersdk.Error - require.ErrorAs(t, err, &apiError) - require.Equal(t, http.StatusNotFound, apiError.StatusCode()) - }) } func onlyIDs(u codersdk.OrganizationMemberWithUserData) uuid.UUID { diff --git a/coderd/organizations.go b/coderd/organizations.go index 49ea77a00f..2acd3fe401 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -1,18 +1,9 @@ package coderd import ( - "database/sql" - "errors" - "fmt" "net/http" - "github.com/google/uuid" - "golang.org/x/xerrors" - - "github.com/coder/coder/v2/coderd/audit" - "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" - "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/codersdk" @@ -40,7 +31,7 @@ func (api *API) organizations(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(organizations, convertOrganization)) + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(organizations, db2sdk.Organization)) } // @Summary Get organization by ID @@ -55,275 +46,5 @@ func (*API) organization(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organization := httpmw.OrganizationParam(r) - httpapi.Write(ctx, rw, http.StatusOK, convertOrganization(organization)) -} - -// @Summary Create organization -// @ID create-organization -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Organizations -// @Param request body codersdk.CreateOrganizationRequest true "Create organization request" -// @Success 201 {object} codersdk.Organization -// @Router /organizations [post] -func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) { - var ( - // organizationID is required before the audit log entry is created. - organizationID = uuid.New() - ctx = r.Context() - apiKey = httpmw.APIKey(r) - auditor = api.Auditor.Load() - aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{ - Audit: *auditor, - Log: api.Logger, - Request: r, - Action: database.AuditActionCreate, - OrganizationID: organizationID, - }) - ) - aReq.Old = database.Organization{} - defer commitAudit() - - var req codersdk.CreateOrganizationRequest - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - if req.Name == codersdk.DefaultOrganization { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), - }) - return - } - - _, err := api.Database.GetOrganizationByName(ctx, req.Name) - if err == nil { - httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ - Message: "Organization already exists with that name.", - }) - return - } - if !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: fmt.Sprintf("Internal error fetching organization %q.", req.Name), - Detail: err.Error(), - }) - return - } - - var organization database.Organization - err = api.Database.InTx(func(tx database.Store) error { - if req.DisplayName == "" { - req.DisplayName = req.Name - } - - organization, err = tx.InsertOrganization(ctx, database.InsertOrganizationParams{ - ID: organizationID, - Name: req.Name, - DisplayName: req.DisplayName, - Description: req.Description, - Icon: req.Icon, - CreatedAt: dbtime.Now(), - UpdatedAt: dbtime.Now(), - }) - if err != nil { - return xerrors.Errorf("create organization: %w", err) - } - _, err = tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{ - OrganizationID: organization.ID, - UserID: apiKey.UserID, - CreatedAt: dbtime.Now(), - UpdatedAt: dbtime.Now(), - Roles: []string{ - // TODO: When organizations are allowed to be created, we should - // come back to determining the default role of the person who - // creates the org. Until that happens, all users in an organization - // should be just regular members. - }, - }) - if err != nil { - return xerrors.Errorf("create organization admin: %w", err) - } - - _, err = tx.InsertAllUsersGroup(ctx, organization.ID) - if err != nil { - return xerrors.Errorf("create %q group: %w", database.EveryoneGroup, err) - } - return nil - }, nil) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error inserting organization member.", - Detail: err.Error(), - }) - return - } - - aReq.New = organization - httpapi.Write(ctx, rw, http.StatusCreated, convertOrganization(organization)) -} - -// @Summary Update organization -// @ID update-organization -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Organizations -// @Param organization path string true "Organization ID or name" -// @Param request body codersdk.UpdateOrganizationRequest true "Patch organization request" -// @Success 200 {object} codersdk.Organization -// @Router /organizations/{organization} [patch] -func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - organization = httpmw.OrganizationParam(r) - auditor = api.Auditor.Load() - aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{ - Audit: *auditor, - Log: api.Logger, - Request: r, - Action: database.AuditActionWrite, - OrganizationID: organization.ID, - }) - ) - aReq.Old = organization - defer commitAudit() - - var req codersdk.UpdateOrganizationRequest - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - // "default" is a reserved name that always refers to the default org (much like the way we - // use "me" for users). - if req.Name == codersdk.DefaultOrganization { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), - }) - return - } - - err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { - var err error - organization, err = tx.GetOrganizationByID(ctx, organization.ID) - if err != nil { - return err - } - - updateOrgParams := database.UpdateOrganizationParams{ - UpdatedAt: dbtime.Now(), - ID: organization.ID, - Name: organization.Name, - DisplayName: organization.DisplayName, - Description: organization.Description, - Icon: organization.Icon, - } - - if req.Name != "" { - updateOrgParams.Name = req.Name - } - if req.DisplayName != "" { - updateOrgParams.DisplayName = req.DisplayName - } - if req.Description != nil { - updateOrgParams.Description = *req.Description - } - if req.Icon != nil { - updateOrgParams.Icon = *req.Icon - } - - organization, err = tx.UpdateOrganization(ctx, updateOrgParams) - if err != nil { - return err - } - return nil - }) - - if httpapi.Is404Error(err) { - httpapi.ResourceNotFound(rw) - return - } - if database.IsUniqueViolation(err) { - httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ - Message: fmt.Sprintf("Organization already exists with the name %q.", req.Name), - Validations: []codersdk.ValidationError{{ - Field: "name", - Detail: "This value is already in use and should be unique.", - }}, - }) - return - } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error updating organization.", - Detail: fmt.Sprintf("update organization: %s", err.Error()), - }) - return - } - - aReq.New = organization - httpapi.Write(ctx, rw, http.StatusOK, convertOrganization(organization)) -} - -// @Summary Delete organization -// @ID delete-organization -// @Security CoderSessionToken -// @Produce json -// @Tags Organizations -// @Param organization path string true "Organization ID or name" -// @Success 200 {object} codersdk.Response -// @Router /organizations/{organization} [delete] -func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - organization = httpmw.OrganizationParam(r) - auditor = api.Auditor.Load() - aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{ - Audit: *auditor, - Log: api.Logger, - Request: r, - Action: database.AuditActionDelete, - OrganizationID: organization.ID, - }) - ) - aReq.Old = organization - defer commitAudit() - - if organization.IsDefault { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Default organization cannot be deleted.", - }) - return - } - - err := api.Database.DeleteOrganization(ctx, organization.ID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error deleting organization.", - Detail: fmt.Sprintf("delete organization: %s", err.Error()), - }) - return - } - - aReq.New = database.Organization{} - httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{ - Message: "Organization has been deleted.", - }) -} - -// convertOrganization consumes the database representation and outputs an API friendly representation. -func convertOrganization(organization database.Organization) codersdk.Organization { - return codersdk.Organization{ - MinimalOrganization: codersdk.MinimalOrganization{ - ID: organization.ID, - Name: organization.Name, - DisplayName: organization.DisplayName, - Icon: organization.Icon, - }, - Description: organization.Description, - CreatedAt: organization.CreatedAt, - UpdatedAt: organization.UpdatedAt, - IsDefault: organization.IsDefault, - } + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Organization(organization)) } diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go index 47c8415fee..c6a26c1f86 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -7,58 +7,10 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) -func TestMultiOrgFetch(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitLong) - - makeOrgs := []string{"foo", "bar", "baz"} - for _, name := range makeOrgs { - _, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: name, - DisplayName: name, - }) - require.NoError(t, err) - } - - myOrgs, err := client.OrganizationsByUser(ctx, codersdk.Me) - require.NoError(t, err) - require.NotNil(t, myOrgs) - require.Len(t, myOrgs, len(makeOrgs)+1) - - orgs, err := client.Organizations(ctx) - require.NoError(t, err) - require.NotNil(t, orgs) - require.ElementsMatch(t, myOrgs, orgs) -} - -func TestOrganizationsByUser(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitLong) - - orgs, err := client.OrganizationsByUser(ctx, codersdk.Me) - require.NoError(t, err) - require.NotNil(t, orgs) - require.Len(t, orgs, 1) - require.True(t, orgs[0].IsDefault, "first org is always default") - - // Make an extra org, and it should not be defaulted. - notDefault, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "another", - DisplayName: "Another", - }) - require.NoError(t, err) - require.False(t, notDefault.IsDefault, "only 1 default org allowed") -} - func TestOrganizationByUserAndName(t *testing.T) { t.Parallel() t.Run("NoExist", func(t *testing.T) { @@ -73,24 +25,6 @@ func TestOrganizationByUserAndName(t *testing.T) { require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) - t.Run("NoMember", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) - other, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) - ctx := testutil.Context(t, testutil.WaitLong) - - org, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "another", - DisplayName: "Another", - }) - require.NoError(t, err) - _, err = other.OrganizationByUserAndName(ctx, codersdk.Me, org.Name) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - }) - t.Run("Valid", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) @@ -103,289 +37,3 @@ func TestOrganizationByUserAndName(t *testing.T) { require.NoError(t, err) }) } - -func TestPostOrganizationsByUser(t *testing.T) { - t.Parallel() - t.Run("Conflict", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitLong) - - org, err := client.Organization(ctx, user.OrganizationID) - require.NoError(t, err) - _, err = client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: org.Name, - DisplayName: org.DisplayName, - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusConflict, apiErr.StatusCode()) - }) - - t.Run("InvalidName", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitLong) - - _, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "A name which is definitely not url safe", - DisplayName: "New", - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - }) - - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitLong) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "new-org", - DisplayName: "New organization", - Description: "A new organization to love and cherish forever.", - Icon: "/emojis/1f48f-1f3ff.png", - }) - require.NoError(t, err) - require.Equal(t, "new-org", o.Name) - require.Equal(t, "New organization", o.DisplayName) - require.Equal(t, "A new organization to love and cherish forever.", o.Description) - require.Equal(t, "/emojis/1f48f-1f3ff.png", o.Icon) - }) - - t.Run("CreateWithoutExplicitDisplayName", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitLong) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "new-org", - }) - require.NoError(t, err) - require.Equal(t, "new-org", o.Name) - require.Equal(t, "new-org", o.DisplayName) // should match the given `Name` - }) -} - -func TestPatchOrganizationsByUser(t *testing.T) { - t.Parallel() - t.Run("Conflict", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - originalOrg, err := client.Organization(ctx, user.OrganizationID) - require.NoError(t, err) - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "something-unique", - DisplayName: "Something Unique", - }) - require.NoError(t, err) - - _, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ - Name: originalOrg.Name, - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusConflict, apiErr.StatusCode()) - }) - - t.Run("ReservedName", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "something-unique", - DisplayName: "Something Unique", - }) - require.NoError(t, err) - - _, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ - Name: codersdk.DefaultOrganization, - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - }) - - t.Run("InvalidName", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "something-unique", - DisplayName: "Something Unique", - }) - require.NoError(t, err) - - _, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ - Name: "something unique but not url safe", - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - }) - - t.Run("UpdateById", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "new-org", - DisplayName: "New organization", - }) - require.NoError(t, err) - - o, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ - Name: "new-new-org", - }) - require.NoError(t, err) - require.Equal(t, "new-new-org", o.Name) - }) - - t.Run("UpdateByName", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "new-org", - DisplayName: "New organization", - }) - require.NoError(t, err) - - o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{ - Name: "new-new-org", - }) - require.NoError(t, err) - require.Equal(t, "new-new-org", o.Name) - require.Equal(t, "New organization", o.DisplayName) // didn't change - }) - - t.Run("UpdateDisplayName", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "new-org", - DisplayName: "New organization", - }) - require.NoError(t, err) - - o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{ - DisplayName: "The Newest One", - }) - require.NoError(t, err) - require.Equal(t, "new-org", o.Name) // didn't change - require.Equal(t, "The Newest One", o.DisplayName) - }) - - t.Run("UpdateDescription", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "new-org", - DisplayName: "New organization", - }) - require.NoError(t, err) - - o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{ - Description: ptr.Ref("wow, this organization description is so updated!"), - }) - - require.NoError(t, err) - require.Equal(t, "new-org", o.Name) // didn't change - require.Equal(t, "New organization", o.DisplayName) // didn't change - require.Equal(t, "wow, this organization description is so updated!", o.Description) - }) - - t.Run("UpdateIcon", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "new-org", - DisplayName: "New organization", - }) - require.NoError(t, err) - - o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{ - Icon: ptr.Ref("/emojis/1f48f-1f3ff.png"), - }) - - require.NoError(t, err) - require.Equal(t, "new-org", o.Name) // didn't change - require.Equal(t, "New organization", o.DisplayName) // didn't change - require.Equal(t, "/emojis/1f48f-1f3ff.png", o.Icon) - }) -} - -func TestDeleteOrganizationsByUser(t *testing.T) { - t.Parallel() - t.Run("Default", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - o, err := client.Organization(ctx, user.OrganizationID) - require.NoError(t, err) - - err = client.DeleteOrganization(ctx, o.ID.String()) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - }) - - t.Run("DeleteById", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "doomed", - DisplayName: "Doomed", - }) - require.NoError(t, err) - - err = client.DeleteOrganization(ctx, o.ID.String()) - require.NoError(t, err) - }) - - t.Run("DeleteByName", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - ctx := testutil.Context(t, testutil.WaitMedium) - - o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "doomed", - DisplayName: "Doomed", - }) - require.NoError(t, err) - - err = client.DeleteOrganization(ctx, o.Name) - require.NoError(t, err) - }) -} diff --git a/coderd/roles_test.go b/coderd/roles_test.go index 9453f610c6..3f98d67454 100644 --- a/coderd/roles_test.go +++ b/coderd/roles_test.go @@ -1,8 +1,6 @@ package coderd_test import ( - "context" - "net/http" "slices" "testing" @@ -11,7 +9,6 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -19,157 +16,6 @@ import ( "github.com/coder/coder/v2/testutil" ) -func TestListRoles(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, nil) - // Create owner, member, and org admin - owner := coderdtest.CreateFirstUser(t, client) - member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - t.Cleanup(cancel) - - otherOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "other", - }) - require.NoError(t, err, "create org") - - const notFound = "Resource not found" - testCases := []struct { - Name string - Client *codersdk.Client - APICall func(context.Context) ([]codersdk.AssignableRoles, error) - ExpectedRoles []codersdk.AssignableRoles - AuthorizedError string - }{ - { - // Members cannot assign any roles - Name: "MemberListSite", - APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { - x, err := member.ListSiteRoles(ctx) - return x, err - }, - ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOwner}: false, - {Name: codersdk.RoleAuditor}: false, - {Name: codersdk.RoleTemplateAdmin}: false, - {Name: codersdk.RoleUserAdmin}: false, - }), - }, - { - Name: "OrgMemberListOrg", - APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { - return member.ListOrganizationRoles(ctx, owner.OrganizationID) - }, - ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: false, - {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: false, - {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: false, - {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: false, - }), - }, - { - Name: "NonOrgMemberListOrg", - APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { - return member.ListOrganizationRoles(ctx, otherOrg.ID) - }, - AuthorizedError: notFound, - }, - // Org admin - { - Name: "OrgAdminListSite", - APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { - return orgAdmin.ListSiteRoles(ctx) - }, - ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOwner}: false, - {Name: codersdk.RoleAuditor}: false, - {Name: codersdk.RoleTemplateAdmin}: false, - {Name: codersdk.RoleUserAdmin}: false, - }), - }, - { - Name: "OrgAdminListOrg", - APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { - return orgAdmin.ListOrganizationRoles(ctx, owner.OrganizationID) - }, - ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, - }), - }, - { - Name: "OrgAdminListOtherOrg", - APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { - return orgAdmin.ListOrganizationRoles(ctx, otherOrg.ID) - }, - AuthorizedError: notFound, - }, - // Admin - { - Name: "AdminListSite", - APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { - return client.ListSiteRoles(ctx) - }, - ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOwner}: true, - {Name: codersdk.RoleAuditor}: true, - {Name: codersdk.RoleTemplateAdmin}: true, - {Name: codersdk.RoleUserAdmin}: true, - }), - }, - { - Name: "AdminListOrg", - APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { - return client.ListOrganizationRoles(ctx, owner.OrganizationID) - }, - ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, - }), - }, - } - - for _, c := range testCases { - c := c - t.Run(c.Name, func(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - roles, err := c.APICall(ctx) - if c.AuthorizedError != "" { - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - require.Contains(t, apiErr.Message, c.AuthorizedError) - } else { - require.NoError(t, err) - ignorePerms := func(f codersdk.AssignableRoles) codersdk.AssignableRoles { - return codersdk.AssignableRoles{ - Role: codersdk.Role{ - Name: f.Name, - DisplayName: f.DisplayName, - }, - Assignable: f.Assignable, - BuiltIn: true, - } - } - expected := db2sdk.List(c.ExpectedRoles, ignorePerms) - found := db2sdk.List(roles, ignorePerms) - require.ElementsMatch(t, expected, found) - } - }) - } -} - func TestListCustomRoles(t *testing.T) { t.Parallel() @@ -208,20 +54,3 @@ func TestListCustomRoles(t *testing.T) { require.Truef(t, found, "custom organization role listed") }) } - -func convertRole(roleName rbac.RoleIdentifier) codersdk.Role { - role, _ := rbac.RoleByName(roleName) - return db2sdk.RBACRole(role) -} - -func convertRoles(assignableRoles map[rbac.RoleIdentifier]bool) []codersdk.AssignableRoles { - converted := make([]codersdk.AssignableRoles, 0, len(assignableRoles)) - for roleName, assignable := range assignableRoles { - role := convertRole(roleName) - converted = append(converted, codersdk.AssignableRoles{ - Role: role, - Assignable: assignable, - }) - } - return converted -} diff --git a/coderd/templates_test.go b/coderd/templates_test.go index f0decd549c..c27a8a616b 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -461,47 +461,6 @@ func TestTemplatesByOrganization(t *testing.T) { require.Equal(t, tmpl.OrganizationIcon, org.Icon, "organization display name") } }) - t.Run("MultipleOrganizations", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - owner := coderdtest.CreateFirstUser(t, client) - org2 := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) - user, _ := coderdtest.CreateAnotherUser(t, client, org2.ID) - - // 2 templates in first organization - version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) - version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) - coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) - coderdtest.CreateTemplate(t, client, owner.OrganizationID, version2.ID) - - // 2 in the second organization - version3 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil) - version4 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil) - coderdtest.CreateTemplate(t, client, org2.ID, version3.ID) - coderdtest.CreateTemplate(t, client, org2.ID, version4.ID) - - ctx := testutil.Context(t, testutil.WaitLong) - - // All 4 are viewable by the owner - templates, err := client.Templates(ctx, codersdk.TemplateFilter{}) - require.NoError(t, err) - require.Len(t, templates, 4) - - // View a single organization from the owner - templates, err = client.Templates(ctx, codersdk.TemplateFilter{ - OrganizationID: owner.OrganizationID, - }) - require.NoError(t, err) - require.Len(t, templates, 2) - - // Only 2 are viewable by the org user - templates, err = user.Templates(ctx, codersdk.TemplateFilter{}) - require.NoError(t, err) - require.Len(t, templates, 2) - for _, tmpl := range templates { - require.Equal(t, tmpl.OrganizationName, org2.Name, "organization name on template") - } - }) } func TestTemplateByOrganizationAndName(t *testing.T) { diff --git a/coderd/users.go b/coderd/users.go index bf06bba694..87ee3d2772 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1167,12 +1167,7 @@ func (api *API) organizationsByUser(rw http.ResponseWriter, r *http.Request) { return } - publicOrganizations := make([]codersdk.Organization, 0, len(organizations)) - for _, organization := range organizations { - publicOrganizations = append(publicOrganizations, convertOrganization(organization)) - } - - httpapi.Write(ctx, rw, http.StatusOK, publicOrganizations) + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(organizations, db2sdk.Organization)) } // @Summary Get organization by user and organization name @@ -1200,7 +1195,7 @@ func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques return } - httpapi.Write(ctx, rw, http.StatusOK, convertOrganization(organization)) + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Organization(organization)) } type CreateUserRequest struct { diff --git a/coderd/users_test.go b/coderd/users_test.go index af4a2c2975..7c19096105 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -480,65 +480,6 @@ func TestPostUsers(t *testing.T) { require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) - t.Run("OrganizationNoAccess", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) - notInOrg, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) - other, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleOwner(), rbac.RoleMember()) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - org, err := other.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "another", - }) - require.NoError(t, err) - - _, err = notInOrg.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "some@domain.com", - Username: "anotheruser", - Password: "SomeSecurePassword!", - OrganizationID: org.ID, - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - }) - - t.Run("CreateWithoutOrg", func(t *testing.T) { - t.Parallel() - auditor := audit.NewMock() - client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor}) - firstUser := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - // Add an extra org to try and confuse user creation - _, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "foobar", - }) - require.NoError(t, err) - - numLogs := len(auditor.AuditLogs()) - - user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "another@user.org", - Username: "someone-else", - Password: "SomeSecurePassword!", - }) - require.NoError(t, err) - numLogs++ // add an audit log for user create - - require.Len(t, auditor.AuditLogs(), numLogs) - require.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[numLogs-1].Action) - require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-3].Action) - - require.Len(t, user.OrganizationIDs, 1) - assert.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0]) - }) - t.Run("Create", func(t *testing.T) { t.Parallel() auditor := audit.NewMock() @@ -990,175 +931,6 @@ func TestUpdateUserPassword(t *testing.T) { }) } -func TestGrantSiteRoles(t *testing.T) { - t.Parallel() - - requireStatusCode := func(t *testing.T, err error, statusCode int) { - t.Helper() - var e *codersdk.Error - require.ErrorAs(t, err, &e, "error is codersdk error") - require.Equal(t, statusCode, e.StatusCode(), "correct status code") - } - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - t.Cleanup(cancel) - var err error - - admin := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, admin) - member, _ := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID) - orgAdmin, _ := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID)) - randOrg, err := admin.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "random", - }) - require.NoError(t, err) - _, randOrgUser := coderdtest.CreateAnotherUser(t, admin, randOrg.ID, rbac.ScopedRoleOrgAdmin(randOrg.ID)) - userAdmin, _ := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID, rbac.RoleUserAdmin()) - - const newUser = "newUser" - - testCases := []struct { - Name string - Client *codersdk.Client - OrgID uuid.UUID - AssignToUser string - Roles []string - ExpectedRoles []string - Error bool - StatusCode int - }{ - { - Name: "OrgRoleInSite", - Client: admin, - AssignToUser: codersdk.Me, - Roles: []string{rbac.RoleOrgAdmin()}, - Error: true, - StatusCode: http.StatusBadRequest, - }, - { - Name: "UserNotExists", - Client: admin, - AssignToUser: uuid.NewString(), - Roles: []string{codersdk.RoleOwner}, - Error: true, - StatusCode: http.StatusBadRequest, - }, - { - Name: "MemberCannotUpdateRoles", - Client: member, - AssignToUser: first.UserID.String(), - Roles: []string{}, - Error: true, - StatusCode: http.StatusBadRequest, - }, - { - // Cannot update your own roles - Name: "AdminOnSelf", - Client: admin, - AssignToUser: first.UserID.String(), - Roles: []string{}, - Error: true, - StatusCode: http.StatusBadRequest, - }, - { - Name: "SiteRoleInOrg", - Client: admin, - OrgID: first.OrganizationID, - AssignToUser: codersdk.Me, - Roles: []string{codersdk.RoleOwner}, - Error: true, - StatusCode: http.StatusBadRequest, - }, - { - Name: "RoleInNotMemberOrg", - Client: orgAdmin, - OrgID: randOrg.ID, - AssignToUser: randOrgUser.ID.String(), - Roles: []string{rbac.RoleOrgMember()}, - Error: true, - StatusCode: http.StatusNotFound, - }, - { - Name: "AdminUpdateOrgSelf", - Client: admin, - OrgID: first.OrganizationID, - AssignToUser: first.UserID.String(), - Roles: []string{}, - Error: true, - StatusCode: http.StatusBadRequest, - }, - { - Name: "OrgAdminPromote", - Client: orgAdmin, - OrgID: first.OrganizationID, - AssignToUser: newUser, - Roles: []string{rbac.RoleOrgAdmin()}, - ExpectedRoles: []string{ - rbac.RoleOrgAdmin(), - }, - Error: false, - }, - { - Name: "UserAdminMakeMember", - Client: userAdmin, - AssignToUser: newUser, - Roles: []string{codersdk.RoleMember}, - ExpectedRoles: []string{ - codersdk.RoleMember, - }, - Error: false, - }, - } - - for _, c := range testCases { - c := c - t.Run(c.Name, func(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - var err error - if c.AssignToUser == newUser { - orgID := first.OrganizationID - if c.OrgID != uuid.Nil { - orgID = c.OrgID - } - _, newUser := coderdtest.CreateAnotherUser(t, admin, orgID) - c.AssignToUser = newUser.ID.String() - } - - var newRoles []codersdk.SlimRole - if c.OrgID != uuid.Nil { - // Org assign - var mem codersdk.OrganizationMember - mem, err = c.Client.UpdateOrganizationMemberRoles(ctx, c.OrgID, c.AssignToUser, codersdk.UpdateRoles{ - Roles: c.Roles, - }) - newRoles = mem.Roles - } else { - // Site assign - var user codersdk.User - user, err = c.Client.UpdateUserRoles(ctx, c.AssignToUser, codersdk.UpdateRoles{ - Roles: c.Roles, - }) - newRoles = user.Roles - } - - if c.Error { - require.Error(t, err) - requireStatusCode(t, err, c.StatusCode) - } else { - require.NoError(t, err) - roles := make([]string, 0, len(newRoles)) - for _, r := range newRoles { - roles = append(roles, r.Name) - } - require.ElementsMatch(t, roles, c.ExpectedRoles) - } - }) - } -} - // TestInitialRoles ensures the starting roles for the first user are correct. func TestInitialRoles(t *testing.T) { t.Parallel() diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index 308c451e87..1d00b7daa7 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -2,7 +2,6 @@ package coderd_test import ( "context" - "net" "net/http" "net/url" "testing" @@ -13,12 +12,9 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" - "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/workspaceapps" - "github.com/coder/coder/v2/coderd/workspaceapps/apptest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" - "github.com/coder/serpent" ) func TestGetAppHost(t *testing.T) { @@ -248,51 +244,3 @@ func TestWorkspaceApplicationAuth(t *testing.T) { }) } } - -func TestWorkspaceApps(t *testing.T) { - t.Parallel() - - apptest.Run(t, true, func(t *testing.T, opts *apptest.DeploymentOptions) *apptest.Deployment { - deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.DisablePathApps = serpent.Bool(opts.DisablePathApps) - deploymentValues.Dangerous.AllowPathAppSharing = serpent.Bool(opts.DangerousAllowPathAppSharing) - deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = serpent.Bool(opts.DangerousAllowPathAppSiteOwnerAccess) - - if opts.DisableSubdomainApps { - opts.AppHost = "" - } - - flushStatsCollectorCh := make(chan chan<- struct{}, 1) - opts.StatsCollectorOptions.Flush = flushStatsCollectorCh - flushStats := func() { - flushStatsCollectorDone := make(chan struct{}, 1) - flushStatsCollectorCh <- flushStatsCollectorDone - <-flushStatsCollectorDone - } - client := coderdtest.New(t, &coderdtest.Options{ - DeploymentValues: deploymentValues, - AppHostname: opts.AppHost, - IncludeProvisionerDaemon: true, - RealIPConfig: &httpmw.RealIPConfig{ - TrustedOrigins: []*net.IPNet{{ - IP: net.ParseIP("127.0.0.1"), - Mask: net.CIDRMask(8, 32), - }}, - TrustedHeaders: []string{ - "CF-Connecting-IP", - }, - }, - WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions, - }) - - user := coderdtest.CreateFirstUser(t, client) - - return &apptest.Deployment{ - Options: opts, - SDKClient: client, - FirstUser: user, - PathAppBaseURL: client.URL, - FlushStats: flushStats, - } - }) -} diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index bd158d3893..c2a8c3c9c4 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -446,45 +446,6 @@ func TestResolveAutostart(t *testing.T) { require.False(t, resolveResp.ParameterMismatch) } -func TestAdminViewAllWorkspaces(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - _, err := client.Workspace(ctx, workspace.ID) - require.NoError(t, err) - - otherOrg, err := client.CreateOrganization(ctx, 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. - otherOwner, _ := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.RoleOwner()) - otherWorkspaces, err := otherOwner.Workspaces(ctx, codersdk.WorkspaceFilter{}) - require.NoError(t, err, "(other) fetch workspaces") - - firstWorkspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{}) - require.NoError(t, err, "(first) fetch workspaces") - - require.ElementsMatch(t, otherWorkspaces.Workspaces, firstWorkspaces.Workspaces) - require.Equal(t, len(firstWorkspaces.Workspaces), 1, "should be 1 workspace present") - - memberView, _ := coderdtest.CreateAnotherUser(t, client, otherOrg.ID) - memberViewWorkspaces, err := memberView.Workspaces(ctx, codersdk.WorkspaceFilter{}) - require.NoError(t, err, "(member) fetch workspaces") - require.Equal(t, 0, len(memberViewWorkspaces.Workspaces), "member in other org should see 0 workspaces") -} - func TestWorkspacesSortOrder(t *testing.T) { t.Parallel() @@ -589,32 +550,6 @@ func TestPostWorkspacesByOrganization(t *testing.T) { require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) }) - t.Run("NoTemplateAccess", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) - other, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleMember(), rbac.RoleOwner()) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - org, err := other.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "another", - }) - require.NoError(t, err) - version := coderdtest.CreateTemplateVersion(t, other, org.ID, nil) - template := coderdtest.CreateTemplate(t, other, org.ID, version.ID) - - _, err = client.CreateWorkspace(ctx, first.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{ - TemplateID: template.ID, - Name: "workspace", - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusForbidden, apiErr.StatusCode()) - }) - t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) diff --git a/enterprise/cli/create_test.go b/enterprise/cli/create_test.go index 085ecdf93c..ee0d7297c5 100644 --- a/enterprise/cli/create_test.go +++ b/enterprise/cli/create_test.go @@ -34,22 +34,26 @@ func TestEnterpriseCreate(t *testing.T) { secondTemplates []string } + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} // setupMultipleOrganizations creates an extra organization, assigns a member // both organizations, and optionally creates templates in each organization. setupMultipleOrganizations := func(t *testing.T, args setupArgs) setupData { ownerClient, first := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ + DeploymentValues: dv, // This only affects the first org. IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) - second := coderdtest.CreateOrganization(t, ownerClient, coderdtest.CreateOrganizationOptions{ + second := coderdenttest.CreateOrganization(t, ownerClient, coderdenttest.CreateOrganizationOptions{ IncludeProvisionerDaemon: true, }) member, _ := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID, rbac.ScopedRoleOrgMember(second.ID)) diff --git a/enterprise/cli/organization_test.go b/enterprise/cli/organization_test.go index 51571602d0..4b98cc90d7 100644 --- a/enterprise/cli/organization_test.go +++ b/enterprise/cli/organization_test.go @@ -9,9 +9,11 @@ import ( "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" "github.com/coder/coder/v2/enterprise/coderd/license" + "github.com/coder/coder/v2/pty/ptytest" "github.com/coder/coder/v2/testutil" ) @@ -110,3 +112,92 @@ func TestEditOrganizationRoles(t *testing.T) { require.ErrorContains(t, err, "not allowed to assign site wide permissions for an organization role") }) } + +func TestShowOrganizations(t *testing.T) { + t.Parallel() + + t.Run("OnlyID", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + ownerClient, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + + // Owner is required to make orgs + client, _ := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID, rbac.RoleOwner()) + + ctx := testutil.Context(t, testutil.WaitMedium) + orgs := []string{"foo", "bar"} + for _, orgName := range orgs { + _, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: orgName, + }) + require.NoError(t, err) + } + + inv, root := clitest.New(t, "organizations", "show", "--only-id", "--org="+first.OrganizationID.String()) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + errC := make(chan error) + go func() { + errC <- inv.Run() + }() + require.NoError(t, <-errC) + pty.ExpectMatch(first.OrganizationID.String()) + }) + + t.Run("UsingFlag", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + ownerClient, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + + // Owner is required to make orgs + client, _ := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID, rbac.RoleOwner()) + + ctx := testutil.Context(t, testutil.WaitMedium) + orgs := map[string]codersdk.Organization{ + "foo": {}, + "bar": {}, + } + for orgName := range orgs { + org, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: orgName, + }) + require.NoError(t, err) + orgs[orgName] = org + } + + inv, root := clitest.New(t, "organizations", "show", "selected", "--only-id", "-O=bar") + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + errC := make(chan error) + go func() { + errC <- inv.Run() + }() + require.NoError(t, <-errC) + pty.ExpectMatch(orgs["bar"].ID.String()) + }) +} diff --git a/enterprise/cli/provisionerdaemonstart_test.go b/enterprise/cli/provisionerdaemonstart_test.go index f1eb0853cc..611abddd9d 100644 --- a/enterprise/cli/provisionerdaemonstart_test.go +++ b/enterprise/cli/provisionerdaemonstart_test.go @@ -30,11 +30,17 @@ func TestProvisionerDaemon_PSK(t *testing.T) { t.Run("OK", func(t *testing.T) { t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, ProvisionerDaemonPSK: "provisionersftw", LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) @@ -64,15 +70,21 @@ func TestProvisionerDaemon_PSK(t *testing.T) { t.Run("AnotherOrg", func(t *testing.T) { t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, ProvisionerDaemonPSK: "provisionersftw", LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) - anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) + anotherOrg := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw", "--name", "org-daemon", "--org", anotherOrg.ID.String()) err := conf.URL().Write(client.URL.String()) require.NoError(t, err) @@ -98,15 +110,21 @@ func TestProvisionerDaemon_PSK(t *testing.T) { t.Run("AnotherOrgByNameWithUser", func(t *testing.T) { t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, ProvisionerDaemonPSK: "provisionersftw", LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) - anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) + anotherOrg := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) anotherClient, _ := coderdtest.CreateAnotherUser(t, client, anotherOrg.ID, rbac.RoleTemplateAdmin()) inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw", "--name", "org-daemon", "--org", anotherOrg.Name) clitest.SetupConfig(t, anotherClient, conf) @@ -119,15 +137,21 @@ func TestProvisionerDaemon_PSK(t *testing.T) { t.Run("AnotherOrgByNameNoUser", func(t *testing.T) { t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, ProvisionerDaemonPSK: "provisionersftw", LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) - anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) + anotherOrg := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw", "--name", "org-daemon", "--org", anotherOrg.Name) err := conf.URL().Write(client.URL.String()) require.NoError(t, err) @@ -266,15 +290,21 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { t.Run("ScopeUserAnotherOrg", func(t *testing.T) { t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, ProvisionerDaemonPSK: "provisionersftw", LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) - anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) + anotherOrg := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, anotherOrg.ID, rbac.RoleTemplateAdmin()) inv, conf := newCLI(t, "provisionerd", "start", "--tag", "scope=user", "--name", "org-daemon", "--org", anotherOrg.ID.String()) clitest.SetupConfig(t, anotherClient, conf) @@ -431,7 +461,7 @@ func TestProvisionerDaemon_ProvisionerKey(t *testing.T) { DeploymentValues: dv, }, }) - anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) + anotherOrg := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) // nolint:gocritic // test res, err := client.CreateProvisionerKey(ctx, anotherOrg.ID, codersdk.CreateProvisionerKeyRequest{ Name: "dont-TEST-me", diff --git a/enterprise/cli/templatecreate_test.go b/enterprise/cli/templatecreate_test.go index 3f089a6262..f67648cfef 100644 --- a/enterprise/cli/templatecreate_test.go +++ b/enterprise/cli/templatecreate_test.go @@ -140,7 +140,10 @@ func TestTemplateCreate(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)} + dv.Experiments = []string{ + string(codersdk.ExperimentCustomRoles), + string(codersdk.ExperimentMultiOrganization), + } ownerClient, _ := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -152,12 +155,13 @@ func TestTemplateCreate(t *testing.T) { codersdk.FeatureAccessControl: 1, codersdk.FeatureCustomRoles: 1, codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) // Create the second organization - secondOrg := coderdtest.CreateOrganization(t, ownerClient, coderdtest.CreateOrganizationOptions{ + secondOrg := coderdenttest.CreateOrganization(t, ownerClient, coderdenttest.CreateOrganizationOptions{ IncludeProvisionerDaemon: true, }) diff --git a/enterprise/cli/templatelist_test.go b/enterprise/cli/templatelist_test.go new file mode 100644 index 0000000000..e0044455fe --- /dev/null +++ b/enterprise/cli/templatelist_test.go @@ -0,0 +1,70 @@ +package cli_test + +import ( + "bytes" + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/license" + "github.com/coder/coder/v2/testutil" +) + +func TestEnterpriseListTemplates(t *testing.T) { + t.Parallel() + + t.Run("MultiOrg", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + + // Template in the first organization + firstVersion := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, firstVersion.ID) + _ = coderdtest.CreateTemplate(t, client, owner.OrganizationID, firstVersion.ID) + + secondOrg := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{ + IncludeProvisionerDaemon: true, + }) + secondVersion := coderdtest.CreateTemplateVersion(t, client, secondOrg.ID, nil) + _ = coderdtest.CreateTemplate(t, client, secondOrg.ID, secondVersion.ID) + + // Create a site wide template admin + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + inv, root := clitest.New(t, "templates", "list", "--output=json") + clitest.SetupConfig(t, templateAdmin, root) + + ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancelFunc() + + out := bytes.NewBuffer(nil) + inv.Stdout = out + err := inv.WithContext(ctx).Run() + require.NoError(t, err) + + var templates []codersdk.Template + require.NoError(t, json.Unmarshal(out.Bytes(), &templates)) + require.Len(t, templates, 2) + }) +} diff --git a/enterprise/coderd/audit_test.go b/enterprise/coderd/audit_test.go new file mode 100644 index 0000000000..1758b1b06f --- /dev/null +++ b/enterprise/coderd/audit_test.go @@ -0,0 +1,115 @@ +package coderd_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/license" +) + +func TestEnterpriseAuditLogs(t *testing.T) { + t.Parallel() + + t.Run("IncludeOrganization", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + //nolint:gocritic // only owners can create organizations + o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "new-org", + DisplayName: "New organization", + Description: "A new organization to love and cherish until the test is over.", + Icon: "/emojis/1f48f-1f3ff.png", + }) + require.NoError(t, err) + + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + OrganizationID: o.ID, + ResourceID: user.UserID, + }) + require.NoError(t, err) + + alogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{ + Pagination: codersdk.Pagination{ + Limit: 1, + }, + }) + require.NoError(t, err) + require.Equal(t, int64(1), alogs.Count) + require.Len(t, alogs.AuditLogs, 1) + + // Make sure the organization is fully populated. + require.Equal(t, &codersdk.MinimalOrganization{ + ID: o.ID, + Name: o.Name, + DisplayName: o.DisplayName, + Icon: o.Icon, + }, alogs.AuditLogs[0].Organization) + + // OrganizationID is deprecated, but make sure it is set. + require.Equal(t, o.ID, alogs.AuditLogs[0].OrganizationID) + + // Delete the org and try again, should be mostly empty. + err = client.DeleteOrganization(ctx, o.ID.String()) + require.NoError(t, err) + + alogs, err = client.AuditLogs(ctx, codersdk.AuditLogsRequest{ + Pagination: codersdk.Pagination{ + Limit: 1, + }, + }) + require.NoError(t, err) + require.Equal(t, int64(1), alogs.Count) + require.Len(t, alogs.AuditLogs, 1) + + require.Equal(t, &codersdk.MinimalOrganization{ + ID: o.ID, + }, alogs.AuditLogs[0].Organization) + + // OrganizationID is deprecated, but make sure it is set. + require.Equal(t, o.ID, alogs.AuditLogs[0].OrganizationID) + + // Some audit entries do not have an organization at all, in which case the + // response omits the organization. + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + ResourceType: codersdk.ResourceTypeAPIKey, + ResourceID: user.UserID, + }) + require.NoError(t, err) + + alogs, err = client.AuditLogs(ctx, codersdk.AuditLogsRequest{ + SearchQuery: "resource_type:api_key", + Pagination: codersdk.Pagination{ + Limit: 1, + }, + }) + require.NoError(t, err) + require.Equal(t, int64(1), alogs.Count) + require.Len(t, alogs.AuditLogs, 1) + + // The other will have no organization. + require.Equal(t, (*codersdk.MinimalOrganization)(nil), alogs.AuditLogs[0].Organization) + + // OrganizationID is deprecated, but make sure it is empty. + require.Equal(t, uuid.Nil, alogs.AuditLogs[0].OrganizationID) + }) +} diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 8cb15a32c0..e9e8d7d196 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -240,6 +240,27 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Delete("/", api.deleteWorkspaceProxy) }) }) + + r.Group(func(r chi.Router) { + r.Use( + apiKeyMiddleware, + api.RequireFeatureMW(codersdk.FeatureMultipleOrganizations), + httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentMultiOrganization), + ) + r.Post("/organizations", api.postOrganizations) + }) + + r.Group(func(r chi.Router) { + r.Use( + apiKeyMiddleware, + api.RequireFeatureMW(codersdk.FeatureMultipleOrganizations), + httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentMultiOrganization), + httpmw.ExtractOrganizationParam(api.Database), + ) + r.Patch("/organizations/{organization}", api.patchOrganization) + r.Delete("/organizations/{organization}", api.deleteOrganization) + }) + r.Route("/organizations/{organization}/groups", func(r chi.Router) { r.Use( apiKeyMiddleware, diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index d55b7f8d44..f5bfd05529 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -7,15 +7,29 @@ import ( "crypto/tls" "io" "net/http" + "strings" "testing" "time" + "github.com/moby/moby/pkg/namesgenerator" + "golang.org/x/xerrors" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbmem" "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/provisioner/echo" + "github.com/coder/coder/v2/provisionerd" + provisionerdproto "github.com/coder/coder/v2/provisionerd/proto" + "github.com/coder/coder/v2/provisionersdk" + sdkproto "github.com/coder/coder/v2/provisionersdk/proto" + "github.com/coder/coder/v2/testutil" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" @@ -248,3 +262,95 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { type nopcloser struct{} func (nopcloser) Close() error { return nil } + +type CreateOrganizationOptions struct { + // IncludeProvisionerDaemon will spin up an external provisioner for the organization. + // This requires enterprise and the feature 'codersdk.FeatureExternalProvisionerDaemons' + IncludeProvisionerDaemon bool +} + +func CreateOrganization(t *testing.T, client *codersdk.Client, opts CreateOrganizationOptions, mutators ...func(*codersdk.CreateOrganizationRequest)) codersdk.Organization { + ctx := testutil.Context(t, testutil.WaitMedium) + req := codersdk.CreateOrganizationRequest{ + Name: strings.ReplaceAll(strings.ToLower(namesgenerator.GetRandomName(0)), "_", "-"), + DisplayName: namesgenerator.GetRandomName(1), + Description: namesgenerator.GetRandomName(1), + Icon: "", + } + for _, mutator := range mutators { + mutator(&req) + } + + org, err := client.CreateOrganization(ctx, req) + require.NoError(t, err) + + if opts.IncludeProvisionerDaemon { + closer := NewExternalProvisionerDaemon(t, client, org.ID, map[string]string{}) + t.Cleanup(func() { + _ = closer.Close() + }) + } + + return org +} + +func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string) io.Closer { + t.Helper() + + // Without this check, the provisioner will silently fail. + entitlements, err := client.Entitlements(context.Background()) + if err != nil { + // AGPL instances will throw this error. They cannot use external + // provisioners. + t.Errorf("external provisioners requires a license with entitlements. The client failed to fetch the entitlements, is this an enterprise instance of coderd?") + t.FailNow() + return nil + } + + feature := entitlements.Features[codersdk.FeatureExternalProvisionerDaemons] + if !feature.Enabled || feature.Entitlement != codersdk.EntitlementEntitled { + require.NoError(t, xerrors.Errorf("external provisioner daemons require an entitled license")) + return nil + } + + echoClient, echoServer := drpc.MemTransportPipe() + ctx, cancelFunc := context.WithCancel(context.Background()) + serveDone := make(chan struct{}) + t.Cleanup(func() { + _ = echoClient.Close() + _ = echoServer.Close() + cancelFunc() + <-serveDone + }) + go func() { + defer close(serveDone) + err := echo.Serve(ctx, &provisionersdk.ServeOptions{ + Listener: echoServer, + WorkDirectory: t.TempDir(), + }) + assert.NoError(t, err) + }() + + daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { + return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + ID: uuid.New(), + Name: t.Name(), + Organization: org, + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho}, + Tags: tags, + }) + }, &provisionerd.Options{ + Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug), + UpdateInterval: 250 * time.Millisecond, + ForceCancelInterval: 5 * time.Second, + Connector: provisionerd.LocalProvisioners{ + string(database.ProvisionerTypeEcho): sdkproto.NewDRPCProvisionerClient(echoClient), + }, + }) + closer := coderdtest.NewProvisionerDaemonCloser(daemon) + t.Cleanup(func() { + _ = closer.Close() + }) + + return closer +} diff --git a/enterprise/coderd/organizations.go b/enterprise/coderd/organizations.go new file mode 100644 index 0000000000..a7ec4050ee --- /dev/null +++ b/enterprise/coderd/organizations.go @@ -0,0 +1,272 @@ +package coderd + +import ( + "database/sql" + "fmt" + "net/http" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/audit" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/codersdk" +) + +// @Summary Update organization +// @ID update-organization +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Organizations +// @Param organization path string true "Organization ID or name" +// @Param request body codersdk.UpdateOrganizationRequest true "Patch organization request" +// @Success 200 {object} codersdk.Organization +// @Router /organizations/{organization} [patch] +func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + organization = httpmw.OrganizationParam(r) + auditor = api.AGPL.Auditor.Load() + aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: organization.ID, + }) + ) + aReq.Old = organization + defer commitAudit() + + var req codersdk.UpdateOrganizationRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + // "default" is a reserved name that always refers to the default org (much like the way we + // use "me" for users). + if req.Name == codersdk.DefaultOrganization { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), + }) + return + } + + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + var err error + organization, err = tx.GetOrganizationByID(ctx, organization.ID) + if err != nil { + return err + } + + updateOrgParams := database.UpdateOrganizationParams{ + UpdatedAt: dbtime.Now(), + ID: organization.ID, + Name: organization.Name, + DisplayName: organization.DisplayName, + Description: organization.Description, + Icon: organization.Icon, + } + + if req.Name != "" { + updateOrgParams.Name = req.Name + } + if req.DisplayName != "" { + updateOrgParams.DisplayName = req.DisplayName + } + if req.Description != nil { + updateOrgParams.Description = *req.Description + } + if req.Icon != nil { + updateOrgParams.Icon = *req.Icon + } + + organization, err = tx.UpdateOrganization(ctx, updateOrgParams) + if err != nil { + return err + } + return nil + }) + + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if database.IsUniqueViolation(err) { + httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ + Message: fmt.Sprintf("Organization already exists with the name %q.", req.Name), + Validations: []codersdk.ValidationError{{ + Field: "name", + Detail: "This value is already in use and should be unique.", + }}, + }) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error updating organization.", + Detail: fmt.Sprintf("update organization: %s", err.Error()), + }) + return + } + + aReq.New = organization + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Organization(organization)) +} + +// @Summary Delete organization +// @ID delete-organization +// @Security CoderSessionToken +// @Produce json +// @Tags Organizations +// @Param organization path string true "Organization ID or name" +// @Success 200 {object} codersdk.Response +// @Router /organizations/{organization} [delete] +func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + organization = httpmw.OrganizationParam(r) + auditor = api.AGPL.Auditor.Load() + aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionDelete, + OrganizationID: organization.ID, + }) + ) + aReq.Old = organization + defer commitAudit() + + if organization.IsDefault { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Default organization cannot be deleted.", + }) + return + } + + err := api.Database.DeleteOrganization(ctx, organization.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error deleting organization.", + Detail: fmt.Sprintf("delete organization: %s", err.Error()), + }) + return + } + + aReq.New = database.Organization{} + httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{ + Message: "Organization has been deleted.", + }) +} + +// @Summary Create organization +// @ID create-organization +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Organizations +// @Param request body codersdk.CreateOrganizationRequest true "Create organization request" +// @Success 201 {object} codersdk.Organization +// @Router /organizations [post] +func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) { + var ( + // organizationID is required before the audit log entry is created. + organizationID = uuid.New() + ctx = r.Context() + apiKey = httpmw.APIKey(r) + auditor = api.AGPL.Auditor.Load() + aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionCreate, + OrganizationID: organizationID, + }) + ) + aReq.Old = database.Organization{} + defer commitAudit() + + var req codersdk.CreateOrganizationRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + if req.Name == codersdk.DefaultOrganization { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), + }) + return + } + + _, err := api.Database.GetOrganizationByName(ctx, req.Name) + if err == nil { + httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ + Message: "Organization already exists with that name.", + }) + return + } + if !xerrors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: fmt.Sprintf("Internal error fetching organization %q.", req.Name), + Detail: err.Error(), + }) + return + } + + var organization database.Organization + err = api.Database.InTx(func(tx database.Store) error { + if req.DisplayName == "" { + req.DisplayName = req.Name + } + + organization, err = tx.InsertOrganization(ctx, database.InsertOrganizationParams{ + ID: organizationID, + Name: req.Name, + DisplayName: req.DisplayName, + Description: req.Description, + Icon: req.Icon, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + }) + if err != nil { + return xerrors.Errorf("create organization: %w", err) + } + _, err = tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{ + OrganizationID: organization.ID, + UserID: apiKey.UserID, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + Roles: []string{ + // TODO: When organizations are allowed to be created, we should + // come back to determining the default role of the person who + // creates the org. Until that happens, all users in an organization + // should be just regular members. + }, + }) + if err != nil { + return xerrors.Errorf("create organization admin: %w", err) + } + + _, err = tx.InsertAllUsersGroup(ctx, organization.ID) + if err != nil { + return xerrors.Errorf("create %q group: %w", database.EveryoneGroup, err) + } + return nil + }, nil) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error inserting organization member.", + Detail: err.Error(), + }) + return + } + + aReq.New = organization + httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.Organization(organization)) +} diff --git a/enterprise/coderd/organizations_test.go b/enterprise/coderd/organizations_test.go new file mode 100644 index 0000000000..8c4e9daa0d --- /dev/null +++ b/enterprise/coderd/organizations_test.go @@ -0,0 +1,605 @@ +package coderd_test + +import ( + "bytes" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/util/ptr" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/license" + "github.com/coder/coder/v2/testutil" +) + +func TestMultiOrgFetch(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + ctx := testutil.Context(t, testutil.WaitLong) + + makeOrgs := []string{"foo", "bar", "baz"} + for _, name := range makeOrgs { + _, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: name, + DisplayName: name, + }) + require.NoError(t, err) + } + + //nolint:gocritic // using the owner intentionally since only they can make orgs + myOrgs, err := client.OrganizationsByUser(ctx, codersdk.Me) + require.NoError(t, err) + require.NotNil(t, myOrgs) + require.Len(t, myOrgs, len(makeOrgs)+1) + + orgs, err := client.Organizations(ctx) + require.NoError(t, err) + require.NotNil(t, orgs) + require.ElementsMatch(t, myOrgs, orgs) +} + +func TestOrganizationsByUser(t *testing.T) { + t.Parallel() + + t.Run("IsDefault", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + ctx := testutil.Context(t, testutil.WaitLong) + + //nolint:gocritic // owner is required to make orgs + orgs, err := client.OrganizationsByUser(ctx, codersdk.Me) + require.NoError(t, err) + require.NotNil(t, orgs) + require.Len(t, orgs, 1) + require.True(t, orgs[0].IsDefault, "first org is always default") + + // Make an extra org, and it should not be defaulted. + notDefault, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "another", + DisplayName: "Another", + }) + require.NoError(t, err) + require.False(t, notDefault.IsDefault, "only 1 default org allowed") + }) + + t.Run("NoMember", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + other, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + ctx := testutil.Context(t, testutil.WaitLong) + + //nolint:gocritic // owner is required to make orgs + org, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "another", + DisplayName: "Another", + }) + require.NoError(t, err) + + _, err = other.OrganizationByUserAndName(ctx, codersdk.Me, org.Name) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) +} + +func TestAddOrganizationMembers(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + _, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitMedium) + //nolint:gocritic // must be an owner, only owners can create orgs + otherOrg, err := ownerClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "Other", + DisplayName: "", + Description: "", + Icon: "", + }) + require.NoError(t, err, "create another organization") + + inv, root := clitest.New(t, "organization", "members", "add", "-O", otherOrg.ID.String(), user.Username) + //nolint:gocritic // must be an owner + clitest.SetupConfig(t, ownerClient, root) + + buf := new(bytes.Buffer) + inv.Stdout = buf + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + + //nolint:gocritic // must be an owner + members, err := ownerClient.OrganizationMembers(ctx, otherOrg.ID) + require.NoError(t, err) + + require.Len(t, members, 2) + }) +} + +func TestDeleteOrganizationsByUser(t *testing.T) { + t.Parallel() + t.Run("Default", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + // nolint:gocritic // owner used below to delete + o, err := client.Organization(ctx, user.OrganizationID) + require.NoError(t, err) + + // nolint:gocritic // only owners can delete orgs + err = client.DeleteOrganization(ctx, o.ID.String()) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + + t.Run("DeleteById", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) + + // nolint:gocritic // only owners can delete orgs + err := client.DeleteOrganization(ctx, o.ID.String()) + require.NoError(t, err) + }) + + t.Run("DeleteByName", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) + + // nolint:gocritic // only owners can delete orgs + err := client.DeleteOrganization(ctx, o.Name) + require.NoError(t, err) + }) +} + +func TestPatchOrganizationsByUser(t *testing.T) { + t.Parallel() + t.Run("Conflict", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + // nolint:gocritic // owner used below as only they can create orgs + originalOrg, err := client.Organization(ctx, user.OrganizationID) + require.NoError(t, err) + + o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) + + // nolint:gocritic // owner used above to make the org + _, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ + Name: originalOrg.Name, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + + t.Run("ReservedName", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + var err error + o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) + + _, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ + Name: codersdk.DefaultOrganization, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + + t.Run("InvalidName", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + var err error + o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) + + _, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ + Name: "something unique but not url safe", + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + + t.Run("UpdateById", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + var err error + o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) + + o, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ + Name: "new-new-org", + }) + require.NoError(t, err) + require.Equal(t, "new-new-org", o.Name) + }) + + t.Run("UpdateByName", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + const displayName = "New Organization" + var err error + o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}, func(request *codersdk.CreateOrganizationRequest) { + request.DisplayName = displayName + }) + + o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{ + Name: "new-new-org", + }) + require.NoError(t, err) + require.Equal(t, "new-new-org", o.Name) + require.Equal(t, displayName, o.DisplayName) // didn't change + }) + + t.Run("UpdateDisplayName", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + var err error + const name = "new-org" + o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}, func(request *codersdk.CreateOrganizationRequest) { + request.Name = name + }) + + const displayName = "The Newest One" + o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{ + DisplayName: "The Newest One", + }) + require.NoError(t, err) + require.Equal(t, "new-org", o.Name) // didn't change + require.Equal(t, displayName, o.DisplayName) + }) + + t.Run("UpdateDescription", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + const displayName = "New Organization" + var err error + o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}, func(request *codersdk.CreateOrganizationRequest) { + request.DisplayName = displayName + request.Name = "new-org" + }) + + const description = "wow, this organization description is so updated!" + o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{ + Description: ptr.Ref(description), + }) + + require.NoError(t, err) + require.Equal(t, "new-org", o.Name) // didn't change + require.Equal(t, displayName, o.DisplayName) // didn't change + require.Equal(t, description, o.Description) + }) + + t.Run("UpdateIcon", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitMedium) + + const displayName = "New Organization" + var err error + o := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}, func(request *codersdk.CreateOrganizationRequest) { + request.DisplayName = displayName + request.Icon = "/emojis/random.png" + request.Name = "new-org" + }) + + const icon = "/emojis/1f48f-1f3ff.png" + o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{ + Icon: ptr.Ref(icon), + }) + + require.NoError(t, err) + require.Equal(t, "new-org", o.Name) // didn't change + require.Equal(t, displayName, o.DisplayName) // didn't change + require.Equal(t, icon, o.Icon) + }) +} + +func TestPostOrganizationsByUser(t *testing.T) { + t.Parallel() + t.Run("Conflict", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitLong) + + //nolint:gocritic // using owner for below + org, err := client.Organization(ctx, user.OrganizationID) + require.NoError(t, err) + + //nolint:gocritic // only owners can create orgs + _, err = client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: org.Name, + DisplayName: org.DisplayName, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + + t.Run("InvalidName", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitLong) + + //nolint:gocritic // only owners can create orgs + _, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "A name which is definitely not url safe", + DisplayName: "New", + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + + t.Run("Create", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitLong) + + //nolint:gocritic // only owners can create orgs + o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "new-org", + DisplayName: "New organization", + Description: "A new organization to love and cherish forever.", + Icon: "/emojis/1f48f-1f3ff.png", + }) + require.NoError(t, err) + require.Equal(t, "new-org", o.Name) + require.Equal(t, "New organization", o.DisplayName) + require.Equal(t, "A new organization to love and cherish forever.", o.Description) + require.Equal(t, "/emojis/1f48f-1f3ff.png", o.Icon) + }) + + t.Run("CreateWithoutExplicitDisplayName", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitLong) + + //nolint:gocritic // only owners can create orgs + o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "new-org", + }) + require.NoError(t, err) + require.Equal(t, "new-org", o.Name) + require.Equal(t, "new-org", o.DisplayName) // should match the given `Name` + }) +} diff --git a/enterprise/coderd/provisionerdaemons_test.go b/enterprise/coderd/provisionerdaemons_test.go index 451ff2249a..b7eef06072 100644 --- a/enterprise/coderd/provisionerdaemons_test.go +++ b/enterprise/coderd/provisionerdaemons_test.go @@ -251,7 +251,7 @@ func TestProvisionerDaemonServe(t *testing.T) { codersdk.FeatureExternalProvisionerDaemons: 1, }, }}) - closer := coderdtest.NewExternalProvisionerDaemon(t, client, user.OrganizationID, map[string]string{ + closer := coderdenttest.NewExternalProvisionerDaemon(t, client, user.OrganizationID, map[string]string{ provisionersdk.TagScope: provisionersdk.ScopeUser, }) defer closer.Close() @@ -303,7 +303,7 @@ func TestProvisionerDaemonServe(t *testing.T) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) another, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) _ = closer.Close() - closer = coderdtest.NewExternalProvisionerDaemon(t, another, user.OrganizationID, map[string]string{ + closer = coderdenttest.NewExternalProvisionerDaemon(t, another, user.OrganizationID, map[string]string{ provisionersdk.TagScope: provisionersdk.ScopeUser, }) defer closer.Close() @@ -727,12 +727,20 @@ func TestGetProvisionerDaemons(t *testing.T) { t.Run("OK", func(t *testing.T) { t.Parallel() - client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureExternalProvisionerDaemons: 1, + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, }, - }}) - org := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + org := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, org.ID, rbac.ScopedRoleOrgAdmin(org.ID)) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() diff --git a/enterprise/coderd/provisionerkeys_test.go b/enterprise/coderd/provisionerkeys_test.go index 6becbe657c..e4bab9b98c 100644 --- a/enterprise/coderd/provisionerkeys_test.go +++ b/enterprise/coderd/provisionerkeys_test.go @@ -33,7 +33,7 @@ func TestProvisionerKeys(t *testing.T) { }) orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - otherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) + otherOrg := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) outsideOrgAdmin, _ := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.ScopedRoleOrgAdmin(otherOrg.ID)) // member cannot create a provisioner key diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index 8006f7a68b..b6fa32e128 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -2,6 +2,8 @@ package coderd_test import ( "bytes" + "context" + "net/http" "slices" "testing" @@ -9,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" @@ -329,3 +332,177 @@ func TestCustomOrganizationRole(t *testing.T) { require.ErrorContains(t, err, "Invalid request") }) } + +func TestListRoles(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + // Create owner, member, and org admin + member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) + + otherOrg := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) + + const notFound = "Resource not found" + testCases := []struct { + Name string + Client *codersdk.Client + APICall func(context.Context) ([]codersdk.AssignableRoles, error) + ExpectedRoles []codersdk.AssignableRoles + AuthorizedError string + }{ + { + // Members cannot assign any roles + Name: "MemberListSite", + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { + x, err := member.ListSiteRoles(ctx) + return x, err + }, + ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ + {Name: codersdk.RoleOwner}: false, + {Name: codersdk.RoleAuditor}: false, + {Name: codersdk.RoleTemplateAdmin}: false, + {Name: codersdk.RoleUserAdmin}: false, + }), + }, + { + Name: "OrgMemberListOrg", + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { + return member.ListOrganizationRoles(ctx, owner.OrganizationID) + }, + ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ + {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: false, + }), + }, + { + Name: "NonOrgMemberListOrg", + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { + return member.ListOrganizationRoles(ctx, otherOrg.ID) + }, + AuthorizedError: notFound, + }, + // Org admin + { + Name: "OrgAdminListSite", + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { + return orgAdmin.ListSiteRoles(ctx) + }, + ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ + {Name: codersdk.RoleOwner}: false, + {Name: codersdk.RoleAuditor}: false, + {Name: codersdk.RoleTemplateAdmin}: false, + {Name: codersdk.RoleUserAdmin}: false, + }), + }, + { + Name: "OrgAdminListOrg", + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { + return orgAdmin.ListOrganizationRoles(ctx, owner.OrganizationID) + }, + ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ + {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, + }), + }, + { + Name: "OrgAdminListOtherOrg", + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { + return orgAdmin.ListOrganizationRoles(ctx, otherOrg.ID) + }, + AuthorizedError: notFound, + }, + // Admin + { + Name: "AdminListSite", + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { + return client.ListSiteRoles(ctx) + }, + ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ + {Name: codersdk.RoleOwner}: true, + {Name: codersdk.RoleAuditor}: true, + {Name: codersdk.RoleTemplateAdmin}: true, + {Name: codersdk.RoleUserAdmin}: true, + }), + }, + { + Name: "AdminListOrg", + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { + return client.ListOrganizationRoles(ctx, owner.OrganizationID) + }, + ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ + {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, + }), + }, + } + + for _, c := range testCases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + roles, err := c.APICall(ctx) + if c.AuthorizedError != "" { + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + require.Contains(t, apiErr.Message, c.AuthorizedError) + } else { + require.NoError(t, err) + ignorePerms := func(f codersdk.AssignableRoles) codersdk.AssignableRoles { + return codersdk.AssignableRoles{ + Role: codersdk.Role{ + Name: f.Name, + DisplayName: f.DisplayName, + }, + Assignable: f.Assignable, + BuiltIn: true, + } + } + expected := db2sdk.List(c.ExpectedRoles, ignorePerms) + found := db2sdk.List(roles, ignorePerms) + require.ElementsMatch(t, expected, found) + } + }) + } +} + +func convertRole(roleName rbac.RoleIdentifier) codersdk.Role { + role, _ := rbac.RoleByName(roleName) + return db2sdk.RBACRole(role) +} + +func convertRoles(assignableRoles map[rbac.RoleIdentifier]bool) []codersdk.AssignableRoles { + converted := make([]codersdk.AssignableRoles, 0, len(assignableRoles)) + for roleName, assignable := range assignableRoles { + role := convertRole(roleName) + converted = append(converted, codersdk.AssignableRoles{ + Role: role, + Assignable: assignable, + }) + } + return converted +} diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index d817698ef7..fedef4edde 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -729,7 +729,7 @@ func TestTemplates(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)} + dv.Experiments = []string{string(codersdk.ExperimentCustomRoles), string(codersdk.ExperimentMultiOrganization)} ownerClient, _ := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -740,12 +740,13 @@ func TestTemplates(t *testing.T) { codersdk.FeatureAccessControl: 1, codersdk.FeatureCustomRoles: 1, codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) ctx := testutil.Context(t, testutil.WaitMedium) - secondOrg := coderdtest.CreateOrganization(t, ownerClient, coderdtest.CreateOrganizationOptions{ + secondOrg := coderdenttest.CreateOrganization(t, ownerClient, coderdenttest.CreateOrganizationOptions{ IncludeProvisionerDaemon: true, }) @@ -774,6 +775,60 @@ func TestTemplates(t *testing.T) { template := coderdtest.CreateTemplate(t, orgTemplateAdmin, secondOrg.ID, version.ID) require.Equal(t, template.OrganizationID, secondOrg.ID) }) + + t.Run("MultipleOrganizations", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin()) + org2 := coderdenttest.CreateOrganization(t, ownerClient, coderdenttest.CreateOrganizationOptions{}) + user, _ := coderdtest.CreateAnotherUser(t, ownerClient, org2.ID) + + // 2 templates in first organization + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version2.ID) + + // 2 in the second organization + version3 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil) + version4 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil) + coderdtest.CreateTemplate(t, client, org2.ID, version3.ID) + coderdtest.CreateTemplate(t, client, org2.ID, version4.ID) + + ctx := testutil.Context(t, testutil.WaitLong) + + // All 4 are viewable by the owner + templates, err := client.Templates(ctx, codersdk.TemplateFilter{}) + require.NoError(t, err) + require.Len(t, templates, 4) + + // View a single organization from the owner + templates, err = client.Templates(ctx, codersdk.TemplateFilter{ + OrganizationID: owner.OrganizationID, + }) + require.NoError(t, err) + require.Len(t, templates, 2) + + // Only 2 are viewable by the org user + templates, err = user.Templates(ctx, codersdk.TemplateFilter{}) + require.NoError(t, err) + require.Len(t, templates, 2) + for _, tmpl := range templates { + require.Equal(t, tmpl.OrganizationName, org2.Name, "organization name on template") + } + }) } func TestTemplateACL(t *testing.T) { @@ -1636,11 +1691,19 @@ func TestTemplateAccess(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong*3) t.Cleanup(cancel) - ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureTemplateRBAC: 1, + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, }, - }}) + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) type coderUser struct { *codersdk.Client @@ -1839,25 +1902,29 @@ func TestTemplateAccess(t *testing.T) { func TestMultipleOrganizationTemplates(t *testing.T) { t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} ownerClient, first := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ // This only affects the first org. IncludeProvisionerDaemon: true, + DeploymentValues: dv, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureExternalProvisionerDaemons: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID, rbac.RoleTemplateAdmin()) - second := coderdtest.CreateOrganization(t, ownerClient, coderdtest.CreateOrganizationOptions{ + second := coderdenttest.CreateOrganization(t, ownerClient, coderdenttest.CreateOrganizationOptions{ IncludeProvisionerDaemon: true, }) - third := coderdtest.CreateOrganization(t, ownerClient, coderdtest.CreateOrganizationOptions{ + third := coderdenttest.CreateOrganization(t, ownerClient, coderdenttest.CreateOrganizationOptions{ IncludeProvisionerDaemon: true, }) diff --git a/enterprise/coderd/users_test.go b/enterprise/coderd/users_test.go index 4f55859cd9..c7efe3c084 100644 --- a/enterprise/coderd/users_test.go +++ b/enterprise/coderd/users_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" @@ -296,3 +298,289 @@ func TestAssignCustomOrgRoles(t *testing.T) { _, err = memberClient.CreateTemplate(ctx, owner.OrganizationID, createTemplateReq) require.NoError(t, err) } + +func TestGrantSiteRoles(t *testing.T) { + t.Parallel() + + requireStatusCode := func(t *testing.T, err error, statusCode int) { + t.Helper() + var e *codersdk.Error + require.ErrorAs(t, err, &e, "error is codersdk error") + require.Equal(t, statusCode, e.StatusCode(), "correct status code") + } + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + admin, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID) + orgAdmin, _ := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID)) + randOrg := coderdenttest.CreateOrganization(t, admin, coderdenttest.CreateOrganizationOptions{}) + + _, randOrgUser := coderdtest.CreateAnotherUser(t, admin, randOrg.ID, rbac.ScopedRoleOrgAdmin(randOrg.ID)) + userAdmin, _ := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID, rbac.RoleUserAdmin()) + + const newUser = "newUser" + + testCases := []struct { + Name string + Client *codersdk.Client + OrgID uuid.UUID + AssignToUser string + Roles []string + ExpectedRoles []string + Error bool + StatusCode int + }{ + { + Name: "OrgRoleInSite", + Client: admin, + AssignToUser: codersdk.Me, + Roles: []string{rbac.RoleOrgAdmin()}, + Error: true, + StatusCode: http.StatusBadRequest, + }, + { + Name: "UserNotExists", + Client: admin, + AssignToUser: uuid.NewString(), + Roles: []string{codersdk.RoleOwner}, + Error: true, + StatusCode: http.StatusBadRequest, + }, + { + Name: "MemberCannotUpdateRoles", + Client: member, + AssignToUser: first.UserID.String(), + Roles: []string{}, + Error: true, + StatusCode: http.StatusBadRequest, + }, + { + // Cannot update your own roles + Name: "AdminOnSelf", + Client: admin, + AssignToUser: first.UserID.String(), + Roles: []string{}, + Error: true, + StatusCode: http.StatusBadRequest, + }, + { + Name: "SiteRoleInOrg", + Client: admin, + OrgID: first.OrganizationID, + AssignToUser: codersdk.Me, + Roles: []string{codersdk.RoleOwner}, + Error: true, + StatusCode: http.StatusBadRequest, + }, + { + Name: "RoleInNotMemberOrg", + Client: orgAdmin, + OrgID: randOrg.ID, + AssignToUser: randOrgUser.ID.String(), + Roles: []string{rbac.RoleOrgMember()}, + Error: true, + StatusCode: http.StatusNotFound, + }, + { + Name: "AdminUpdateOrgSelf", + Client: admin, + OrgID: first.OrganizationID, + AssignToUser: first.UserID.String(), + Roles: []string{}, + Error: true, + StatusCode: http.StatusBadRequest, + }, + { + Name: "OrgAdminPromote", + Client: orgAdmin, + OrgID: first.OrganizationID, + AssignToUser: newUser, + Roles: []string{rbac.RoleOrgAdmin()}, + ExpectedRoles: []string{ + rbac.RoleOrgAdmin(), + }, + Error: false, + }, + { + Name: "UserAdminMakeMember", + Client: userAdmin, + AssignToUser: newUser, + Roles: []string{codersdk.RoleMember}, + ExpectedRoles: []string{ + codersdk.RoleMember, + }, + Error: false, + }, + } + + for _, c := range testCases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + var err error + if c.AssignToUser == newUser { + orgID := first.OrganizationID + if c.OrgID != uuid.Nil { + orgID = c.OrgID + } + _, newUser := coderdtest.CreateAnotherUser(t, admin, orgID) + c.AssignToUser = newUser.ID.String() + } + + var newRoles []codersdk.SlimRole + if c.OrgID != uuid.Nil { + // Org assign + var mem codersdk.OrganizationMember + mem, err = c.Client.UpdateOrganizationMemberRoles(ctx, c.OrgID, c.AssignToUser, codersdk.UpdateRoles{ + Roles: c.Roles, + }) + newRoles = mem.Roles + } else { + // Site assign + var user codersdk.User + user, err = c.Client.UpdateUserRoles(ctx, c.AssignToUser, codersdk.UpdateRoles{ + Roles: c.Roles, + }) + newRoles = user.Roles + } + + if c.Error { + require.Error(t, err) + requireStatusCode(t, err, c.StatusCode) + } else { + require.NoError(t, err) + roles := make([]string, 0, len(newRoles)) + for _, r := range newRoles { + roles = append(roles, r.Name) + } + require.ElementsMatch(t, roles, c.ExpectedRoles) + } + }) + } +} + +func TestEnterprisePostUser(t *testing.T) { + t.Parallel() + + t.Run("OrganizationNoAccess", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + + client, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + notInOrg, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + other, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleOwner(), rbac.RoleMember()) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + org := coderdenttest.CreateOrganization(t, other, coderdenttest.CreateOrganizationOptions{}, func(request *codersdk.CreateOrganizationRequest) { + request.Name = "another" + }) + + _, err := notInOrg.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "some@domain.com", + Username: "anotheruser", + Password: "SomeSecurePassword!", + OrganizationID: org.ID, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("OrganizationNoAccess", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + + client, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + notInOrg, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + other, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleOwner(), rbac.RoleMember()) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + org := coderdenttest.CreateOrganization(t, other, coderdenttest.CreateOrganizationOptions{}) + + _, err := notInOrg.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "some@domain.com", + Username: "anotheruser", + Password: "SomeSecurePassword!", + OrganizationID: org.ID, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("CreateWithoutOrg", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + + client, firstUser := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Add an extra org to try and confuse user creation + coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{}) + + // nolint:gocritic // intentional using the owner. + // Manually making a user with the request instead of the coderdtest util + user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", + }) + require.NoError(t, err) + + require.Len(t, user.OrganizationIDs, 1) + assert.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0]) + }) +} diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 11923e6889..bef6bd0ded 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -47,6 +47,45 @@ func agplUserQuietHoursScheduleStore() *atomic.Pointer[agplschedule.UserQuietHou func TestCreateWorkspace(t *testing.T) { t.Parallel() + t.Run("NoTemplateAccess", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + other, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleMember(), rbac.RoleOwner()) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + org, err := other.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "another", + }) + require.NoError(t, err) + version := coderdtest.CreateTemplateVersion(t, other, org.ID, nil) + template := coderdtest.CreateTemplate(t, other, org.ID, version.ID) + + _, err = client.CreateWorkspace(ctx, first.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{ + TemplateID: template.ID, + Name: "workspace", + }) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusForbidden, apiErr.StatusCode()) + }) + // Test that a user cannot indirectly access // a template they do not have access to. t.Run("Unauthorized", func(t *testing.T) { @@ -1297,6 +1336,60 @@ func TestResolveAutostart(t *testing.T) { require.True(t, resp.ParameterMismatch) } +func TestAdminViewAllWorkspaces(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + //nolint:gocritic // intentionally using owner + _, err := client.Workspace(ctx, workspace.ID) + require.NoError(t, err) + + otherOrg, err := client.CreateOrganization(ctx, 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. + otherOwner, _ := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.RoleOwner()) + otherWorkspaces, err := otherOwner.Workspaces(ctx, codersdk.WorkspaceFilter{}) + require.NoError(t, err, "(other) fetch workspaces") + + firstWorkspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{}) + require.NoError(t, err, "(first) fetch workspaces") + + require.ElementsMatch(t, otherWorkspaces.Workspaces, firstWorkspaces.Workspaces) + require.Equal(t, len(firstWorkspaces.Workspaces), 1, "should be 1 workspace present") + + memberView, _ := coderdtest.CreateAnotherUser(t, client, otherOrg.ID) + memberViewWorkspaces, err := memberView.Workspaces(ctx, codersdk.WorkspaceFilter{}) + require.NoError(t, err, "(member) fetch workspaces") + require.Equal(t, 0, len(memberViewWorkspaces.Workspaces), "member in other org should see 0 workspaces") +} + func must[T any](value T, err error) T { if err != nil { panic(err) diff --git a/enterprise/members_test.go b/enterprise/members_test.go new file mode 100644 index 0000000000..c328ce71d0 --- /dev/null +++ b/enterprise/members_test.go @@ -0,0 +1,121 @@ +package enterprise_test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/license" + "github.com/coder/coder/v2/testutil" +) + +func TestEnterpriseMembers(t *testing.T) { + t.Parallel() + + t.Run("PostUser", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + owner, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + ctx := testutil.Context(t, testutil.WaitMedium) + org := coderdenttest.CreateOrganization(t, owner, coderdenttest.CreateOrganizationOptions{}) + + // Make a user not in the second organization + _, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID) + + // Use scoped user admin in org to add the user + client, userAdmin := coderdtest.CreateAnotherUser(t, owner, org.ID, rbac.ScopedRoleOrgUserAdmin(org.ID)) + + members, err := client.OrganizationMembers(ctx, org.ID) + require.NoError(t, err) + require.Len(t, members, 2) // Verify the 2 members at the start + + // Add user to org + _, err = client.PostOrganizationMember(ctx, org.ID, user.Username) + require.NoError(t, err) + + members, err = client.OrganizationMembers(ctx, org.ID) + require.NoError(t, err) + // Owner + user admin + new member + require.Len(t, members, 3) + require.ElementsMatch(t, + []uuid.UUID{first.UserID, user.ID, userAdmin.ID}, + db2sdk.List(members, onlyIDs)) + }) + + t.Run("PostUserNotExists", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + owner, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + org := coderdenttest.CreateOrganization(t, owner, coderdenttest.CreateOrganizationOptions{}) + + ctx := testutil.Context(t, testutil.WaitMedium) + // Add user to org + //nolint:gocritic // Using owner to ensure it's not a 404 error + _, err := owner.PostOrganizationMember(ctx, org.ID, uuid.NewString()) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Contains(t, apiErr.Message, "must be an existing") + }) + + // Calling it from a user without the org access. + t.Run("ListNotInOrg", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + owner, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + client, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID)) + org := coderdenttest.CreateOrganization(t, owner, coderdenttest.CreateOrganizationOptions{}) + + ctx := testutil.Context(t, testutil.WaitShort) + + // 404 error is expected instead of a 403/401 to not leak existence of + // an organization. + _, err := client.OrganizationMembers(ctx, org.ID) + require.ErrorContains(t, err, "404") + }) +} + +func onlyIDs(u codersdk.OrganizationMemberWithUserData) uuid.UUID { + return u.UserID +} diff --git a/enterprise/workspaceapps_test.go b/enterprise/workspaceapps_test.go new file mode 100644 index 0000000000..e9d758b7d4 --- /dev/null +++ b/enterprise/workspaceapps_test.go @@ -0,0 +1,71 @@ +package enterprise_test + +import ( + "net" + "testing" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/workspaceapps/apptest" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/license" + "github.com/coder/serpent" +) + +func TestWorkspaceApps(t *testing.T) { + t.Parallel() + + apptest.Run(t, true, func(t *testing.T, opts *apptest.DeploymentOptions) *apptest.Deployment { + deploymentValues := coderdtest.DeploymentValues(t) + deploymentValues.DisablePathApps = serpent.Bool(opts.DisablePathApps) + deploymentValues.Dangerous.AllowPathAppSharing = serpent.Bool(opts.DangerousAllowPathAppSharing) + deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = serpent.Bool(opts.DangerousAllowPathAppSiteOwnerAccess) + deploymentValues.Experiments = []string{ + "*", + string(codersdk.ExperimentMultiOrganization), + } + + if opts.DisableSubdomainApps { + opts.AppHost = "" + } + + flushStatsCollectorCh := make(chan chan<- struct{}, 1) + opts.StatsCollectorOptions.Flush = flushStatsCollectorCh + flushStats := func() { + flushStatsCollectorDone := make(chan struct{}, 1) + flushStatsCollectorCh <- flushStatsCollectorDone + <-flushStatsCollectorDone + } + client, _, _, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: deploymentValues, + AppHostname: opts.AppHost, + IncludeProvisionerDaemon: true, + RealIPConfig: &httpmw.RealIPConfig{ + TrustedOrigins: []*net.IPNet{{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.CIDRMask(8, 32), + }}, + TrustedHeaders: []string{ + "CF-Connecting-IP", + }, + }, + WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + return &apptest.Deployment{ + Options: opts, + SDKClient: client, + FirstUser: user, + PathAppBaseURL: client.URL, + FlushStats: flushStats, + } + }) +} diff --git a/enterprise/wsproxy/wsproxy_test.go b/enterprise/wsproxy/wsproxy_test.go index cff04e26a4..f49f4074dc 100644 --- a/enterprise/wsproxy/wsproxy_test.go +++ b/enterprise/wsproxy/wsproxy_test.go @@ -45,6 +45,7 @@ func TestDERPOnly(t *testing.T) { deploymentValues := coderdtest.DeploymentValues(t) deploymentValues.Experiments = []string{ "*", + string(codersdk.ExperimentMultiOrganization), } client, closer, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ @@ -64,7 +65,8 @@ func TestDERPOnly(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) @@ -92,6 +94,7 @@ func TestDERP(t *testing.T) { deploymentValues := coderdtest.DeploymentValues(t) deploymentValues.Experiments = []string{ "*", + string(codersdk.ExperimentMultiOrganization), } client, closer, api, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ @@ -111,7 +114,8 @@ func TestDERP(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) @@ -323,6 +327,7 @@ func TestDERPEndToEnd(t *testing.T) { deploymentValues := coderdtest.DeploymentValues(t) deploymentValues.Experiments = []string{ "*", + string(codersdk.ExperimentMultiOrganization), } deploymentValues.DERP.Config.BlockDirect = true @@ -343,7 +348,8 @@ func TestDERPEndToEnd(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) @@ -461,6 +467,7 @@ func TestDERPMesh(t *testing.T) { deploymentValues := coderdtest.DeploymentValues(t) deploymentValues.Experiments = []string{ "*", + string(codersdk.ExperimentMultiOrganization), } client, closer, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ @@ -480,7 +487,8 @@ func TestDERPMesh(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) @@ -602,6 +610,7 @@ func TestWorkspaceProxyDERPMeshProbe(t *testing.T) { deploymentValues := coderdtest.DeploymentValues(t) deploymentValues.Experiments = []string{ "*", + string(codersdk.ExperimentMultiOrganization), } client, closer, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ @@ -621,7 +630,8 @@ func TestWorkspaceProxyDERPMeshProbe(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) @@ -712,6 +722,7 @@ func TestWorkspaceProxyDERPMeshProbe(t *testing.T) { deploymentValues := coderdtest.DeploymentValues(t) deploymentValues.Experiments = []string{ "*", + string(codersdk.ExperimentMultiOrganization), } client, closer, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ @@ -731,7 +742,8 @@ func TestWorkspaceProxyDERPMeshProbe(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) @@ -799,6 +811,7 @@ func TestWorkspaceProxyDERPMeshProbe(t *testing.T) { deploymentValues := coderdtest.DeploymentValues(t) deploymentValues.Experiments = []string{ "*", + string(codersdk.ExperimentMultiOrganization), } client, closer, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ @@ -818,7 +831,8 @@ func TestWorkspaceProxyDERPMeshProbe(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) @@ -913,6 +927,7 @@ func TestWorkspaceProxyWorkspaceApps(t *testing.T) { deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = serpent.Bool(opts.DangerousAllowPathAppSiteOwnerAccess) deploymentValues.Experiments = []string{ "*", + string(codersdk.ExperimentMultiOrganization), } proxyStatsCollectorFlushCh := make(chan chan<- struct{}, 1) @@ -943,7 +958,8 @@ func TestWorkspaceProxyWorkspaceApps(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, }) @@ -982,6 +998,7 @@ func TestWorkspaceProxyWorkspaceApps_BlockDirect(t *testing.T) { deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = serpent.Bool(opts.DangerousAllowPathAppSiteOwnerAccess) deploymentValues.Experiments = []string{ "*", + string(codersdk.ExperimentMultiOrganization), } proxyStatsCollectorFlushCh := make(chan chan<- struct{}, 1) @@ -1012,7 +1029,8 @@ func TestWorkspaceProxyWorkspaceApps_BlockDirect(t *testing.T) { }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ - codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureWorkspaceProxy: 1, + codersdk.FeatureMultipleOrganizations: 1, }, }, })