mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
chore: protect organization endpoints with license (#14001)
* chore: move multi-org endpoints into enterprise directory All multi-organization features are gated behind "premium" licenses. Enterprise licenses can no longer access organization CRUD.
This commit is contained in:
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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})
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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})
|
||||
|
Reference in New Issue
Block a user