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:
Steven Masley
2024-07-25 16:07:53 -05:00
committed by GitHub
parent 915f69080a
commit 7ea1a4c686
37 changed files with 2222 additions and 1678 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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.

View File

@ -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})
}

View File

@ -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,
}
}

View File

@ -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 {

View File

@ -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))
}

View File

@ -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)
})
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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()

View File

@ -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,
}
})
}

View File

@ -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})