mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: allow member users to generate support bundles (#23040)
Fixes AIGOV-141 The `coder support bundle` command previously required admin permissions (`Read DeploymentConfig`) and would abort entirely for non-admin `member` users with: ``` failed authorization check: cannot Read DeploymentValues ``` This change makes the command **degrade gracefully** instead of failing outright. <details> <summary> Changes </summary> ### `support/support.go` - **`Run()`**: The authorization check for `Read DeploymentValues` is now a soft warning instead of a hard gate. Unauthenticated users (401) still fail, but authenticated users with insufficient permissions proceed with reduced data. - **`DeploymentInfo()`**: `DeploymentConfig` and `DebugHealth` fetches now handle 403/401 responses gracefully, matching the existing pattern used by `DeploymentStats`, `Entitlements`, and `HealthSettings`. - **`NetworkInfo()`**: Coordinator debug and tailnet debug fetches now check response status codes for 403/401 before reading the body. ### `cli/support.go` - **`summarizeBundle()`**: No longer returns early when `Config` or `HealthReport` is nil. Instead prints warnings and continues summarizing available data (e.g., netcheck). ### Tests - `MissingPrivilege` → `MemberNoWorkspace`: Asserts member users can generate a bundle successfully with degraded admin-only data. - `NoPrivilege` → `MemberCanGenerateBundle`: Asserts the CLI produces a valid zip bundle for member users. - All existing tests continue to pass (`NoAuth`, `OK`, `OK_NoWorkspace`, `DontPanic`, etc.). ## Behavior matrix | User type | Before | After | |---|---|---| | **Admin** | Full bundle | Full bundle (no change) | | **Member** | Hard error | Bundle with degraded admin-only data | | **Unauthenticated** | Hard error | Hard error (no change) | Related to PRODUCT-182
This commit is contained in:
+26
-17
@@ -113,6 +113,20 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
|||||||
)
|
)
|
||||||
cliLog.Debug(inv.Context(), "invocation", slog.F("args", strings.Join(os.Args, " ")))
|
cliLog.Debug(inv.Context(), "invocation", slog.F("args", strings.Join(os.Args, " ")))
|
||||||
|
|
||||||
|
// Bypass rate limiting for support bundle collection since it makes many API calls.
|
||||||
|
// Note: this can only be done by the owner user.
|
||||||
|
if ok, err := support.CanGenerateFull(inv.Context(), client); err == nil && ok {
|
||||||
|
cliLog.Debug(inv.Context(), "running as owner")
|
||||||
|
client.HTTPClient.Transport = &codersdk.HeaderTransport{
|
||||||
|
Transport: client.HTTPClient.Transport,
|
||||||
|
Header: http.Header{codersdk.BypassRatelimitHeader: {"true"}},
|
||||||
|
}
|
||||||
|
} else if !ok {
|
||||||
|
cliLog.Warn(inv.Context(), "not running as owner, not all information available")
|
||||||
|
} else {
|
||||||
|
cliLog.Error(inv.Context(), "failed to look up current user", slog.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we're running inside a workspace
|
// Check if we're running inside a workspace
|
||||||
if val, found := os.LookupEnv("CODER"); found && val == "true" {
|
if val, found := os.LookupEnv("CODER"); found && val == "true" {
|
||||||
cliui.Warn(inv.Stderr, "Running inside Coder workspace; this can affect results!")
|
cliui.Warn(inv.Stderr, "Running inside Coder workspace; this can affect results!")
|
||||||
@@ -200,12 +214,6 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
|||||||
_, _ = fmt.Fprintln(inv.Stderr, "pprof data collection will take approximately 30 seconds...")
|
_, _ = fmt.Fprintln(inv.Stderr, "pprof data collection will take approximately 30 seconds...")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bypass rate limiting for support bundle collection since it makes many API calls.
|
|
||||||
client.HTTPClient.Transport = &codersdk.HeaderTransport{
|
|
||||||
Transport: client.HTTPClient.Transport,
|
|
||||||
Header: http.Header{codersdk.BypassRatelimitHeader: {"true"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
deps := support.Deps{
|
deps := support.Deps{
|
||||||
Client: client,
|
Client: client,
|
||||||
// Support adds a sink so we don't need to supply one ourselves.
|
// Support adds a sink so we don't need to supply one ourselves.
|
||||||
@@ -354,19 +362,20 @@ func summarizeBundle(inv *serpent.Invocation, bun *support.Bundle) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if bun.Deployment.Config == nil {
|
var docsURL string
|
||||||
cliui.Error(inv.Stdout, "No deployment configuration available!")
|
if bun.Deployment.Config != nil {
|
||||||
return
|
docsURL = bun.Deployment.Config.Values.DocsURL.String()
|
||||||
|
} else {
|
||||||
|
cliui.Warn(inv.Stdout, "No deployment configuration available. This may require the Owner role.")
|
||||||
}
|
}
|
||||||
|
|
||||||
docsURL := bun.Deployment.Config.Values.DocsURL.String()
|
if bun.Deployment.HealthReport != nil {
|
||||||
if bun.Deployment.HealthReport == nil {
|
deployHealthSummary := bun.Deployment.HealthReport.Summarize(docsURL)
|
||||||
cliui.Error(inv.Stdout, "No deployment health report available!")
|
if len(deployHealthSummary) > 0 {
|
||||||
return
|
cliui.Warn(inv.Stdout, "Deployment health issues detected:", deployHealthSummary...)
|
||||||
}
|
}
|
||||||
deployHealthSummary := bun.Deployment.HealthReport.Summarize(docsURL)
|
} else {
|
||||||
if len(deployHealthSummary) > 0 {
|
cliui.Warn(inv.Stdout, "No deployment health report available.")
|
||||||
cliui.Warn(inv.Stdout, "Deployment health issues detected:", deployHealthSummary...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if bun.Network.Netcheck == nil {
|
if bun.Network.Netcheck == nil {
|
||||||
|
|||||||
+30
-3
@@ -132,12 +132,35 @@ func TestSupportBundle(t *testing.T) {
|
|||||||
assertBundleContents(t, path, true, false, []string{secretValue})
|
assertBundleContents(t, path, true, false, []string{secretValue})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NoPrivilege", func(t *testing.T) {
|
t.Run("MemberCanGenerateBundle", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
inv, root := clitest.New(t, "support", "bundle", memberWorkspace.Workspace.Name, "--yes")
|
|
||||||
|
d := t.TempDir()
|
||||||
|
path := filepath.Join(d, "bundle.zip")
|
||||||
|
inv, root := clitest.New(t, "support", "bundle", memberWorkspace.Workspace.Name, "--output-file", path, "--yes")
|
||||||
clitest.SetupConfig(t, memberClient, root)
|
clitest.SetupConfig(t, memberClient, root)
|
||||||
err := inv.Run()
|
err := inv.Run()
|
||||||
require.ErrorContains(t, err, "failed authorization check")
|
require.NoError(t, err)
|
||||||
|
r, err := zip.OpenReader(path)
|
||||||
|
require.NoError(t, err, "open zip file")
|
||||||
|
defer r.Close()
|
||||||
|
fileNames := make(map[string]struct{}, len(r.File))
|
||||||
|
for _, f := range r.File {
|
||||||
|
fileNames[f.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
// These should always be present in the zip structure, even if
|
||||||
|
// the content is null/empty for non-admin users.
|
||||||
|
for _, name := range []string{
|
||||||
|
"deployment/buildinfo.json",
|
||||||
|
"deployment/config.json",
|
||||||
|
"workspace/workspace.json",
|
||||||
|
"logs.txt",
|
||||||
|
"cli_logs.txt",
|
||||||
|
"network/netcheck.json",
|
||||||
|
"network/interfaces.json",
|
||||||
|
} {
|
||||||
|
require.Contains(t, fileNames, name)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// This ensures that the CLI does not panic when trying to generate a support bundle
|
// This ensures that the CLI does not panic when trying to generate a support bundle
|
||||||
@@ -159,6 +182,10 @@ func TestSupportBundle(t *testing.T) {
|
|||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
t.Logf("received request: %s %s", r.Method, r.URL)
|
t.Logf("received request: %s %s", r.Method, r.URL)
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
|
case "/api/v2/users/me":
|
||||||
|
resp := codersdk.User{}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
assert.NoError(t, json.NewEncoder(w).Encode(resp))
|
||||||
case "/api/v2/authcheck":
|
case "/api/v2/authcheck":
|
||||||
// Fake auth check
|
// Fake auth check
|
||||||
resp := codersdk.AuthorizationResponse{
|
resp := codersdk.AuthorizationResponse{
|
||||||
|
|||||||
@@ -27,32 +27,32 @@ A brief overview of all files contained in the bundle is provided below:
|
|||||||
> Detailed descriptions of all the information available in the bundle is
|
> Detailed descriptions of all the information available in the bundle is
|
||||||
> out of scope, as support bundles are primarily intended for internal use.
|
> out of scope, as support bundles are primarily intended for internal use.
|
||||||
|
|
||||||
| Filename | Description |
|
| Filename | Description |
|
||||||
|-----------------------------------|------------------------------------------------------------------------------------------------------------|
|
|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `agent/agent.json` | The agent used to connect to the workspace with environment variables stripped. |
|
| `agent/agent.json` | The agent used to connect to the workspace with environment variables stripped. |
|
||||||
| `agent/agent_magicsock.html` | The contents of the HTTP debug endpoint of the agent's Tailscale Wireguard connection. |
|
| `agent/agent_magicsock.html` | The contents of the HTTP debug endpoint of the agent's Tailscale Wireguard connection. |
|
||||||
| `agent/client_magicsock.html` | The contents of the HTTP debug endpoint of the client's Tailscale Wireguard connection. |
|
| `agent/client_magicsock.html` | The contents of the HTTP debug endpoint of the client's Tailscale Wireguard connection. |
|
||||||
| `agent/listening_ports.json` | The listening ports detected by the selected agent running in the workspace. |
|
| `agent/listening_ports.json` | The listening ports detected by the selected agent running in the workspace. |
|
||||||
| `agent/logs.txt` | The logs of the selected agent running in the workspace. |
|
| `agent/logs.txt` | The logs of the selected agent running in the workspace. |
|
||||||
| `agent/manifest.json` | The manifest of the selected agent with environment variables stripped. |
|
| `agent/manifest.json` | The manifest of the selected agent with environment variables stripped. |
|
||||||
| `agent/startup_logs.txt` | Startup logs of the workspace agent. |
|
| `agent/startup_logs.txt` | Startup logs of the workspace agent. |
|
||||||
| `agent/prometheus.txt` | The contents of the agent's Prometheus endpoint. |
|
| `agent/prometheus.txt` | The contents of the agent's Prometheus endpoint. |
|
||||||
| `cli_logs.txt` | Logs from running the `coder support bundle` command. |
|
| `cli_logs.txt` | Logs from running the `coder support bundle` command. |
|
||||||
| `deployment/buildinfo.json` | Coder version and build information. |
|
| `deployment/buildinfo.json` | Coder version and build information. |
|
||||||
| `deployment/config.json` | Deployment [configuration](../reference/api/general.md#get-deployment-config), with secret values removed. |
|
| `deployment/config.json` | Deployment [configuration](../reference/api/general.md#get-deployment-config), with secret values removed. *Requires Owner role.* |
|
||||||
| `deployment/experiments.json` | Any [experiments](../reference/cli/server.md#--experiments) currently enabled for the deployment. |
|
| `deployment/experiments.json` | Any [experiments](../reference/cli/server.md#--experiments) currently enabled for the deployment. |
|
||||||
| `deployment/health.json` | A snapshot of the [health status](../admin/monitoring/health-check.md) of the deployment. |
|
| `deployment/health.json` | A snapshot of the [health status](../admin/monitoring/health-check.md) of the deployment. *Requires Owner role.* |
|
||||||
| `logs.txt` | Logs from the `codersdk.Client` used to generate the bundle. |
|
| `logs.txt` | Logs from the `codersdk.Client` used to generate the bundle. |
|
||||||
| `network/connection_info.json` | Information used by workspace agents used to connect to Coder (DERP map etc.) |
|
| `network/connection_info.json` | Information used by workspace agents used to connect to Coder (DERP map etc.) |
|
||||||
| `network/coordinator_debug.html` | Peers currently connected to each Coder instance and the tunnels established between peers. |
|
| `network/coordinator_debug.html` | Peers currently connected to each Coder instance and the tunnels established between peers. *Requires Owner role.* |
|
||||||
| `network/netcheck.json` | Results of running `coder netcheck` locally. |
|
| `network/netcheck.json` | Results of running `coder netcheck` locally. |
|
||||||
| `network/tailnet_debug.html` | Tailnet coordinators, their heartbeat ages, connected peers, and tunnels. |
|
| `network/tailnet_debug.html` | Tailnet coordinators, their heartbeat ages, connected peers, and tunnels. *Requires Owner role.* |
|
||||||
| `workspace/build_logs.txt` | Build logs of the selected workspace. |
|
| `workspace/build_logs.txt` | Build logs of the selected workspace. |
|
||||||
| `workspace/workspace.json` | Details of the selected workspace. |
|
| `workspace/workspace.json` | Details of the selected workspace. |
|
||||||
| `workspace/parameters.json` | Build parameters of the selected workspace. |
|
| `workspace/parameters.json` | Build parameters of the selected workspace. |
|
||||||
| `workspace/template.json` | The template currently in use by the selected workspace. |
|
| `workspace/template.json` | The template currently in use by the selected workspace. |
|
||||||
| `workspace/template_file.zip` | The source code of the template currently in use by the selected workspace. |
|
| `workspace/template_file.zip` | The source code of the template currently in use by the selected workspace. |
|
||||||
| `workspace/template_version.json` | The template version currently in use by the selected workspace. |
|
| `workspace/template_version.json` | The template version currently in use by the selected workspace. |
|
||||||
|
|
||||||
## How do I generate a Support Bundle?
|
## How do I generate a Support Bundle?
|
||||||
|
|
||||||
@@ -67,7 +67,10 @@ A brief overview of all files contained in the bundle is provided below:
|
|||||||
> experiencing workspace connectivity issues.
|
> experiencing workspace connectivity issues.
|
||||||
|
|
||||||
3. Ensure you are [logged in](../reference/cli/login.md#login) to your Coder
|
3. Ensure you are [logged in](../reference/cli/login.md#login) to your Coder
|
||||||
deployment as a user with the Owner privilege.
|
deployment. Any authenticated user can generate a support bundle. Users with
|
||||||
|
the Owner role will get the most complete bundle; non-admin users will still
|
||||||
|
get a useful bundle but some admin-only data will be omitted (see the note
|
||||||
|
below).
|
||||||
|
|
||||||
4. Run `coder support bundle [owner/workspace]`, and respond `yes` to the
|
4. Run `coder support bundle [owner/workspace]`, and respond `yes` to the
|
||||||
prompt. The support bundle will be generated in the current directory with
|
prompt. The support bundle will be generated in the current directory with
|
||||||
|
|||||||
+53
-12
@@ -163,6 +163,11 @@ func DeploymentInfo(ctx context.Context, client *codersdk.Client, log slog.Logge
|
|||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
dc, err := client.DeploymentConfig(ctx)
|
dc, err := client.DeploymentConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if cerr, ok := codersdk.AsError(err); ok && (cerr.StatusCode() == http.StatusForbidden || cerr.StatusCode() == http.StatusUnauthorized) {
|
||||||
|
log.Warn(ctx, "unable to fetch deployment config",
|
||||||
|
slog.F("status", cerr.StatusCode()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return xerrors.Errorf("fetch deployment config: %w", err)
|
return xerrors.Errorf("fetch deployment config: %w", err)
|
||||||
}
|
}
|
||||||
d.Config = dc
|
d.Config = dc
|
||||||
@@ -172,6 +177,11 @@ func DeploymentInfo(ctx context.Context, client *codersdk.Client, log slog.Logge
|
|||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
hr, err := healthsdk.New(client).DebugHealth(ctx)
|
hr, err := healthsdk.New(client).DebugHealth(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if cerr, ok := codersdk.AsError(err); ok && (cerr.StatusCode() == http.StatusForbidden || cerr.StatusCode() == http.StatusUnauthorized) {
|
||||||
|
log.Warn(ctx, "unable to fetch health report",
|
||||||
|
slog.F("status", cerr.StatusCode()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return xerrors.Errorf("fetch health report: %w", err)
|
return xerrors.Errorf("fetch health report: %w", err)
|
||||||
}
|
}
|
||||||
d.HealthReport = &hr
|
d.HealthReport = &hr
|
||||||
@@ -365,6 +375,12 @@ func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger)
|
|||||||
return xerrors.Errorf("fetch coordinator debug page: %w", err)
|
return xerrors.Errorf("fetch coordinator debug page: %w", err)
|
||||||
}
|
}
|
||||||
defer coordResp.Body.Close()
|
defer coordResp.Body.Close()
|
||||||
|
if coordResp.StatusCode == http.StatusForbidden || coordResp.StatusCode == http.StatusUnauthorized {
|
||||||
|
_, _ = io.Copy(io.Discard, coordResp.Body)
|
||||||
|
log.Warn(ctx, "unable to fetch coordinator debug page",
|
||||||
|
slog.F("status", coordResp.StatusCode))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
bs, err := io.ReadAll(coordResp.Body)
|
bs, err := io.ReadAll(coordResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("read coordinator debug page: %w", err)
|
return xerrors.Errorf("read coordinator debug page: %w", err)
|
||||||
@@ -379,6 +395,12 @@ func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger)
|
|||||||
return xerrors.Errorf("fetch tailnet debug page: %w", err)
|
return xerrors.Errorf("fetch tailnet debug page: %w", err)
|
||||||
}
|
}
|
||||||
defer tailResp.Body.Close()
|
defer tailResp.Body.Close()
|
||||||
|
if tailResp.StatusCode == http.StatusForbidden || tailResp.StatusCode == http.StatusUnauthorized {
|
||||||
|
_, _ = io.Copy(io.Discard, tailResp.Body)
|
||||||
|
log.Warn(ctx, "unable to fetch tailnet debug page",
|
||||||
|
slog.F("status", tailResp.StatusCode))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
bs, err := io.ReadAll(tailResp.Body)
|
bs, err := io.ReadAll(tailResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("read tailnet debug page: %w", err)
|
return xerrors.Errorf("read tailnet debug page: %w", err)
|
||||||
@@ -976,11 +998,11 @@ func PprofInfoFromAgent(ctx context.Context, conn workspacesdk.AgentConn, log sl
|
|||||||
return &p
|
return &p
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run generates a support bundle with the given dependencies.
|
// CanGenerateFull checks if the user can generate a 'full' support bundle or
|
||||||
func Run(ctx context.Context, d *Deps) (*Bundle, error) {
|
// only has permissions to generate a 'partial' bundle.
|
||||||
var b Bundle
|
func CanGenerateFull(ctx context.Context, client *codersdk.Client) (bool, error) {
|
||||||
if d.Client == nil {
|
if client == nil {
|
||||||
return nil, xerrors.Errorf("developer error: missing client!")
|
return false, xerrors.Errorf("developer error: missing client!")
|
||||||
}
|
}
|
||||||
|
|
||||||
authChecks := map[string]codersdk.AuthorizationCheck{
|
authChecks := map[string]codersdk.AuthorizationCheck{
|
||||||
@@ -992,6 +1014,28 @@ func Run(ctx context.Context, d *Deps) (*Bundle, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authResp, err := client.AuthCheck(ctx, codersdk.AuthorizationRequest{Checks: authChecks})
|
||||||
|
if err != nil {
|
||||||
|
// If the auth check itself fails (e.g., 401 Unauthorized
|
||||||
|
// because there is no valid session), this is a hard error.
|
||||||
|
return false, xerrors.Errorf("check authorization: %w", err)
|
||||||
|
}
|
||||||
|
for _, v := range authResp {
|
||||||
|
if !v { // all checks must pass
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run generates a support bundle with the given dependencies.
|
||||||
|
func Run(ctx context.Context, d *Deps) (*Bundle, error) {
|
||||||
|
var b Bundle
|
||||||
|
if d.Client == nil {
|
||||||
|
return nil, xerrors.Errorf("developer error: missing client!")
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure we capture logs from the client.
|
// Ensure we capture logs from the client.
|
||||||
var logw strings.Builder
|
var logw strings.Builder
|
||||||
d.Log = d.Log.AppendSinks(sloghuman.Sink(&logw))
|
d.Log = d.Log.AppendSinks(sloghuman.Sink(&logw))
|
||||||
@@ -1000,15 +1044,12 @@ func Run(ctx context.Context, d *Deps) (*Bundle, error) {
|
|||||||
b.Logs = strings.Split(logw.String(), "\n")
|
b.Logs = strings.Split(logw.String(), "\n")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
authResp, err := d.Client.AuthCheck(ctx, codersdk.AuthorizationRequest{Checks: authChecks})
|
// No point running without auth as minimal information available.
|
||||||
|
me, err := d.Client.User(ctx, codersdk.Me)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &b, xerrors.Errorf("check authorization: %w", err)
|
return nil, err
|
||||||
}
|
|
||||||
for k, v := range authResp {
|
|
||||||
if !v {
|
|
||||||
return &b, xerrors.Errorf("failed authorization check: cannot %s", k)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
d.Log.Info(ctx, "running as user", slog.F("me", me))
|
||||||
|
|
||||||
totalCap := d.WorkspacesTotalCap
|
totalCap := d.WorkspacesTotalCap
|
||||||
|
|
||||||
|
|||||||
+20
-8
@@ -162,14 +162,12 @@ func TestRun(t *testing.T) {
|
|||||||
Log: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Named("bundle").Leveled(slog.LevelDebug),
|
Log: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Named("bundle").Leveled(slog.LevelDebug),
|
||||||
})
|
})
|
||||||
var sdkErr *codersdk.Error
|
var sdkErr *codersdk.Error
|
||||||
require.NotNil(t, bun)
|
require.Nil(t, bun)
|
||||||
require.ErrorAs(t, err, &sdkErr)
|
require.ErrorAs(t, err, &sdkErr)
|
||||||
require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode())
|
require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode())
|
||||||
require.NotEmpty(t, bun)
|
|
||||||
require.NotEmpty(t, bun.Logs)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("MissingPrivilege", func(t *testing.T) {
|
t.Run("MemberNoWorkspace", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
client := coderdtest.New(t, &coderdtest.Options{
|
client := coderdtest.New(t, &coderdtest.Options{
|
||||||
@@ -179,11 +177,25 @@ func TestRun(t *testing.T) {
|
|||||||
memberClient, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
memberClient, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||||
bun, err := support.Run(ctx, &support.Deps{
|
bun, err := support.Run(ctx, &support.Deps{
|
||||||
Client: memberClient,
|
Client: memberClient,
|
||||||
Log: testutil.Logger(t).Named("bundle"),
|
Log: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Named("bundle").Leveled(slog.LevelDebug),
|
||||||
})
|
})
|
||||||
require.ErrorContains(t, err, "failed authorization check")
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, bun)
|
require.NotNil(t, bun)
|
||||||
require.NotEmpty(t, bun.Logs)
|
// Member should still get build info.
|
||||||
|
assertNotNilNotEmpty(t, bun.Deployment.BuildInfo, "deployment build info should be present")
|
||||||
|
// Experiments may be empty if none are configured, but should not error.
|
||||||
|
require.NotNil(t, bun.Deployment.Experiments, "deployment experiments should not be nil")
|
||||||
|
// Member should NOT get admin-only deployment information.
|
||||||
|
assert.Nil(t, bun.Deployment.Config, "member should not see deployment config")
|
||||||
|
assert.Nil(t, bun.Deployment.HealthReport, "member should not see health report")
|
||||||
|
// Member should still get network info from client-side checks.
|
||||||
|
assertNotNilNotEmpty(t, bun.Network.ConnectionInfo, "agent connection info should be present")
|
||||||
|
assertNotNilNotEmpty(t, bun.Network.Interfaces, "network interfaces should be present")
|
||||||
|
assertNotNilNotEmpty(t, bun.Network.Netcheck, "network netcheck should be present")
|
||||||
|
// No workspace specified.
|
||||||
|
assert.Empty(t, bun.Workspace.Workspace, "did not expect workspace to be present")
|
||||||
|
assert.Empty(t, bun.Agent, "did not expect agent to be present")
|
||||||
|
assertNotNilNotEmpty(t, bun.Logs, "bundle logs should be present")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user