mirror of https://github.com/coder/coder.git
feat: add endpoints to list all authed external apps (#10944)
* feat: add endpoints to list all authed external apps Listing the apps allows users to auth to external apps without going through the create workspace flow.
This commit is contained in:
parent
feaa9894a4
commit
81a3b36884
|
@ -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": {
|
||||
|
@ -8852,6 +8904,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": {
|
||||
|
|
|
@ -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": {
|
||||
|
@ -7944,6 +7990,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": {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -754,6 +754,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)
|
||||
}
|
||||
|
@ -996,10 +1003,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) {
|
||||
|
|
|
@ -1027,6 +1027,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()
|
||||
|
|
|
@ -176,6 +176,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)
|
||||
|
|
|
@ -238,6 +238,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()
|
||||
|
|
|
@ -51,6 +51,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
|
||||
|
|
|
@ -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
|
||||
`
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 != "",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3001,6 +3001,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
|
||||
|
|
|
@ -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[];
|
||||
|
|
Loading…
Reference in New Issue