diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c88eb6fb2ba21..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": [ @@ -781,6 +806,33 @@ const docTemplate = `{ } } } + }, + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Git" + ], + "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": { @@ -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..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": [ @@ -667,6 +688,31 @@ } } } + }, + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Git"], + "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": { @@ -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..c5dbc9a89d061 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.listUserExternalAuths) + 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..6f7dfebc9731f 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(_ 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..32d182a249c1e 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" @@ -20,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] @@ -77,6 +78,39 @@ 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 +// @Tags Git +// @Success 200 +// @Param externalauth path string true "Git Provider ID" format(string) +// @Router /external-auth/{externalauth} [delete] +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 +309,64 @@ func (api *API) externalAuthCallback(externalAuthConfig *externalauth.Config) ht http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect) } } + +// 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 Git +// @Success 200 {object} codersdk.ExternalAuthLink +// @Router /external-auth [get] +func (api *API) listUserExternalAuths(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. 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), + }) +} + +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..ad2988edb7d74 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,37 @@ 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. It is still valuable + // to include these links so that the user can unlink them. + 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 +155,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/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/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/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[];