Skip to content

fix(cli): add check for DisableOwnerWorkspaceExec in scaletest #14417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions cli/exp_scaletest.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func (s *scaletestTracingFlags) provider(ctx context.Context) (trace.TracerProvi
}

var closeTracingOnce sync.Once
return tracerProvider, func(ctx context.Context) error {
return tracerProvider, func(_ context.Context) error {
var err error
closeTracingOnce.Do(func() {
// Allow time to upload traces even if ctx is canceled
Expand Down Expand Up @@ -430,7 +430,7 @@ func (r *RootCmd) scaletestCleanup() *serpent.Command {
}

cliui.Infof(inv.Stdout, "Fetching scaletest workspaces...")
workspaces, err := getScaletestWorkspaces(ctx, client, template)
workspaces, _, err := getScaletestWorkspaces(ctx, client, "", template)
if err != nil {
return err
}
Expand Down Expand Up @@ -863,6 +863,7 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Command {
tickInterval time.Duration
bytesPerTick int64
ssh bool
useHostLogin bool
app string
template string
targetWorkspaces string
Expand Down Expand Up @@ -926,10 +927,18 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Command {
return xerrors.Errorf("get app host: %w", err)
}

workspaces, err := getScaletestWorkspaces(inv.Context(), client, template)
var owner string
if useHostLogin {
owner = codersdk.Me
}

workspaces, numSkipped, err := getScaletestWorkspaces(inv.Context(), client, owner, template)
if err != nil {
return err
}
if numSkipped > 0 {
cliui.Warnf(inv.Stdout, "CODER_DISABLE_OWNER_WORKSPACE_ACCESS is set on the deployment.\n\t%d workspace(s) were skipped due to ownership mismatch.\n\tSet --use-host-login to only target workspaces you own.", numSkipped)
}

if targetWorkspaceEnd == 0 {
targetWorkspaceEnd = len(workspaces)
Expand Down Expand Up @@ -1092,6 +1101,13 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Command {
Description: "Send WebSocket traffic to a workspace app (proxied via coderd), cannot be used with --ssh.",
Value: serpent.StringOf(&app),
},
{
Flag: "use-host-login",
Env: "CODER_SCALETEST_USE_HOST_LOGIN",
Default: "false",
Description: "Connect as the currently logged in user.",
Value: serpent.BoolOf(&useHostLogin),
},
}

tracingFlags.attach(&cmd.Options)
Expand Down Expand Up @@ -1378,22 +1394,35 @@ func isScaleTestWorkspace(workspace codersdk.Workspace) bool {
strings.HasPrefix(workspace.Name, "scaletest-")
}

func getScaletestWorkspaces(ctx context.Context, client *codersdk.Client, template string) ([]codersdk.Workspace, error) {
func getScaletestWorkspaces(ctx context.Context, client *codersdk.Client, owner, template string) ([]codersdk.Workspace, int, error) {
var (
pageNumber = 0
limit = 100
workspaces []codersdk.Workspace
skipped int
)

me, err := client.User(ctx, codersdk.Me)
if err != nil {
return nil, 0, xerrors.Errorf("check logged-in user")
}

dv, err := client.DeploymentConfig(ctx)
if err != nil {
return nil, 0, xerrors.Errorf("fetch deployment config: %w", err)
}
noOwnerAccess := dv.Values != nil && dv.Values.DisableOwnerWorkspaceExec.Value()

for {
page, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Name: "scaletest-",
Template: template,
Owner: owner,
Offset: pageNumber * limit,
Limit: limit,
})
if err != nil {
return nil, xerrors.Errorf("fetch scaletest workspaces page %d: %w", pageNumber, err)
return nil, 0, xerrors.Errorf("fetch scaletest workspaces page %d: %w", pageNumber, err)
}

pageNumber++
Expand All @@ -1403,13 +1432,18 @@ func getScaletestWorkspaces(ctx context.Context, client *codersdk.Client, templa

pageWorkspaces := make([]codersdk.Workspace, 0, len(page.Workspaces))
for _, w := range page.Workspaces {
if isScaleTestWorkspace(w) {
pageWorkspaces = append(pageWorkspaces, w)
if !isScaleTestWorkspace(w) {
continue
}
if noOwnerAccess && w.OwnerID != me.ID {
skipped++
continue
}
pageWorkspaces = append(pageWorkspaces, w)
}
workspaces = append(workspaces, pageWorkspaces...)
}
return workspaces, nil
return workspaces, skipped, nil
}

func getScaletestUsers(ctx context.Context, client *codersdk.Client) ([]codersdk.User, error) {
Expand Down
70 changes: 70 additions & 0 deletions cli/exptest/exptest_scaletest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package exptest_test

import (
"bytes"
"context"
"testing"

"github.com/stretchr/testify/require"

"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
)

// This test validates that the scaletest CLI filters out workspaces not owned
// when disable owner workspace access is set.
// This test is in its own package because it mutates a global variable that
// can influence other tests in the same package.
// nolint:paralleltest
func TestScaleTestWorkspaceTraffic_UseHostLogin(t *testing.T) {
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancelFunc()

log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
client := coderdtest.New(t, &coderdtest.Options{
Logger: &log,
IncludeProvisionerDaemon: true,
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
dv.DisableOwnerWorkspaceExec = true
}),
})
Comment on lines +22 to +33
Copy link
Member

@Emyrk Emyrk Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this test not run in go tests?

I ask because these roles are global, and it is an unfortunate consequence atm. (maybe custom roles can solve this without being global in the future).

You can see how I reset the global state here when using this option:

rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
NoOwnerWorkspaceExec: true,
})
t.Cleanup(func() { rbac.ReloadBuiltinRoles(nil) })

and of course, no t.Parallel()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, each package is its own test binary. So the global variable would only affect tests run in the same package.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a require.FailNow() and validated that the test does indeed get run.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is passing, so the global-ness is not impacting the rest 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Cian is right, you can think of each package as being built as its own test binary (basically that's what actually happens too), so global scope changes are constrained to that package. This is one of the test parallelism mechanisms in Go too (run different packages concurrently) and works irrespective of t.Parallel().

owner := coderdtest.CreateFirstUser(t, client)
tv := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, tv.ID)
tpl := coderdtest.CreateTemplate(t, client, owner.OrganizationID, tv.ID)
// Create a workspace owned by a different user
memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
_ = coderdtest.CreateWorkspace(t, memberClient, tpl.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.Name = "scaletest-workspace"
})

// Test without --use-host-login first.g
inv, root := clitest.New(t, "exp", "scaletest", "workspace-traffic",
"--template", tpl.Name,
)
// nolint:gocritic // We are intentionally testing this as the owner.
clitest.SetupConfig(t, client, root)
var stdoutBuf bytes.Buffer
inv.Stdout = &stdoutBuf

err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "no scaletest workspaces exist")
require.Contains(t, stdoutBuf.String(), `1 workspace(s) were skipped`)

// Test once again with --use-host-login.
inv, root = clitest.New(t, "exp", "scaletest", "workspace-traffic",
"--template", tpl.Name,
"--use-host-login",
)
// nolint:gocritic // We are intentionally testing this as the owner.
clitest.SetupConfig(t, client, root)
stdoutBuf.Reset()
inv.Stdout = &stdoutBuf

err = inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "no scaletest workspaces exist")
require.NotContains(t, stdoutBuf.String(), `1 workspace(s) were skipped`)
}
16 changes: 13 additions & 3 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import (
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/telemetry"
"github.com/coder/coder/v2/coderd/unhanger"
Expand Down Expand Up @@ -268,9 +269,18 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
if options.DeploymentValues == nil {
options.DeploymentValues = DeploymentValues(t)
}
// This value is not safe to run in parallel.
if options.DeploymentValues.DisableOwnerWorkspaceExec {
t.Logf("WARNING: DisableOwnerWorkspaceExec is set, this is not safe in parallel tests!")
// DisableOwnerWorkspaceExec modifies the 'global' RBAC roles. Fast-fail tests if we detect this.
if !options.DeploymentValues.DisableOwnerWorkspaceExec.Value() {
ownerSubj := rbac.Subject{
Roles: rbac.RoleIdentifiers{rbac.RoleOwner()},
Scope: rbac.ScopeAll,
}
if err := options.Authorizer.Authorize(context.Background(), ownerSubj, policy.ActionSSH, rbac.ResourceWorkspace); err != nil {
if rbac.IsUnauthorizedError(err) {
t.Fatal("Side-effect of DisableOwnerWorkspaceExec detected in unrelated test. Please move the test that requires DisableOwnerWorkspaceExec to its own package so that it does not impact other tests!")
}
require.NoError(t, err)
}
}

// If no ratelimits are set, disable all rate limiting for tests.
Expand Down
Loading