From c8c39ecd8dda2ddc003966fd78fe1473b84ead0d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 29 Nov 2023 11:51:42 -0600 Subject: [PATCH 1/2] feat: add endpoints to list all authed external apps Also add support for unlinking on the coder side to allow reflow. --- coderd/apidoc/docs.go | 75 ++++++++++++++++++++ coderd/apidoc/swagger.json | 69 ++++++++++++++++++ coderd/coderd.go | 17 +++-- coderd/database/db2sdk/db2sdk.go | 18 +++++ coderd/database/dbauthz/dbauthz.go | 12 ++-- coderd/database/dbmem/dbmem.go | 23 ++++++ coderd/database/dbmetrics/dbmetrics.go | 7 ++ coderd/database/dbmock/dbmock.go | 14 ++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 14 ++++ coderd/database/queries/externalauth.sql | 3 + coderd/externalauth.go | 89 ++++++++++++++++++++++++ coderd/externalauth_test.go | 55 +++++++++++++++ codersdk/externalauth.go | 60 ++++++++++++++++ docs/api/default.md | 27 +++++++ docs/api/schemas.md | 22 ++++++ docs/api/users.md | 35 ++++++++++ docs/manifest.json | 4 ++ site/src/api/typesGenerated.ts | 26 +++++++ 19 files changed, 562 insertions(+), 9 deletions(-) create mode 100644 docs/api/default.md diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c88eb6fb2ba21..c29917db3ca90 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -781,6 +781,33 @@ const docTemplate = `{ } } } + }, + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "summary": "Delete external auth user link by ID", + "operationId": "delete-external-auth-user-link-by-id", + "parameters": [ + { + "type": "string", + "format": "string", + "description": "Git Provider ID", + "name": "externalauth", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } } }, "/external-auth/{externalauth}/device": { @@ -3437,6 +3464,31 @@ const docTemplate = `{ } } }, + "/users/external-auths": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get user external auths", + "operationId": "get-user-external-auths", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ExternalAuthLink" + } + } + } + } + }, "/users/first": { "get": { "security": [ @@ -8845,6 +8897,29 @@ const docTemplate = `{ } } }, + "codersdk.ExternalAuthLink": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "expires": { + "type": "string", + "format": "date-time" + }, + "has_refresh_token": { + "type": "boolean" + }, + "provider_id": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, "codersdk.ExternalAuthUser": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ed7fa177e6f76..d65b55b0df129 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -667,6 +667,31 @@ } } } + }, + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "summary": "Delete external auth user link by ID", + "operationId": "delete-external-auth-user-link-by-id", + "parameters": [ + { + "type": "string", + "format": "string", + "description": "Git Provider ID", + "name": "externalauth", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } } }, "/external-auth/{externalauth}/device": { @@ -3023,6 +3048,27 @@ } } }, + "/users/external-auths": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Get user external auths", + "operationId": "get-user-external-auths", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ExternalAuthLink" + } + } + } + } + }, "/users/first": { "get": { "security": [ @@ -7937,6 +7983,29 @@ } } }, + "codersdk.ExternalAuthLink": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "expires": { + "type": "string", + "format": "date-time" + }, + "has_refresh_token": { + "type": "boolean" + }, + "provider_id": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, "codersdk.ExternalAuthUser": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 08784b6ff4e43..5b10a2e09e969 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -660,14 +660,21 @@ func New(options *Options) *API { r.Get("/{fileID}", api.fileByID) r.Post("/", api.postFile) }) - r.Route("/external-auth/{externalauth}", func(r chi.Router) { + r.Route("/external-auth", func(r chi.Router) { r.Use( apiKeyMiddleware, - httpmw.ExtractExternalAuthParam(options.ExternalAuthConfigs), ) - r.Get("/", api.externalAuthByID) - r.Post("/device", api.postExternalAuthDeviceByID) - r.Get("/device", api.externalAuthDeviceByID) + // Get without a specific external auth ID will return all external auths. + r.Get("/", api.userExternalAuths) + r.Route("/{externalauth}", func(r chi.Router) { + r.Use( + httpmw.ExtractExternalAuthParam(options.ExternalAuthConfigs), + ) + r.Delete("/", api.deleteExternalAuthByID) + r.Get("/", api.externalAuthByID) + r.Post("/device", api.postExternalAuthDeviceByID) + r.Get("/device", api.externalAuthDeviceByID) + }) }) r.Route("/organizations", func(r chi.Router) { r.Use( diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 6add6d0146796..f1104f7b1213a 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -16,6 +16,24 @@ import ( "github.com/coder/coder/v2/provisionersdk/proto" ) +func ExternalAuths(auths []database.ExternalAuthLink) []codersdk.ExternalAuthLink { + out := make([]codersdk.ExternalAuthLink, 0, len(auths)) + for _, auth := range auths { + out = append(out, ExternalAuth(auth)) + } + return out +} + +func ExternalAuth(auth database.ExternalAuthLink) codersdk.ExternalAuthLink { + return codersdk.ExternalAuthLink{ + ProviderID: auth.ProviderID, + CreatedAt: auth.CreatedAt, + UpdatedAt: auth.UpdatedAt, + HasRefreshToken: auth.OAuthRefreshToken != "", + Expires: auth.OAuthExpiry, + } +} + func WorkspaceBuildParameters(params []database.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter { out := make([]codersdk.WorkspaceBuildParameter, len(params)) for i, p := range params { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 176c92ca19c78..44af9b75c0ca2 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -740,6 +740,13 @@ func (q *querier) DeleteCoordinator(ctx context.Context, id uuid.UUID) error { return q.db.DeleteCoordinator(ctx, id) } +func (q *querier) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error { + return deleteQ(q.log, q.auth, func(ctx context.Context, arg database.DeleteExternalAuthLinkParams) (database.ExternalAuthLink, error) { + //nolint:gosimple + return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID}) + }, q.db.DeleteExternalAuthLink)(ctx, arg) +} + func (q *querier) DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error { return deleteQ(q.log, q.auth, q.db.GetGitSSHKey, q.db.DeleteGitSSHKey)(ctx, userID) } @@ -975,10 +982,7 @@ func (q *querier) GetExternalAuthLink(ctx context.Context, arg database.GetExter } func (q *querier) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]database.ExternalAuthLink, error) { - if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil { - return nil, err - } - return q.db.GetExternalAuthLinksByUserID(ctx, userID) + return fetchWithPostFilter(q.auth, q.db.GetExternalAuthLinksByUserID)(ctx, userID) } func (q *querier) GetFileByHashAndCreator(ctx context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 3b78d67ebeba4..d6b248e1869d0 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1019,6 +1019,29 @@ func (*FakeQuerier) DeleteCoordinator(context.Context, uuid.UUID) error { return ErrUnimplemented } +func (q *FakeQuerier) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, key := range q.externalAuthLinks { + if key.UserID != arg.UserID { + continue + } + if key.ProviderID != arg.ProviderID { + continue + } + q.externalAuthLinks[index] = q.externalAuthLinks[len(q.externalAuthLinks)-1] + q.externalAuthLinks = q.externalAuthLinks[:len(q.externalAuthLinks)-1] + return nil + } + return sql.ErrNoRows +} + func (q *FakeQuerier) DeleteGitSSHKey(_ context.Context, userID uuid.UUID) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index e37153463a24a..46e6e93c602c8 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -162,6 +162,13 @@ func (m metricsStore) DeleteCoordinator(ctx context.Context, id uuid.UUID) error return m.s.DeleteCoordinator(ctx, id) } +func (m metricsStore) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error { + start := time.Now() + r0 := m.s.DeleteExternalAuthLink(ctx, arg) + m.queryLatencies.WithLabelValues("DeleteExternalAuthLink").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error { start := time.Now() err := m.s.DeleteGitSSHKey(ctx, userID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index bcc03a77ddd1a..2c356e1ec145a 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -210,6 +210,20 @@ func (mr *MockStoreMockRecorder) DeleteCoordinator(arg0, arg1 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCoordinator", reflect.TypeOf((*MockStore)(nil).DeleteCoordinator), arg0, arg1) } +// DeleteExternalAuthLink mocks base method. +func (m *MockStore) DeleteExternalAuthLink(arg0 context.Context, arg1 database.DeleteExternalAuthLinkParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteExternalAuthLink", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteExternalAuthLink indicates an expected call of DeleteExternalAuthLink. +func (mr *MockStoreMockRecorder) DeleteExternalAuthLink(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExternalAuthLink", reflect.TypeOf((*MockStore)(nil).DeleteExternalAuthLink), arg0, arg1) +} + // DeleteGitSSHKey mocks base method. func (m *MockStore) DeleteGitSSHKey(arg0 context.Context, arg1 uuid.UUID) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d1b3e070d8b13..ec4403cabb36b 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -49,6 +49,7 @@ type sqlcQuerier interface { DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) error DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error DeleteCoordinator(ctx context.Context, id uuid.UUID) error + DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error DeleteGroupByID(ctx context.Context, id uuid.UUID) error DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteGroupMemberFromGroupParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3604b7a58d0b1..851cfc949b21b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -788,6 +788,20 @@ func (q *sqlQuerier) RevokeDBCryptKey(ctx context.Context, activeKeyDigest strin return err } +const deleteExternalAuthLink = `-- name: DeleteExternalAuthLink :exec +DELETE FROM external_auth_links WHERE provider_id = $1 AND user_id = $2 +` + +type DeleteExternalAuthLinkParams struct { + ProviderID string `db:"provider_id" json:"provider_id"` + UserID uuid.UUID `db:"user_id" json:"user_id"` +} + +func (q *sqlQuerier) DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error { + _, err := q.db.ExecContext(ctx, deleteExternalAuthLink, arg.ProviderID, arg.UserID) + return err +} + const getExternalAuthLink = `-- name: GetExternalAuthLink :one SELECT provider_id, user_id, created_at, updated_at, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, oauth_extra FROM external_auth_links WHERE provider_id = $1 AND user_id = $2 ` diff --git a/coderd/database/queries/externalauth.sql b/coderd/database/queries/externalauth.sql index dfc195b9ea886..8470c44ea9125 100644 --- a/coderd/database/queries/externalauth.sql +++ b/coderd/database/queries/externalauth.sql @@ -1,6 +1,9 @@ -- name: GetExternalAuthLink :one SELECT * FROM external_auth_links WHERE provider_id = $1 AND user_id = $2; +-- name: DeleteExternalAuthLink :exec +DELETE FROM external_auth_links WHERE provider_id = $1 AND user_id = $2; + -- name: GetExternalAuthLinksByUserID :many SELECT * FROM external_auth_links WHERE user_id = $1; diff --git a/coderd/externalauth.go b/coderd/externalauth.go index b1b7acc8bc449..752b3edce3919 100644 --- a/coderd/externalauth.go +++ b/coderd/externalauth.go @@ -10,6 +10,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/httpapi" @@ -77,6 +78,38 @@ func (api *API) externalAuthByID(w http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, w, http.StatusOK, res) } +// @Summary Delete external auth user link by ID +// @ID delete-external-auth-user-link-by-id +// @Security CoderSessionToken +// @Produce json +// @Param externalauth path string true "Git Provider ID" format(string) +// @Success 200 +// @Router /external-auth/{externalauth} [delete] +// deleteExternalAuthByID only deletes the link on the Coder side, does not revoke the token on the provider side. +func (api *API) deleteExternalAuthByID(w http.ResponseWriter, r *http.Request) { + config := httpmw.ExternalAuthParam(r) + apiKey := httpmw.APIKey(r) + ctx := r.Context() + + err := api.Database.DeleteExternalAuthLink(ctx, database.DeleteExternalAuthLinkParams{ + ProviderID: config.ID, + UserID: apiKey.UserID, + }) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + httpapi.ResourceNotFound(w) + return + } + httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to delete external auth link.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, w, http.StatusOK, "OK") +} + // @Summary Post external auth device by ID // @ID post-external-auth-device-by-id // @Security CoderSessionToken @@ -275,3 +308,59 @@ func (api *API) externalAuthCallback(externalAuthConfig *externalauth.Config) ht http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect) } } + +// @Summary Get user external auths +// @ID get-user-external-auths +// @Security CoderSessionToken +// @Produce json +// @Tags Users +// @Success 200 {object} codersdk.ExternalAuthLink +// @Router /users/external-auths [get] +func (api *API) userExternalAuths(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + key := httpmw.APIKey(r) + + links, err := api.Database.GetExternalAuthLinksByUserID(ctx, key.UserID) + if err != nil { + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching user's external auths.", + Detail: err.Error(), + }) + return + } + + // Note: It would be really nice if we could cfg.Validate() the links and + // return their authenticated status. To do this, we would also have to + // refresh expired tokens too. + httpapi.Write(ctx, rw, http.StatusOK, codersdk.ListUserExternalAuthResponse{ + Providers: ExternalAuthConfigs(api.ExternalAuthConfigs), + Links: db2sdk.ExternalAuths(links), + }) +} + +func ExternalAuthConfigs(auths []*externalauth.Config) []codersdk.ExternalAuthLinkProvider { + out := make([]codersdk.ExternalAuthLinkProvider, 0, len(auths)) + for _, auth := range auths { + if auth == nil { + continue + } + out = append(out, ExternalAuthConfig(auth)) + } + return out +} + +func ExternalAuthConfig(cfg *externalauth.Config) codersdk.ExternalAuthLinkProvider { + return codersdk.ExternalAuthLinkProvider{ + ID: cfg.ID, + Type: cfg.Type, + Device: cfg.DeviceAuth != nil, + DisplayName: cfg.DisplayName, + DisplayIcon: cfg.DisplayIcon, + AllowRefresh: !cfg.NoRefresh, + AllowValidate: cfg.ValidateURL != "", + } +} diff --git a/coderd/externalauth_test.go b/coderd/externalauth_test.go index 41ef1ac11dce5..34c1fe7bcdc1e 100644 --- a/coderd/externalauth_test.go +++ b/coderd/externalauth_test.go @@ -145,6 +145,61 @@ func TestExternalAuthByID(t *testing.T) { }) } +// TestExternalAuthManagement is for testing the apis interacting with +// external auths from the user perspective. We assume the external auth +// will always work, so we can test the managing apis like unlinking and +// listing. +func TestExternalAuthManagement(t *testing.T) { + t.Parallel() + t.Run("ListProviders", func(t *testing.T) { + t.Parallel() + const githubID = "fake-github" + const gitlabID = "fake-gitlab" + + github := oidctest.NewFakeIDP(t, oidctest.WithServing()) + gitlab := oidctest.NewFakeIDP(t, oidctest.WithServing()) + + owner := coderdtest.New(t, &coderdtest.Options{ + ExternalAuthConfigs: []*externalauth.Config{ + github.ExternalAuthConfig(t, githubID, nil, func(cfg *externalauth.Config) { + cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String() + }), + gitlab.ExternalAuthConfig(t, gitlabID, nil, func(cfg *externalauth.Config) { + cfg.Type = codersdk.EnhancedExternalAuthProviderGitLab.String() + }), + }, + }) + ownerUser := coderdtest.CreateFirstUser(t, owner) + // Just a regular user + client, _ := coderdtest.CreateAnotherUser(t, owner, ownerUser.OrganizationID) + ctx := testutil.Context(t, testutil.WaitLong) + + // List auths without any links. + list, err := client.ListExternalAuths(ctx) + require.NoError(t, err) + require.Len(t, list.Providers, 2) + require.Len(t, list.Links, 0) + + // Log into github + github.ExternalLogin(t, client) + + list, err = client.ListExternalAuths(ctx) + require.NoError(t, err) + require.Len(t, list.Providers, 2) + require.Len(t, list.Links, 1) + require.Equal(t, list.Links[0].ProviderID, githubID) + + // Unlink + err = client.UnlinkExternalAuthByID(ctx, githubID) + require.NoError(t, err) + + list, err = client.ListExternalAuths(ctx) + require.NoError(t, err) + require.Len(t, list.Providers, 2) + require.Len(t, list.Links, 0) + }) +} + func TestExternalAuthDevice(t *testing.T) { t.Parallel() t.Run("NotSupported", func(t *testing.T) { diff --git a/codersdk/externalauth.go b/codersdk/externalauth.go index 8d858670eed3d..157f1307684be 100644 --- a/codersdk/externalauth.go +++ b/codersdk/externalauth.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "time" ) // EnhancedExternalAuthProvider is a constant that represents enhanced @@ -57,6 +58,36 @@ type ExternalAuth struct { AppInstallURL string `json:"app_install_url"` } +type ListUserExternalAuthResponse struct { + Providers []ExternalAuthLinkProvider `json:"providers"` + // Links are all the authenticated links for the user. + // If a link has a provider ID that does not exist, then that provider + // is no longer configured, rendering it unusable. + Links []ExternalAuthLink `json:"links"` +} + +// ExternalAuthLink is a link between a user and an external auth provider. +// It excludes information that requires a token to access, so can be statically +// built from the database and configs. +type ExternalAuthLink struct { + ProviderID string `json:"provider_id"` + CreatedAt time.Time `json:"created_at" format:"date-time"` + UpdatedAt time.Time `json:"updated_at" format:"date-time"` + HasRefreshToken bool `json:"has_refresh_token"` + Expires time.Time `json:"expires" format:"date-time"` +} + +// ExternalAuthLinkProvider are the static details of a provider. +type ExternalAuthLinkProvider struct { + ID string `json:"id"` + Type string `json:"type"` + Device bool `json:"device"` + DisplayName string `json:"display_name"` + DisplayIcon string `json:"display_icon"` + AllowRefresh bool `json:"allow_refresh"` + AllowValidate bool `json:"allow_validate"` +} + type ExternalAuthAppInstallation struct { ID int `json:"id"` Account ExternalAuthUser `json:"account"` @@ -123,3 +154,32 @@ func (c *Client) ExternalAuthByID(ctx context.Context, provider string) (Externa var extAuth ExternalAuth return extAuth, json.NewDecoder(res.Body).Decode(&extAuth) } + +// UnlinkExternalAuthByID deletes the external auth for the given provider by ID +// for the user. This does not revoke the token from the IDP. +func (c *Client) UnlinkExternalAuthByID(ctx context.Context, provider string) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/external-auth/%s", provider), nil) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return ReadBodyAsError(res) + } + return nil +} + +// ListExternalAuths returns the available external auth providers and the user's +// authenticated links if they exist. +func (c *Client) ListExternalAuths(ctx context.Context) (ListUserExternalAuthResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/external-auth", nil) + if err != nil { + return ListUserExternalAuthResponse{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return ListUserExternalAuthResponse{}, ReadBodyAsError(res) + } + var extAuth ListUserExternalAuthResponse + return extAuth, json.NewDecoder(res.Body).Decode(&extAuth) +} diff --git a/docs/api/default.md b/docs/api/default.md new file mode 100644 index 0000000000000..000a623af0547 --- /dev/null +++ b/docs/api/default.md @@ -0,0 +1,27 @@ +# Default + +## Delete external auth user link by ID + +### Code samples + +```shell +# Example request using curl +curl -X DELETE http://coder-server:8080/api/v2/external-auth/{externalauth} \ + -H 'Coder-Session-Token: API_KEY' +``` + +`DELETE /external-auth/{externalauth}` + +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ---- | -------------- | -------- | --------------- | +| `externalauth` | path | string(string) | true | Git Provider ID | + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | + +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 b2d344fbd9db8..a2d008cb0a2ce 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2997,6 +2997,28 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `user_code` | string | false | | | | `verification_uri` | string | false | | | +## codersdk.ExternalAuthLink + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "expires": "2019-08-24T14:15:22Z", + "has_refresh_token": true, + "provider_id": "string", + "updated_at": "2019-08-24T14:15:22Z" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------- | ------- | -------- | ------------ | ----------- | +| `created_at` | string | false | | | +| `expires` | string | false | | | +| `has_refresh_token` | boolean | false | | | +| `provider_id` | string | false | | | +| `updated_at` | string | false | | | + ## codersdk.ExternalAuthUser ```json diff --git a/docs/api/users.md b/docs/api/users.md index 1ea652b3ab2ef..f9a435ba4e2e0 100644 --- a/docs/api/users.md +++ b/docs/api/users.md @@ -165,6 +165,41 @@ curl -X GET http://coder-server:8080/api/v2/users/authmethods \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get user external auths + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/users/external-auths \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /users/external-auths` + +### Example responses + +> 200 Response + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "expires": "2019-08-24T14:15:22Z", + "has_refresh_token": true, + "provider_id": "string", + "updated_at": "2019-08-24T14:15:22Z" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ExternalAuthLink](schemas.md#codersdkexternalauthlink) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Check initial user created ### Code samples diff --git a/docs/manifest.json b/docs/manifest.json index eb4276ca99ebe..553e19425ac8f 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -523,6 +523,10 @@ "title": "Debug", "path": "./api/debug.md" }, + { + "title": "Default", + "path": "./api/default.md" + }, { "title": "Enterprise", "path": "./api/enterprise.md" diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 3f4763e583f79..580b5dc8ee293 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -495,6 +495,26 @@ export interface ExternalAuthDeviceExchange { readonly device_code: string; } +// From codersdk/externalauth.go +export interface ExternalAuthLink { + readonly provider_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly has_refresh_token: boolean; + readonly expires: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthLinkProvider { + readonly id: string; + readonly type: string; + readonly device: boolean; + readonly display_name: string; + readonly display_icon: string; + readonly allow_refresh: boolean; + readonly allow_validate: boolean; +} + // From codersdk/externalauth.go export interface ExternalAuthUser { readonly login: string; @@ -588,6 +608,12 @@ export interface LinkConfig { readonly icon: string; } +// From codersdk/externalauth.go +export interface ListUserExternalAuthResponse { + readonly providers: ExternalAuthLinkProvider[]; + readonly links: ExternalAuthLink[]; +} + // From codersdk/deployment.go export interface LoggingConfig { readonly log_filter: string[]; From c02c7542782d0b6d413e0c25c444a15c448f2740 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 30 Nov 2023 14:53:10 -0600 Subject: [PATCH 2/2] Update swagger --- coderd/apidoc/docs.go | 54 +++++++++++++++--------------- coderd/apidoc/swagger.json | 44 ++++++++++++------------ coderd/coderd.go | 2 +- coderd/database/dbmem/dbmem.go | 2 +- coderd/externalauth.go | 22 +++++++----- codersdk/externalauth.go | 3 +- docs/api/default.md | 27 --------------- docs/api/git.md | 61 ++++++++++++++++++++++++++++++++++ docs/api/users.md | 35 ------------------- docs/manifest.json | 4 --- 10 files changed, 128 insertions(+), 126 deletions(-) delete mode 100644 docs/api/default.md diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c29917db3ca90..3727429c8ff29 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -748,6 +748,31 @@ const docTemplate = `{ } } }, + "/external-auth": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Git" + ], + "summary": "Get user external auths", + "operationId": "get-user-external-auths", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ExternalAuthLink" + } + } + } + } + }, "/external-auth/{externalauth}": { "get": { "security": [ @@ -788,8 +813,8 @@ const docTemplate = `{ "CoderSessionToken": [] } ], - "produces": [ - "application/json" + "tags": [ + "Git" ], "summary": "Delete external auth user link by ID", "operationId": "delete-external-auth-user-link-by-id", @@ -3464,31 +3489,6 @@ const docTemplate = `{ } } }, - "/users/external-auths": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Get user external auths", - "operationId": "get-user-external-auths", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.ExternalAuthLink" - } - } - } - } - }, "/users/first": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d65b55b0df129..067daf8cebcbc 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -638,6 +638,27 @@ } } }, + "/external-auth": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Git"], + "summary": "Get user external auths", + "operationId": "get-user-external-auths", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ExternalAuthLink" + } + } + } + } + }, "/external-auth/{externalauth}": { "get": { "security": [ @@ -674,7 +695,7 @@ "CoderSessionToken": [] } ], - "produces": ["application/json"], + "tags": ["Git"], "summary": "Delete external auth user link by ID", "operationId": "delete-external-auth-user-link-by-id", "parameters": [ @@ -3048,27 +3069,6 @@ } } }, - "/users/external-auths": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Users"], - "summary": "Get user external auths", - "operationId": "get-user-external-auths", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.ExternalAuthLink" - } - } - } - } - }, "/users/first": { "get": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index 5b10a2e09e969..c5dbc9a89d061 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -665,7 +665,7 @@ func New(options *Options) *API { apiKeyMiddleware, ) // Get without a specific external auth ID will return all external auths. - r.Get("/", api.userExternalAuths) + r.Get("/", api.listUserExternalAuths) r.Route("/{externalauth}", func(r chi.Router) { r.Use( httpmw.ExtractExternalAuthParam(options.ExternalAuthConfigs), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index d6b248e1869d0..6f7dfebc9731f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1019,7 +1019,7 @@ func (*FakeQuerier) DeleteCoordinator(context.Context, uuid.UUID) error { return ErrUnimplemented } -func (q *FakeQuerier) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error { +func (q *FakeQuerier) DeleteExternalAuthLink(_ context.Context, arg database.DeleteExternalAuthLinkParams) error { err := validateDatabaseType(arg) if err != nil { return err diff --git a/coderd/externalauth.go b/coderd/externalauth.go index 752b3edce3919..32d182a249c1e 100644 --- a/coderd/externalauth.go +++ b/coderd/externalauth.go @@ -21,8 +21,8 @@ import ( // @Summary Get external auth by ID // @ID get-external-auth-by-id // @Security CoderSessionToken -// @Produce json // @Tags Git +// @Produce json // @Param externalauth path string true "Git Provider ID" format(string) // @Success 200 {object} codersdk.ExternalAuth // @Router /external-auth/{externalauth} [get] @@ -78,14 +78,15 @@ func (api *API) externalAuthByID(w http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, w, http.StatusOK, res) } +// deleteExternalAuthByID only deletes the link on the Coder side, does not revoke the token on the provider side. +// // @Summary Delete external auth user link by ID // @ID delete-external-auth-user-link-by-id // @Security CoderSessionToken -// @Produce json -// @Param externalauth path string true "Git Provider ID" format(string) +// @Tags Git // @Success 200 +// @Param externalauth path string true "Git Provider ID" format(string) // @Router /external-auth/{externalauth} [delete] -// deleteExternalAuthByID only deletes the link on the Coder side, does not revoke the token on the provider side. func (api *API) deleteExternalAuthByID(w http.ResponseWriter, r *http.Request) { config := httpmw.ExternalAuthParam(r) apiKey := httpmw.APIKey(r) @@ -309,14 +310,17 @@ func (api *API) externalAuthCallback(externalAuthConfig *externalauth.Config) ht } } +// listUserExternalAuths lists all external auths available to a user and +// their auth links if they exist. +// // @Summary Get user external auths // @ID get-user-external-auths // @Security CoderSessionToken // @Produce json -// @Tags Users +// @Tags Git // @Success 200 {object} codersdk.ExternalAuthLink -// @Router /users/external-auths [get] -func (api *API) userExternalAuths(rw http.ResponseWriter, r *http.Request) { +// @Router /external-auth [get] +func (api *API) listUserExternalAuths(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() key := httpmw.APIKey(r) @@ -335,7 +339,9 @@ func (api *API) userExternalAuths(rw http.ResponseWriter, r *http.Request) { // Note: It would be really nice if we could cfg.Validate() the links and // return their authenticated status. To do this, we would also have to - // refresh expired tokens too. + // refresh expired tokens too. For now, I do not want to cause the excess + // traffic on this request, so the user will have to do this with a separate + // call. httpapi.Write(ctx, rw, http.StatusOK, codersdk.ListUserExternalAuthResponse{ Providers: ExternalAuthConfigs(api.ExternalAuthConfigs), Links: db2sdk.ExternalAuths(links), diff --git a/codersdk/externalauth.go b/codersdk/externalauth.go index 157f1307684be..ad2988edb7d74 100644 --- a/codersdk/externalauth.go +++ b/codersdk/externalauth.go @@ -62,7 +62,8 @@ type ListUserExternalAuthResponse struct { Providers []ExternalAuthLinkProvider `json:"providers"` // Links are all the authenticated links for the user. // If a link has a provider ID that does not exist, then that provider - // is no longer configured, rendering it unusable. + // is no longer configured, rendering it unusable. It is still valuable + // to include these links so that the user can unlink them. Links []ExternalAuthLink `json:"links"` } diff --git a/docs/api/default.md b/docs/api/default.md deleted file mode 100644 index 000a623af0547..0000000000000 --- a/docs/api/default.md +++ /dev/null @@ -1,27 +0,0 @@ -# Default - -## Delete external auth user link by ID - -### Code samples - -```shell -# Example request using curl -curl -X DELETE http://coder-server:8080/api/v2/external-auth/{externalauth} \ - -H 'Coder-Session-Token: API_KEY' -``` - -`DELETE /external-auth/{externalauth}` - -### Parameters - -| Name | In | Type | Required | Description | -| -------------- | ---- | -------------- | -------- | --------------- | -| `externalauth` | path | string(string) | true | Git Provider ID | - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/git.md b/docs/api/git.md index 9f2014705da7f..07678b0b8f2c5 100644 --- a/docs/api/git.md +++ b/docs/api/git.md @@ -1,5 +1,40 @@ # Git +## Get user external auths + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/external-auth \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /external-auth` + +### Example responses + +> 200 Response + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "expires": "2019-08-24T14:15:22Z", + "has_refresh_token": true, + "provider_id": "string", + "updated_at": "2019-08-24T14:15:22Z" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ExternalAuthLink](schemas.md#codersdkexternalauthlink) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get external auth by ID ### Code samples @@ -59,6 +94,32 @@ curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth} \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Delete external auth user link by ID + +### Code samples + +```shell +# Example request using curl +curl -X DELETE http://coder-server:8080/api/v2/external-auth/{externalauth} \ + -H 'Coder-Session-Token: API_KEY' +``` + +`DELETE /external-auth/{externalauth}` + +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ---- | -------------- | -------- | --------------- | +| `externalauth` | path | string(string) | true | Git Provider ID | + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get external auth device by ID. ### Code samples diff --git a/docs/api/users.md b/docs/api/users.md index f9a435ba4e2e0..1ea652b3ab2ef 100644 --- a/docs/api/users.md +++ b/docs/api/users.md @@ -165,41 +165,6 @@ curl -X GET http://coder-server:8080/api/v2/users/authmethods \ To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Get user external auths - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/users/external-auths \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /users/external-auths` - -### Example responses - -> 200 Response - -```json -{ - "created_at": "2019-08-24T14:15:22Z", - "expires": "2019-08-24T14:15:22Z", - "has_refresh_token": true, - "provider_id": "string", - "updated_at": "2019-08-24T14:15:22Z" -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ExternalAuthLink](schemas.md#codersdkexternalauthlink) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - ## Check initial user created ### Code samples diff --git a/docs/manifest.json b/docs/manifest.json index 553e19425ac8f..eb4276ca99ebe 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -523,10 +523,6 @@ "title": "Debug", "path": "./api/debug.md" }, - { - "title": "Default", - "path": "./api/default.md" - }, { "title": "Enterprise", "path": "./api/enterprise.md"