From abba0d6b6d1c975d7741ed4e1cd6db8b3676695e Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 4 Oct 2023 14:21:46 +0000 Subject: [PATCH 1/9] feat: add `external-auth` cli --- cli/externalauth.go | 7 + cli/gitaskpass.go | 10 +- cli/gitaskpass_test.go | 8 +- coderd/apidoc/docs.go | 26 +- coderd/apidoc/swagger.json | 26 +- coderd/coderd.go | 4 +- coderd/database/queries.sql.go | 226 +++++++++--------- coderd/externalauth_test.go | 43 +++- coderd/workspaceagents.go | 98 +++++--- codersdk/agentsdk/agentsdk.go | 41 +++- docs/api/agents.md | 17 +- docs/api/schemas.md | 16 +- provisioner/terraform/resources.go | 17 +- .../DeploySettingsLayout/Sidebar.tsx | 4 +- 14 files changed, 328 insertions(+), 215 deletions(-) create mode 100644 cli/externalauth.go diff --git a/cli/externalauth.go b/cli/externalauth.go new file mode 100644 index 0000000000000..b6921dbdf6093 --- /dev/null +++ b/cli/externalauth.go @@ -0,0 +1,7 @@ +package cli + +import "github.com/coder/coder/v2/cli/clibase" + +func (r *RootCmd) externalAuth() *clibase.Cmd { + return &clibase.Cmd{} +} diff --git a/cli/gitaskpass.go b/cli/gitaskpass.go index b7636494c3a25..83ac98094e72e 100644 --- a/cli/gitaskpass.go +++ b/cli/gitaskpass.go @@ -13,6 +13,7 @@ import ( "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/cli/gitauth" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/retry" ) @@ -38,7 +39,9 @@ func (r *RootCmd) gitAskpass() *clibase.Cmd { return xerrors.Errorf("create agent client: %w", err) } - token, err := client.GitAuth(ctx, host, false) + token, err := client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{ + Match: host, + }) if err != nil { var apiError *codersdk.Error if errors.As(err, &apiError) && apiError.StatusCode() == http.StatusNotFound { @@ -63,7 +66,10 @@ func (r *RootCmd) gitAskpass() *clibase.Cmd { } for r := retry.New(250*time.Millisecond, 10*time.Second); r.Wait(ctx); { - token, err = client.GitAuth(ctx, host, true) + token, err = client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{ + Match: host, + Listen: true, + }) if err != nil { continue } diff --git a/cli/gitaskpass_test.go b/cli/gitaskpass_test.go index 5ec7f4c6bb258..92fe3943c1eb8 100644 --- a/cli/gitaskpass_test.go +++ b/cli/gitaskpass_test.go @@ -23,7 +23,7 @@ func TestGitAskpass(t *testing.T) { t.Run("UsernameAndPassword", func(t *testing.T) { t.Parallel() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.GitAuthResponse{ + httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{ Username: "something", Password: "bananas", }) @@ -65,8 +65,8 @@ func TestGitAskpass(t *testing.T) { t.Run("Poll", func(t *testing.T) { t.Parallel() - resp := atomic.Pointer[agentsdk.GitAuthResponse]{} - resp.Store(&agentsdk.GitAuthResponse{ + resp := atomic.Pointer[agentsdk.ExternalAuthResponse]{} + resp.Store(&agentsdk.ExternalAuthResponse{ URL: "https://something.org", }) poll := make(chan struct{}, 10) @@ -96,7 +96,7 @@ func TestGitAskpass(t *testing.T) { }() <-poll stderr.ExpectMatch("Open the following URL to authenticate") - resp.Store(&agentsdk.GitAuthResponse{ + resp.Store(&agentsdk.ExternalAuthResponse{ Username: "username", Password: "password", }) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index e523745f5e303..d6a0e211bf8c9 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4561,7 +4561,7 @@ const docTemplate = `{ } } }, - "/workspaceagents/me/gitauth": { + "/workspaceagents/me/external-auth": { "get": { "security": [ { @@ -4574,17 +4574,24 @@ const docTemplate = `{ "tags": [ "Agents" ], - "summary": "Get workspace agent Git auth", - "operationId": "get-workspace-agent-git-auth", + "summary": "Get workspace agent external auth", + "operationId": "get-workspace-agent-external-auth", "parameters": [ { "type": "string", "format": "uri", - "description": "Git URL", + "description": "Matching URL", "name": "url", "in": "query", "required": true }, + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "query", + "required": true + }, { "type": "boolean", "description": "Wait for a new token to be issued", @@ -4596,7 +4603,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/agentsdk.GitAuthResponse" + "$ref": "#/definitions/agentsdk.ExternalAuthResponse" } } } @@ -6376,16 +6383,23 @@ const docTemplate = `{ } } }, - "agentsdk.GitAuthResponse": { + "agentsdk.ExternalAuthResponse": { "type": "object", "properties": { + "access_token": { + "type": "string" + }, "password": { "type": "string" }, + "type": { + "type": "string" + }, "url": { "type": "string" }, "username": { + "description": "Deprecated: Only supported on ` + "`" + `/workspaceagents/me/gitauth` + "`" + `\nfor backwards compatibility.", "type": "string" } } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index a97a865aaeb9d..2dc4837fc5742 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4011,7 +4011,7 @@ } } }, - "/workspaceagents/me/gitauth": { + "/workspaceagents/me/external-auth": { "get": { "security": [ { @@ -4020,17 +4020,24 @@ ], "produces": ["application/json"], "tags": ["Agents"], - "summary": "Get workspace agent Git auth", - "operationId": "get-workspace-agent-git-auth", + "summary": "Get workspace agent external auth", + "operationId": "get-workspace-agent-external-auth", "parameters": [ { "type": "string", "format": "uri", - "description": "Git URL", + "description": "Matching URL", "name": "url", "in": "query", "required": true }, + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "query", + "required": true + }, { "type": "boolean", "description": "Wait for a new token to be issued", @@ -4042,7 +4049,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/agentsdk.GitAuthResponse" + "$ref": "#/definitions/agentsdk.ExternalAuthResponse" } } } @@ -5614,16 +5621,23 @@ } } }, - "agentsdk.GitAuthResponse": { + "agentsdk.ExternalAuthResponse": { "type": "object", "properties": { + "access_token": { + "type": "string" + }, "password": { "type": "string" }, + "type": { + "type": "string" + }, "url": { "type": "string" }, "username": { + "description": "Deprecated: Only supported on `/workspaceagents/me/gitauth`\nfor backwards compatibility.", "type": "string" } } diff --git a/coderd/coderd.go b/coderd/coderd.go index b8cf0957773c4..00fb41ca401c1 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -812,7 +812,9 @@ func New(options *Options) *API { r.Patch("/startup-logs", api.patchWorkspaceAgentLogsDeprecated) r.Patch("/logs", api.patchWorkspaceAgentLogs) r.Post("/app-health", api.postWorkspaceAppHealth) - r.Get("/gitauth", api.workspaceAgentsGitAuth) + // Deprecated: Required to support legacy agents + r.Get("/gitauth", api.workspaceAgentsExternalAuth) + r.Get("/external-auth", api.workspaceAgentsExternalAuth) r.Get("/gitsshkey", api.agentGitSSHKey) r.Get("/coordinate", api.workspaceAgentCoordinate) r.Post("/report-stats", api.workspaceAgentReportStats) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 40b7ee7e72715..f1ea7719dfdaf 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -9554,6 +9554,119 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } +const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many +SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many +INSERT INTO + workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) +SELECT + $1 :: uuid AS workspace_agent_id, + $2 :: timestamptz AS created_at, + unnest($3 :: uuid [ ]) AS log_source_id, + unnest($4 :: text [ ]) AS log_path, + unnest($5 :: text [ ]) AS script, + unnest($6 :: text [ ]) AS cron, + unnest($7 :: boolean [ ]) AS start_blocks_login, + unnest($8 :: boolean [ ]) AS run_on_start, + unnest($9 :: boolean [ ]) AS run_on_stop, + unnest($10 :: integer [ ]) AS timeout_seconds +RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds +` + +type InsertWorkspaceAgentScriptsParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` + LogPath []string `db:"log_path" json:"log_path"` + Script []string `db:"script" json:"script"` + Cron []string `db:"cron" json:"cron"` + StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` + RunOnStart []bool `db:"run_on_start" json:"run_on_start"` + RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` + TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, + arg.WorkspaceAgentID, + arg.CreatedAt, + pq.Array(arg.LogSourceID), + pq.Array(arg.LogPath), + pq.Array(arg.Script), + pq.Array(arg.Cron), + pq.Array(arg.StartBlocksLogin), + pq.Array(arg.RunOnStart), + pq.Array(arg.RunOnStop), + pq.Array(arg.TimeoutSeconds), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT @@ -10502,116 +10615,3 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID) return err } - -const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many -SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) -` - -func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many -INSERT INTO - workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) -SELECT - $1 :: uuid AS workspace_agent_id, - $2 :: timestamptz AS created_at, - unnest($3 :: uuid [ ]) AS log_source_id, - unnest($4 :: text [ ]) AS log_path, - unnest($5 :: text [ ]) AS script, - unnest($6 :: text [ ]) AS cron, - unnest($7 :: boolean [ ]) AS start_blocks_login, - unnest($8 :: boolean [ ]) AS run_on_start, - unnest($9 :: boolean [ ]) AS run_on_stop, - unnest($10 :: integer [ ]) AS timeout_seconds -RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds -` - -type InsertWorkspaceAgentScriptsParams struct { - WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` - LogPath []string `db:"log_path" json:"log_path"` - Script []string `db:"script" json:"script"` - Cron []string `db:"cron" json:"cron"` - StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` - RunOnStart []bool `db:"run_on_start" json:"run_on_start"` - RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` - TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` -} - -func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, - arg.WorkspaceAgentID, - arg.CreatedAt, - pq.Array(arg.LogSourceID), - pq.Array(arg.LogPath), - pq.Array(arg.Script), - pq.Array(arg.Cron), - pq.Array(arg.StartBlocksLogin), - pq.Array(arg.RunOnStart), - pq.Array(arg.RunOnStop), - pq.Array(arg.TimeoutSeconds), - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/coderd/externalauth_test.go b/coderd/externalauth_test.go index df4e092e05e9b..9ba18b2c0f3a8 100644 --- a/coderd/externalauth_test.go +++ b/coderd/externalauth_test.go @@ -132,7 +132,7 @@ func TestExternalAuthByID(t *testing.T) { }) } -func TestGitAuthDevice(t *testing.T) { +func TestExternalAuthDevice(t *testing.T) { t.Parallel() t.Run("NotSupported", func(t *testing.T) { t.Parallel() @@ -214,7 +214,7 @@ func TestGitAuthDevice(t *testing.T) { } // nolint:bodyclose -func TestGitAuthCallback(t *testing.T) { +func TestExternalAuthCallback(t *testing.T) { t.Parallel() t.Run("NoMatchingConfig", func(t *testing.T) { t.Parallel() @@ -236,7 +236,9 @@ func TestGitAuthCallback(t *testing.T) { agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - _, err := agentClient.GitAuth(context.Background(), "github.com", false) + _, err := agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{ + Match: "github.com", + }) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusNotFound, apiError.StatusCode()) @@ -266,7 +268,9 @@ func TestGitAuthCallback(t *testing.T) { agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - token, err := agentClient.GitAuth(context.Background(), "github.com/asd/asd", false) + token, err := agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + }) require.NoError(t, err) require.True(t, strings.HasSuffix(token.URL, fmt.Sprintf("/external-auth/%s", "github")), token.URL) }) @@ -345,7 +349,9 @@ func TestGitAuthCallback(t *testing.T) { srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) }) - res, err := agentClient.GitAuth(ctx, "github.com/asd/asd", false) + res, err := agentClient.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + }) require.NoError(t, err) require.NotEmpty(t, res.URL) @@ -355,7 +361,9 @@ func TestGitAuthCallback(t *testing.T) { w.WriteHeader(http.StatusForbidden) w.Write([]byte("Something went wrong!")) }) - _, err = agentClient.GitAuth(ctx, "github.com/asd/asd", false) + _, err = agentClient.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + }) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusInternalServerError, apiError.StatusCode()) @@ -395,7 +403,9 @@ func TestGitAuthCallback(t *testing.T) { agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - token, err := agentClient.GitAuth(context.Background(), "github.com/asd/asd", false) + token, err := agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + }) require.NoError(t, err) require.NotEmpty(t, token.URL) @@ -407,7 +417,9 @@ func TestGitAuthCallback(t *testing.T) { // Because the token is expired and `NoRefresh` is specified, // a redirect URL should be returned again. - token, err = agentClient.GitAuth(context.Background(), "github.com/asd/asd", false) + token, err = agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + }) require.NoError(t, err) require.NotEmpty(t, token.URL) }) @@ -438,14 +450,19 @@ func TestGitAuthCallback(t *testing.T) { agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(authToken) - token, err := agentClient.GitAuth(context.Background(), "github.com/asd/asd", false) + token, err := agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + }) require.NoError(t, err) require.NotEmpty(t, token.URL) // Start waiting for the token callback... - tokenChan := make(chan agentsdk.GitAuthResponse, 1) + tokenChan := make(chan agentsdk.ExternalAuthResponse, 1) go func() { - token, err := agentClient.GitAuth(context.Background(), "github.com/asd/asd", true) + token, err := agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + Listen: true, + }) assert.NoError(t, err) tokenChan <- token }() @@ -457,7 +474,9 @@ func TestGitAuthCallback(t *testing.T) { token = <-tokenChan require.Equal(t, "access_token", token.Username) - token, err = agentClient.GitAuth(context.Background(), "github.com/asd/asd", false) + token, err = agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + }) require.NoError(t, err) }) } diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index a3b624dc376ce..acc80a585d25a 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -223,13 +223,20 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request) vscodeProxyURI += fmt.Sprintf(":%s", api.AccessURL.Port()) } + gitAuthConfigs := 0 + for _, cfg := range api.ExternalAuthConfigs { + if codersdk.EnhancedExternalAuthProvider(cfg.Type).Git() { + gitAuthConfigs++ + } + } + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Manifest{ AgentID: apiAgent.ID, Apps: convertApps(dbApps, workspaceAgent, owner, workspace), Scripts: convertScripts(scripts), DERPMap: api.DERPMap(), DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(), - GitAuthConfigs: len(api.ExternalAuthConfigs), + GitAuthConfigs: gitAuthConfigs, EnvironmentVariables: apiAgent.EnvironmentVariables, Directory: apiAgent.Directory, VSCodePortProxyURI: vscodeProxyURI, @@ -2153,44 +2160,62 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request) httpapi.Write(ctx, rw, http.StatusOK, nil) } -// workspaceAgentsGitAuth returns a username and password for use -// with GIT_ASKPASS. +// workspaceAgentsExternalAuth returns an access token for a given URL +// or finds a provider by ID. // -// @Summary Get workspace agent Git auth -// @ID get-workspace-agent-git-auth +// @Summary Get workspace agent external auth +// @ID get-workspace-agent-external-auth // @Security CoderSessionToken // @Produce json // @Tags Agents -// @Param url query string true "Git URL" format(uri) +// @Param match query string true "Match" +// @Param id query string true "Provider ID" // @Param listen query bool false "Wait for a new token to be issued" -// @Success 200 {object} agentsdk.GitAuthResponse -// @Router /workspaceagents/me/gitauth [get] -func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) { +// @Success 200 {object} agentsdk.ExternalAuthResponse +// @Router /workspaceagents/me/external-auth [get] +func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - gitURL := r.URL.Query().Get("url") - if gitURL == "" { + // Either match or configID must be provided! + match := r.URL.Query().Get("match") + if match == "" { + // Support legacy agents! + match = r.URL.Query().Get("url") + } + id := chi.URLParam(r, "id") + if match == "" && id == "" { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "'url' or 'id' must be provided!", + }) + return + } + if match != "" && id != "" { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Missing 'url' query parameter!", + Message: "'url' and 'id' cannot be provided together!", }) return } + // listen determines if the request will wait for a // new token to be issued! listen := r.URL.Query().Has("listen") var externalAuthConfig *externalauth.Config - for _, gitAuth := range api.ExternalAuthConfigs { - if gitAuth.Regex == nil { + for _, extAuth := range api.ExternalAuthConfigs { + if extAuth.ID == id { + externalAuthConfig = extAuth + break + } + if match == "" || extAuth.Regex == nil { continue } - matches := gitAuth.Regex.MatchString(gitURL) + matches := extAuth.Regex.MatchString(match) if !matches { continue } - externalAuthConfig = gitAuth + externalAuthConfig = extAuth } if externalAuthConfig == nil { - detail := "No external auth providers are configured." + detail := "External auth provider not found." if len(api.ExternalAuthConfigs) > 0 { regexURLs := make([]string, 0, len(api.ExternalAuthConfigs)) for _, extAuth := range api.ExternalAuthConfigs { @@ -2199,21 +2224,14 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) } regexURLs = append(regexURLs, fmt.Sprintf("%s=%q", extAuth.ID, extAuth.Regex.String())) } - detail = fmt.Sprintf("The configured external auth provider have regex filters that do not match the git url. Provider url regexs: %s", strings.Join(regexURLs, ",")) + detail = fmt.Sprintf("The configured external auth provider have regex filters that do not match the url. Provider url regex: %s", strings.Join(regexURLs, ",")) } httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ - Message: fmt.Sprintf("No matching external auth provider found in Coder for the url %q.", gitURL), + Message: fmt.Sprintf("No matching external auth provider found in Coder for the url %q.", match), Detail: detail, }) return } - enhancedType := codersdk.EnhancedExternalAuthProvider(externalAuthConfig.Type) - if !enhancedType.Git() { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "External auth provider does not support git.", - }) - return - } workspaceAgent := httpmw.WorkspaceAgent(r) // We must get the workspace to get the owner ID! resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID) @@ -2285,7 +2303,7 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) if !valid { continue } - httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(enhancedType, externalAuthLink.OAuthAccessToken)) + httpapi.Write(ctx, rw, http.StatusOK, createExternalAuthResponse(externalAuthConfig.Type, externalAuthLink.OAuthAccessToken)) return } } @@ -2313,7 +2331,7 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) return } - httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.ExternalAuthResponse{ URL: redirectURL.String(), }) return @@ -2328,35 +2346,39 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) return } if !updated { - httpapi.Write(ctx, rw, http.StatusOK, agentsdk.GitAuthResponse{ + httpapi.Write(ctx, rw, http.StatusOK, agentsdk.ExternalAuthResponse{ URL: redirectURL.String(), }) return } - httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(enhancedType, externalAuthLink.OAuthAccessToken)) + httpapi.Write(ctx, rw, http.StatusOK, createExternalAuthResponse(externalAuthConfig.Type, externalAuthLink.OAuthAccessToken)) } -// Provider types have different username/password formats. -func formatGitAuthAccessToken(typ codersdk.EnhancedExternalAuthProvider, token string) agentsdk.GitAuthResponse { - var resp agentsdk.GitAuthResponse +// createExternalAuthResponse creates an ExternalAuthResponse based on the +// provider type. This is to support legacy `/workspaceagents/me/gitauth` +// which uses `Username` and `Password`. +func createExternalAuthResponse(typ, token string) agentsdk.ExternalAuthResponse { + var resp agentsdk.ExternalAuthResponse switch typ { - case codersdk.EnhancedExternalAuthProviderGitLab: + case string(codersdk.EnhancedExternalAuthProviderGitLab): // https://stackoverflow.com/questions/25409700/using-gitlab-token-to-clone-without-authentication - resp = agentsdk.GitAuthResponse{ + resp = agentsdk.ExternalAuthResponse{ Username: "oauth2", Password: token, } - case codersdk.EnhancedExternalAuthProviderBitBucket: + case string(codersdk.EnhancedExternalAuthProviderBitBucket): // https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token - resp = agentsdk.GitAuthResponse{ + resp = agentsdk.ExternalAuthResponse{ Username: "x-token-auth", Password: token, } default: - resp = agentsdk.GitAuthResponse{ + resp = agentsdk.ExternalAuthResponse{ Username: token, } } + resp.AccessToken = token + resp.Type = typ return resp } diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index c0750b1a22999..2236bc8669b83 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -710,30 +710,51 @@ func (c *Client) GetServiceBanner(ctx context.Context) (codersdk.ServiceBannerCo return cfg.ServiceBanner, json.NewDecoder(res.Body).Decode(&cfg) } -type GitAuthResponse struct { +type ExternalAuthResponse struct { + AccessToken string `json:"access_token"` + URL string `json:"url"` + Type string `json:"type"` + + // Deprecated: Only supported on `/workspaceagents/me/gitauth` + // for backwards compatibility. Username string `json:"username"` Password string `json:"password"` - URL string `json:"url"` } -// GitAuth submits a URL to fetch a GIT_ASKPASS username and password for. +// ExternalAuthRequest is used to request an access token for a provider. +// Either ID or Match must be specified, but not both. +type ExternalAuthRequest struct { + // ID is the ID of a provider to request authentication for. + ID string + // Match is an arbitrary string matched against the regex of the provider. + Match string + // Listen indicates that the request should be long-lived and listen for + // a new token to be requested. + Listen bool +} + +// ExternalAuth submits a URL or provider ID to fetch an access token for. // nolint:revive -func (c *Client) GitAuth(ctx context.Context, gitURL string, listen bool) (GitAuthResponse, error) { - reqURL := "/api/v2/workspaceagents/me/gitauth?url=" + url.QueryEscape(gitURL) - if listen { - reqURL += "&listen" +func (c *Client) ExternalAuth(ctx context.Context, req ExternalAuthRequest) (ExternalAuthResponse, error) { + q := url.Values{ + "id": []string{req.ID}, + "match": []string{req.Match}, + } + if req.Listen { + q.Set("listen", "true") } + reqURL := "/api/v2/workspaceagents/me/external-auth?" + q.Encode() res, err := c.SDK.Request(ctx, http.MethodGet, reqURL, nil) if err != nil { - return GitAuthResponse{}, xerrors.Errorf("execute request: %w", err) + return ExternalAuthResponse{}, xerrors.Errorf("execute request: %w", err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return GitAuthResponse{}, codersdk.ReadBodyAsError(res) + return ExternalAuthResponse{}, codersdk.ReadBodyAsError(res) } - var authResp GitAuthResponse + var authResp ExternalAuthResponse return authResp, json.NewDecoder(res.Body).Decode(&authResp) } diff --git a/docs/api/agents.md b/docs/api/agents.md index 326f523415f41..c1fc5edb37fa7 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -221,24 +221,25 @@ incoming connections and publishes node updates. To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Get workspace agent Git auth +## Get workspace agent external auth ### Code samples ```shell # Example request using curl -curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitauth?url=http%3A%2F%2Fexample.com \ +curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/external-auth?url=http%3A%2F%2Fexample.com&id=string \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` -`GET /workspaceagents/me/gitauth` +`GET /workspaceagents/me/external-auth` ### Parameters | Name | In | Type | Required | Description | | -------- | ----- | ----------- | -------- | --------------------------------- | -| `url` | query | string(uri) | true | Git URL | +| `url` | query | string(uri) | true | Matching URL | +| `id` | query | string | true | Provider ID | | `listen` | query | boolean | false | Wait for a new token to be issued | ### Example responses @@ -247,7 +248,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitauth?url=http% ```json { + "access_token": "string", "password": "string", + "type": "string", "url": "string", "username": "string" } @@ -255,9 +258,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitauth?url=http% ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.GitAuthResponse](schemas.md#agentsdkgitauthresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.ExternalAuthResponse](schemas.md#agentsdkexternalauthresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 7413bf4b4815e..f77f8542b33c7 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -109,11 +109,13 @@ | `encoding` | string | true | | | | `signature` | string | true | | | -## agentsdk.GitAuthResponse +## agentsdk.ExternalAuthResponse ```json { + "access_token": "string", "password": "string", + "type": "string", "url": "string", "username": "string" } @@ -121,11 +123,13 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -| ---------- | ------ | -------- | ------------ | ----------- | -| `password` | string | false | | | -| `url` | string | false | | | -| `username` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| -------------- | ------ | -------- | ------------ | ---------------------------------------------------------------------------------------- | +| `access_token` | string | false | | | +| `password` | string | false | | | +| `type` | string | false | | | +| `url` | string | false | | | +| `username` | string | false | | Deprecated: Only supported on `/workspaceagents/me/gitauth` for backwards compatibility. | ## agentsdk.GitSSHKey diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 36d494711d0fd..77fb7e6f906e0 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -661,28 +661,29 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error } // A map is used to ensure we don't have duplicates! - gitAuthProvidersMap := map[string]struct{}{} + externalAuthProvidersMap := map[string]struct{}{} for _, tfResources := range tfResourcesByLabel { for _, resource := range tfResources { - if resource.Type != "coder_git_auth" { + // Checking for `coder_git_auth` is legacy! + if resource.Type != "coder_external_auth" && resource.Type != "coder_git_auth" { continue } id, ok := resource.AttributeValues["id"].(string) if !ok { - return nil, xerrors.Errorf("git auth id is not a string") + return nil, xerrors.Errorf("external auth id is not a string") } - gitAuthProvidersMap[id] = struct{}{} + externalAuthProvidersMap[id] = struct{}{} } } - gitAuthProviders := make([]string, 0, len(gitAuthProvidersMap)) - for id := range gitAuthProvidersMap { - gitAuthProviders = append(gitAuthProviders, id) + externalAuthProviders := make([]string, 0, len(externalAuthProvidersMap)) + for id := range externalAuthProvidersMap { + externalAuthProviders = append(externalAuthProviders, id) } return &State{ Resources: resources, Parameters: parameters, - ExternalAuthProviders: gitAuthProviders, + ExternalAuthProviders: externalAuthProviders, }, nil } diff --git a/site/src/components/DeploySettingsLayout/Sidebar.tsx b/site/src/components/DeploySettingsLayout/Sidebar.tsx index b2af2612373a6..1228bb0fb0dfc 100644 --- a/site/src/components/DeploySettingsLayout/Sidebar.tsx +++ b/site/src/components/DeploySettingsLayout/Sidebar.tsx @@ -111,10 +111,10 @@ export const Sidebar: React.FC = () => { User Authentication } > - Git Authentication + External Authentication }> Network From 1f50431a24cee45705a4891e4461de28146f6724 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 4 Oct 2023 18:23:51 +0000 Subject: [PATCH 2/9] Add subcommands --- cli/externalauth.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/cli/externalauth.go b/cli/externalauth.go index b6921dbdf6093..4bd4a58777d8c 100644 --- a/cli/externalauth.go +++ b/cli/externalauth.go @@ -3,5 +3,33 @@ package cli import "github.com/coder/coder/v2/cli/clibase" func (r *RootCmd) externalAuth() *clibase.Cmd { - return &clibase.Cmd{} + return &clibase.Cmd{ + Use: "external-auth", + Short: "Manage external authentication", + Long: "Authenticate with external services inside of a workspace.", + Handler: func(i *clibase.Invocation) error { + return i.Command.HelpHandler(i) + }, + } +} + +func (r *RootCmd) externalAuthList() *clibase.Cmd { + return &clibase.Cmd{ + Use: "list", + Short: "List external authentication providers", + Long: "List external authentication.", + Handler: func(i *clibase.Invocation) error { + return i.Command.HelpHandler(i) + }, + } +} + +func (r *RootCmd) externalAuthAccessToken() *clibase.Cmd { + return &clibase.Cmd{ + Use: "access-token", + Short: "Print auth for an external provider", + Long: "Print an access-token for an external auth provider. " + + "If the user has a refresh-token, the access-token automatically refresh the access-token. " + + "If the user has not authenticated before, ", + } } From df8ac5d74b1a0f5088793194e63945232009bb94 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 4 Oct 2023 19:42:39 +0000 Subject: [PATCH 3/9] Improve descriptions --- cli/externalauth.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/cli/externalauth.go b/cli/externalauth.go index 4bd4a58777d8c..1fe3bab823941 100644 --- a/cli/externalauth.go +++ b/cli/externalauth.go @@ -29,7 +29,29 @@ func (r *RootCmd) externalAuthAccessToken() *clibase.Cmd { Use: "access-token", Short: "Print auth for an external provider", Long: "Print an access-token for an external auth provider. " + - "If the user has a refresh-token, the access-token automatically refresh the access-token. " + - "If the user has not authenticated before, ", + "The access-token will be validated and sent to stdout with exit code 0. " + + "If a valid access-token cannot be obtained, the URL to authenticate will be sent to stderr with exit code 1\n" + formatExamples( + example{ + Description: "Ensure that the user is authenticated with GitHub before cloning.", + Command: `#!/usr/bin/env sh + +if coder external-auth access-token github ; then + echo "Authenticated with GitHub" +else + echo "Please authenticate with GitHub:" + coder external-auth url github +fi +`, + }, + ), + Options: clibase.OptionSet{{ + Name: "Silent", + Flag: "s", + Description: "Do not print the URL or access token.", + }}, + + Handler: func(i *clibase.Invocation) error { + return nil + }, } } From 64f22416f0fd99de8192ed882e226792403485ed Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Oct 2023 19:33:45 +0000 Subject: [PATCH 4/9] Add external-auth subcommand --- cli/externalauth.go | 61 ++++++++++++++++++++------ cli/externalauth_test.go | 49 +++++++++++++++++++++ cli/root.go | 1 + coderd/apidoc/docs.go | 5 +-- coderd/apidoc/swagger.json | 5 +-- docs/api/agents.md | 12 ++--- docs/cli.md | 1 + docs/cli/external-auth.md | 23 ++++++++++ docs/cli/external-auth_access-token.md | 38 ++++++++++++++++ docs/manifest.json | 10 +++++ 10 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 cli/externalauth_test.go create mode 100644 docs/cli/external-auth.md create mode 100644 docs/cli/external-auth_access-token.md diff --git a/cli/externalauth.go b/cli/externalauth.go index 1fe3bab823941..bd82bb584ed38 100644 --- a/cli/externalauth.go +++ b/cli/externalauth.go @@ -1,6 +1,14 @@ package cli -import "github.com/coder/coder/v2/cli/clibase" +import ( + "os/signal" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/cli/clibase" + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/codersdk/agentsdk" +) func (r *RootCmd) externalAuth() *clibase.Cmd { return &clibase.Cmd{ @@ -10,27 +18,20 @@ func (r *RootCmd) externalAuth() *clibase.Cmd { Handler: func(i *clibase.Invocation) error { return i.Command.HelpHandler(i) }, - } -} - -func (r *RootCmd) externalAuthList() *clibase.Cmd { - return &clibase.Cmd{ - Use: "list", - Short: "List external authentication providers", - Long: "List external authentication.", - Handler: func(i *clibase.Invocation) error { - return i.Command.HelpHandler(i) + Children: []*clibase.Cmd{ + r.externalAuthAccessToken(), }, } } func (r *RootCmd) externalAuthAccessToken() *clibase.Cmd { + var silent bool return &clibase.Cmd{ - Use: "access-token", + Use: "access-token ", Short: "Print auth for an external provider", Long: "Print an access-token for an external auth provider. " + "The access-token will be validated and sent to stdout with exit code 0. " + - "If a valid access-token cannot be obtained, the URL to authenticate will be sent to stderr with exit code 1\n" + formatExamples( + "If a valid access-token cannot be obtained, the URL to authenticate will be sent to stdout with exit code 1\n" + formatExamples( example{ Description: "Ensure that the user is authenticated with GitHub before cloning.", Command: `#!/usr/bin/env sh @@ -48,9 +49,41 @@ fi Name: "Silent", Flag: "s", Description: "Do not print the URL or access token.", + Value: clibase.BoolOf(&silent), }}, - Handler: func(i *clibase.Invocation) error { + Handler: func(inv *clibase.Invocation) error { + ctx := inv.Context() + + ctx, stop := signal.NotifyContext(ctx, InterruptSignals...) + defer stop() + + client, err := r.createAgentClient() + if err != nil { + return xerrors.Errorf("create agent client: %w", err) + } + + token, err := client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{ + ID: inv.Args[0], + }) + if err != nil { + return xerrors.Errorf("get external auth token: %w", err) + } + + if !silent { + if token.URL != "" { + _, err = inv.Stdout.Write([]byte(token.URL)) + } else { + _, err = inv.Stdout.Write([]byte(token.AccessToken)) + } + if err != nil { + return err + } + } + + if token.URL != "" { + return cliui.Canceled + } return nil }, } diff --git a/cli/externalauth_test.go b/cli/externalauth_test.go new file mode 100644 index 0000000000000..3af933e888921 --- /dev/null +++ b/cli/externalauth_test.go @@ -0,0 +1,49 @@ +package cli_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/pty/ptytest" +) + +func TestExternalAuth(t *testing.T) { + t.Parallel() + t.Run("CanceledWithURL", func(t *testing.T) { + t.Parallel() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{ + URL: "https://github.com", + }) + })) + t.Cleanup(srv.Close) + url := srv.URL + inv, _ := clitest.New(t, "--agent-url", url, "external-auth", "access-token", "github") + pty := ptytest.New(t) + inv.Stdout = pty.Output() + waiter := clitest.StartWithWaiter(t, inv) + pty.ExpectMatch("https://github.com") + waiter.RequireIs(cliui.Canceled) + }) + t.Run("SuccessWithToken", func(t *testing.T) { + t.Parallel() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{ + AccessToken: "bananas", + }) + })) + t.Cleanup(srv.Close) + url := srv.URL + inv, _ := clitest.New(t, "--agent-url", url, "external-auth", "access-token", "github") + pty := ptytest.New(t) + inv.Stdout = pty.Output() + clitest.Start(t, inv) + pty.ExpectMatch("bananas") + }) +} diff --git a/cli/root.go b/cli/root.go index d75187d3f4133..d2eea4b3785c1 100644 --- a/cli/root.go +++ b/cli/root.go @@ -83,6 +83,7 @@ func (r *RootCmd) Core() []*clibase.Cmd { // Please re-sort this list alphabetically if you change it! return []*clibase.Cmd{ r.dotfiles(), + r.externalAuth(), r.login(), r.logout(), r.netcheck(), diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 3008a216a513e..c49dd8aa8f06f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4579,9 +4579,8 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "format": "uri", - "description": "Matching URL", - "name": "url", + "description": "Match", + "name": "match", "in": "query", "required": true }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7017b8259f26b..0de433387ffb5 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4025,9 +4025,8 @@ "parameters": [ { "type": "string", - "format": "uri", - "description": "Matching URL", - "name": "url", + "description": "Match", + "name": "match", "in": "query", "required": true }, diff --git a/docs/api/agents.md b/docs/api/agents.md index c1fc5edb37fa7..7da237ab5892b 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -227,7 +227,7 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio ```shell # Example request using curl -curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/external-auth?url=http%3A%2F%2Fexample.com&id=string \ +curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/external-auth?match=string&id=string \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` @@ -236,11 +236,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/external-auth?url ### Parameters -| Name | In | Type | Required | Description | -| -------- | ----- | ----------- | -------- | --------------------------------- | -| `url` | query | string(uri) | true | Matching URL | -| `id` | query | string | true | Provider ID | -| `listen` | query | boolean | false | Wait for a new token to be issued | +| Name | In | Type | Required | Description | +| -------- | ----- | ------- | -------- | --------------------------------- | +| `match` | query | string | true | Match | +| `id` | query | string | true | Provider ID | +| `listen` | query | boolean | false | Wait for a new token to be issued | ### Example responses diff --git a/docs/cli.md b/docs/cli.md index a63ccad623a6f..57ce052fa443d 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -29,6 +29,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr | [create](./cli/create.md) | Create a workspace | | [delete](./cli/delete.md) | Delete a workspace | | [dotfiles](./cli/dotfiles.md) | Personalize your workspace by applying a canonical dotfiles repository | +| [external-auth](./cli/external-auth.md) | Manage external authentication | | [features](./cli/features.md) | List Enterprise features | | [groups](./cli/groups.md) | Manage groups | | [licenses](./cli/licenses.md) | Add, delete, and list licenses | diff --git a/docs/cli/external-auth.md b/docs/cli/external-auth.md new file mode 100644 index 0000000000000..ebe16435feb62 --- /dev/null +++ b/docs/cli/external-auth.md @@ -0,0 +1,23 @@ + + +# external-auth + +Manage external authentication + +## Usage + +```console +coder external-auth +``` + +## Description + +```console +Authenticate with external services inside of a workspace. +``` + +## Subcommands + +| Name | Purpose | +| ------------------------------------------------------------ | ----------------------------------- | +| [access-token](./external-auth_access-token.md) | Print auth for an external provider | diff --git a/docs/cli/external-auth_access-token.md b/docs/cli/external-auth_access-token.md new file mode 100644 index 0000000000000..653eece1d8eef --- /dev/null +++ b/docs/cli/external-auth_access-token.md @@ -0,0 +1,38 @@ + + +# external-auth access-token + +Print auth for an external provider + +## Usage + +```console +coder external-auth access-token [flags] +``` + +## Description + +```console +Print an access-token for an external auth provider. The access-token will be validated and sent to stdout with exit code 0. If a valid access-token cannot be obtained, the URL to authenticate will be sent to stdout with exit code 1 + - Ensure that the user is authenticated with GitHub before cloning.: + + $ #!/usr/bin/env sh + +if coder external-auth access-token github ; then + echo "Authenticated with GitHub" +else + echo "Please authenticate with GitHub:" + coder external-auth url github +fi + +``` + +## Options + +### --s + +| | | +| ---- | ----------------- | +| Type | bool | + +Do not print the URL or access token. diff --git a/docs/manifest.json b/docs/manifest.json index e6541f5634250..3cb049b249708 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -557,6 +557,16 @@ "description": "Personalize your workspace by applying a canonical dotfiles repository", "path": "cli/dotfiles.md" }, + { + "title": "external-auth", + "description": "Manage external authentication", + "path": "cli/external-auth.md" + }, + { + "title": "external-auth access-token", + "description": "Print auth for an external provider", + "path": "cli/external-auth_access-token.md" + }, { "title": "features", "description": "List Enterprise features", From 471e62f77f5a8c3d33dca1cc7256716aa25e07e5 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Oct 2023 19:45:17 +0000 Subject: [PATCH 5/9] Fix docs --- coderd/apidoc/docs.go | 47 ++++++++++++++++++++++++++++++++++++++ coderd/apidoc/swagger.json | 43 ++++++++++++++++++++++++++++++++++ coderd/coderd.go | 2 +- coderd/deprecated.go | 14 ++++++++++++ docs/api/agents.md | 43 ++++++++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 1 deletion(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c49dd8aa8f06f..04edd7ef96e0c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4608,6 +4608,53 @@ const docTemplate = `{ } } }, + "/workspaceagents/me/gitauth": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Agents" + ], + "summary": "Removed: Get workspace agent git auth", + "operationId": "removed-get-workspace-agent-git-auth", + "parameters": [ + { + "type": "string", + "description": "Match", + "name": "match", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Wait for a new token to be issued", + "name": "listen", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/agentsdk.ExternalAuthResponse" + } + } + } + } + }, "/workspaceagents/me/gitsshkey": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0de433387ffb5..4c303da5ace28 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4054,6 +4054,49 @@ } } }, + "/workspaceagents/me/gitauth": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Agents"], + "summary": "Removed: Get workspace agent git auth", + "operationId": "removed-get-workspace-agent-git-auth", + "parameters": [ + { + "type": "string", + "description": "Match", + "name": "match", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Wait for a new token to be issued", + "name": "listen", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/agentsdk.ExternalAuthResponse" + } + } + } + } + }, "/workspaceagents/me/gitsshkey": { "get": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index 879f63a24abaa..cabf63c34bd98 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -813,7 +813,7 @@ func New(options *Options) *API { r.Patch("/logs", api.patchWorkspaceAgentLogs) r.Post("/app-health", api.postWorkspaceAppHealth) // Deprecated: Required to support legacy agents - r.Get("/gitauth", api.workspaceAgentsExternalAuth) + r.Get("/gitauth", api.workspaceAgentsGitAuth) r.Get("/external-auth", api.workspaceAgentsExternalAuth) r.Get("/gitsshkey", api.agentGitSSHKey) r.Get("/coordinate", api.workspaceAgentCoordinate) diff --git a/coderd/deprecated.go b/coderd/deprecated.go index f656451a83edd..0b7b0b14a2762 100644 --- a/coderd/deprecated.go +++ b/coderd/deprecated.go @@ -56,3 +56,17 @@ func (api *API) patchWorkspaceAgentLogsDeprecated(rw http.ResponseWriter, r *htt func (api *API) workspaceAgentLogsDeprecated(rw http.ResponseWriter, r *http.Request) { api.workspaceAgentLogs(rw, r) } + +// @Summary Removed: Get workspace agent git auth +// @ID removed-get-workspace-agent-git-auth +// @Security CoderSessionToken +// @Produce json +// @Tags Agents +// @Param match query string true "Match" +// @Param id query string true "Provider ID" +// @Param listen query bool false "Wait for a new token to be issued" +// @Success 200 {object} agentsdk.ExternalAuthResponse +// @Router /workspaceagents/me/gitauth [get] +func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) { + api.workspaceAgentsExternalAuth(rw, r) +} diff --git a/docs/api/agents.md b/docs/api/agents.md index 7da237ab5892b..99d4509c4b6ba 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -264,6 +264,49 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/external-auth?mat To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Removed: Get workspace agent git auth + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitauth?match=string&id=string \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /workspaceagents/me/gitauth` + +### Parameters + +| Name | In | Type | Required | Description | +| -------- | ----- | ------- | -------- | --------------------------------- | +| `match` | query | string | true | Match | +| `id` | query | string | true | Provider ID | +| `listen` | query | boolean | false | Wait for a new token to be issued | + +### Example responses + +> 200 Response + +```json +{ + "access_token": "string", + "password": "string", + "type": "string", + "url": "string", + "username": "string" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.ExternalAuthResponse](schemas.md#agentsdkexternalauthresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get workspace agent Git SSH key ### Code samples From 51519a8be6996673a6d45760f570cb4e4f4fcd48 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Oct 2023 19:51:56 +0000 Subject: [PATCH 6/9] Fix gen --- cli/testdata/coder_--help.golden | 1 + .../coder_external-auth_--help.golden | 14 ++ ...r_external-auth_access-token_--help.golden | 27 +++ coderd/database/queries.sql.go | 226 +++++++++--------- 4 files changed, 155 insertions(+), 113 deletions(-) create mode 100644 cli/testdata/coder_external-auth_--help.golden create mode 100644 cli/testdata/coder_external-auth_access-token_--help.golden diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index d44c487bfdf17..d04546ce01959 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -20,6 +20,7 @@ SUBCOMMANDS: delete Delete a workspace dotfiles Personalize your workspace by applying a canonical dotfiles repository + external-auth Manage external authentication list List workspaces login Authenticate with Coder deployment logout Unauthenticate your local session diff --git a/cli/testdata/coder_external-auth_--help.golden b/cli/testdata/coder_external-auth_--help.golden new file mode 100644 index 0000000000000..42b465068d9c8 --- /dev/null +++ b/cli/testdata/coder_external-auth_--help.golden @@ -0,0 +1,14 @@ +coder v0.0.0-devel + +USAGE: + coder external-auth + + Manage external authentication + + Authenticate with external services inside of a workspace. + +SUBCOMMANDS: + access-token Print auth for an external provider + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_external-auth_access-token_--help.golden b/cli/testdata/coder_external-auth_access-token_--help.golden new file mode 100644 index 0000000000000..61ed8528e53ab --- /dev/null +++ b/cli/testdata/coder_external-auth_access-token_--help.golden @@ -0,0 +1,27 @@ +coder v0.0.0-devel + +USAGE: + coder external-auth access-token [flags] + + Print auth for an external provider + + Print an access-token for an external auth provider. The access-token will be + validated and sent to stdout with exit code 0. If a valid access-token cannot + be obtained, the URL to authenticate will be sent to stdout with exit code 1 + - Ensure that the user is authenticated with GitHub before cloning.: + + $ #!/usr/bin/env sh + + if coder external-auth access-token github ; then + echo "Authenticated with GitHub" + else + echo "Please authenticate with GitHub:" + coder external-auth url github + fi + +OPTIONS: + --s bool + Do not print the URL or access token. + +——— +Run `coder --help` for a list of global options. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 47474782633dc..2e165b006778d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -9561,119 +9561,6 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } -const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many -SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) -` - -func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many -INSERT INTO - workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) -SELECT - $1 :: uuid AS workspace_agent_id, - $2 :: timestamptz AS created_at, - unnest($3 :: uuid [ ]) AS log_source_id, - unnest($4 :: text [ ]) AS log_path, - unnest($5 :: text [ ]) AS script, - unnest($6 :: text [ ]) AS cron, - unnest($7 :: boolean [ ]) AS start_blocks_login, - unnest($8 :: boolean [ ]) AS run_on_start, - unnest($9 :: boolean [ ]) AS run_on_stop, - unnest($10 :: integer [ ]) AS timeout_seconds -RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds -` - -type InsertWorkspaceAgentScriptsParams struct { - WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` - LogPath []string `db:"log_path" json:"log_path"` - Script []string `db:"script" json:"script"` - Cron []string `db:"cron" json:"cron"` - StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` - RunOnStart []bool `db:"run_on_start" json:"run_on_start"` - RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` - TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` -} - -func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, - arg.WorkspaceAgentID, - arg.CreatedAt, - pq.Array(arg.LogSourceID), - pq.Array(arg.LogPath), - pq.Array(arg.Script), - pq.Array(arg.Cron), - pq.Array(arg.StartBlocksLogin), - pq.Array(arg.RunOnStart), - pq.Array(arg.RunOnStop), - pq.Array(arg.TimeoutSeconds), - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT @@ -10634,3 +10521,116 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID) return err } + +const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many +SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many +INSERT INTO + workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) +SELECT + $1 :: uuid AS workspace_agent_id, + $2 :: timestamptz AS created_at, + unnest($3 :: uuid [ ]) AS log_source_id, + unnest($4 :: text [ ]) AS log_path, + unnest($5 :: text [ ]) AS script, + unnest($6 :: text [ ]) AS cron, + unnest($7 :: boolean [ ]) AS start_blocks_login, + unnest($8 :: boolean [ ]) AS run_on_start, + unnest($9 :: boolean [ ]) AS run_on_stop, + unnest($10 :: integer [ ]) AS timeout_seconds +RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds +` + +type InsertWorkspaceAgentScriptsParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` + LogPath []string `db:"log_path" json:"log_path"` + Script []string `db:"script" json:"script"` + Cron []string `db:"cron" json:"cron"` + StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` + RunOnStart []bool `db:"run_on_start" json:"run_on_start"` + RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` + TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, + arg.WorkspaceAgentID, + arg.CreatedAt, + pq.Array(arg.LogSourceID), + pq.Array(arg.LogPath), + pq.Array(arg.Script), + pq.Array(arg.Cron), + pq.Array(arg.StartBlocksLogin), + pq.Array(arg.RunOnStart), + pq.Array(arg.RunOnStop), + pq.Array(arg.TimeoutSeconds), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} From 1f473686a20c4fe7d612a23411b72855ebe793fa Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Oct 2023 22:15:05 +0000 Subject: [PATCH 7/9] Fix comment --- cli/externalauth.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/externalauth.go b/cli/externalauth.go index bd82bb584ed38..5d2cdfa82b46e 100644 --- a/cli/externalauth.go +++ b/cli/externalauth.go @@ -36,11 +36,12 @@ func (r *RootCmd) externalAuthAccessToken() *clibase.Cmd { Description: "Ensure that the user is authenticated with GitHub before cloning.", Command: `#!/usr/bin/env sh -if coder external-auth access-token github ; then +OUTPUT=$(coder external-auth access-token github) +if [ $? -eq 0 ]; then echo "Authenticated with GitHub" else echo "Please authenticate with GitHub:" - coder external-auth url github + echo $OUTPUT fi `, }, From 15377887e4f26ffd51680af523bf97a4888270e1 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Oct 2023 22:15:52 +0000 Subject: [PATCH 8/9] Fix golden file --- cli/testdata/coder_external-auth_access-token_--help.golden | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/testdata/coder_external-auth_access-token_--help.golden b/cli/testdata/coder_external-auth_access-token_--help.golden index 61ed8528e53ab..9a623a042cc84 100644 --- a/cli/testdata/coder_external-auth_access-token_--help.golden +++ b/cli/testdata/coder_external-auth_access-token_--help.golden @@ -12,11 +12,12 @@ USAGE: $ #!/usr/bin/env sh - if coder external-auth access-token github ; then + OUTPUT=$(coder external-auth access-token github) + if [ $? -eq 0 ]; then echo "Authenticated with GitHub" else echo "Please authenticate with GitHub:" - coder external-auth url github + echo $OUTPUT fi OPTIONS: From b60f208c1fe07abd219c8f9ceb27dc4f1ac1f0ee Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Oct 2023 22:34:03 +0000 Subject: [PATCH 9/9] Fix docgen --- docs/cli/external-auth_access-token.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/cli/external-auth_access-token.md b/docs/cli/external-auth_access-token.md index 653eece1d8eef..1ca1b32fe9a72 100644 --- a/docs/cli/external-auth_access-token.md +++ b/docs/cli/external-auth_access-token.md @@ -18,11 +18,12 @@ Print an access-token for an external auth provider. The access-token will be va $ #!/usr/bin/env sh -if coder external-auth access-token github ; then +OUTPUT=$(coder external-auth access-token github) +if [ $? -eq 0 ]; then echo "Authenticated with GitHub" else echo "Please authenticate with GitHub:" - coder external-auth url github + echo $OUTPUT fi ```