mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
[CLOUD-203] Handle GitHub App setup callback (#29467)
Co-authored-by: Milan Freml <kopancek@users.noreply.github.com> Co-authored-by: Rafał Gajdulewicz <rafax@users.noreply.github.com>
This commit is contained in:
parent
0a6f2c886c
commit
7c13225fca
@ -7,31 +7,31 @@ import (
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/webhooks"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
)
|
||||
|
||||
// Services is a bag of HTTP handlers and factory functions that are registered by the
|
||||
// enterprise frontend setup hook.
|
||||
type Services struct {
|
||||
GitHubWebhook webhooks.Registerer
|
||||
GitLabWebhook http.Handler
|
||||
BitbucketServerWebhook http.Handler
|
||||
NewCodeIntelUploadHandler NewCodeIntelUploadHandler
|
||||
NewExecutorProxyHandler NewExecutorProxyHandler
|
||||
AuthzResolver graphqlbackend.AuthzResolver
|
||||
BatchChangesResolver graphqlbackend.BatchChangesResolver
|
||||
CodeIntelResolver graphqlbackend.CodeIntelResolver
|
||||
InsightsResolver graphqlbackend.InsightsResolver
|
||||
CodeMonitorsResolver graphqlbackend.CodeMonitorsResolver
|
||||
LicenseResolver graphqlbackend.LicenseResolver
|
||||
DotcomResolver graphqlbackend.DotcomRootResolver
|
||||
SearchContextsResolver graphqlbackend.SearchContextsResolver
|
||||
OrgRepositoryResolver graphqlbackend.OrgRepositoryResolver
|
||||
NotebooksResolver graphqlbackend.NotebooksResolver
|
||||
GitHubWebhook webhooks.Registerer
|
||||
GitLabWebhook http.Handler
|
||||
BitbucketServerWebhook http.Handler
|
||||
NewCodeIntelUploadHandler NewCodeIntelUploadHandler
|
||||
NewExecutorProxyHandler NewExecutorProxyHandler
|
||||
NewGitHubAppCloudSetupHandler NewGitHubAppCloudSetupHandler
|
||||
AuthzResolver graphqlbackend.AuthzResolver
|
||||
BatchChangesResolver graphqlbackend.BatchChangesResolver
|
||||
CodeIntelResolver graphqlbackend.CodeIntelResolver
|
||||
InsightsResolver graphqlbackend.InsightsResolver
|
||||
CodeMonitorsResolver graphqlbackend.CodeMonitorsResolver
|
||||
LicenseResolver graphqlbackend.LicenseResolver
|
||||
DotcomResolver graphqlbackend.DotcomRootResolver
|
||||
SearchContextsResolver graphqlbackend.SearchContextsResolver
|
||||
OrgRepositoryResolver graphqlbackend.OrgRepositoryResolver
|
||||
NotebooksResolver graphqlbackend.NotebooksResolver
|
||||
}
|
||||
|
||||
// NewCodeIntelUploadHandler creates a new handler for the LSIF upload endpoint. The
|
||||
@ -43,14 +43,19 @@ type NewCodeIntelUploadHandler func(internal bool) http.Handler
|
||||
// via a shared username and password.
|
||||
type NewExecutorProxyHandler func() http.Handler
|
||||
|
||||
// NewGitHubAppCloudSetupHandler creates a new handler for the Sourcegraph Cloud
|
||||
// GitHub App setup URL endpoint.
|
||||
type NewGitHubAppCloudSetupHandler func() http.Handler
|
||||
|
||||
// DefaultServices creates a new Services value that has default implementations for all services.
|
||||
func DefaultServices() Services {
|
||||
return Services{
|
||||
GitHubWebhook: registerFunc(func(webhook *webhooks.GitHubWebhook) {}),
|
||||
GitLabWebhook: makeNotFoundHandler("gitlab webhook"),
|
||||
BitbucketServerWebhook: makeNotFoundHandler("bitbucket server webhook"),
|
||||
NewCodeIntelUploadHandler: func(_ bool) http.Handler { return makeNotFoundHandler("code intel upload") },
|
||||
NewExecutorProxyHandler: func() http.Handler { return makeNotFoundHandler("executor proxy") },
|
||||
GitHubWebhook: registerFunc(func(webhook *webhooks.GitHubWebhook) {}),
|
||||
GitLabWebhook: makeNotFoundHandler("gitlab webhook"),
|
||||
BitbucketServerWebhook: makeNotFoundHandler("bitbucket server webhook"),
|
||||
NewCodeIntelUploadHandler: func(_ bool) http.Handler { return makeNotFoundHandler("code intel upload") },
|
||||
NewExecutorProxyHandler: func() http.Handler { return makeNotFoundHandler("executor proxy") },
|
||||
NewGitHubAppCloudSetupHandler: func() http.Handler { return makeNotFoundHandler("Sourcegraph Cloud GitHub App setup") },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,14 +5,13 @@ import (
|
||||
|
||||
"github.com/NYTimes/gziphandler"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/userpasswd"
|
||||
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/api"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/globals"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/app/errorutil"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/app/router"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/app/ui"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/auth/userpasswd"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/session"
|
||||
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/trace"
|
||||
)
|
||||
@ -21,7 +20,7 @@ import (
|
||||
//
|
||||
// 🚨 SECURITY: The caller MUST wrap the returned handler in middleware that checks authentication
|
||||
// and sets the actor in the request context.
|
||||
func NewHandler(db database.DB) http.Handler {
|
||||
func NewHandler(db database.DB, githubAppCloudSetupHandler http.Handler) http.Handler {
|
||||
session.SetSessionStore(session.NewRedisStore(func() bool {
|
||||
return globals.ExternalURL().Scheme == "https"
|
||||
}))
|
||||
@ -68,6 +67,9 @@ func NewHandler(db database.DB) http.Handler {
|
||||
// Ping retrieval
|
||||
r.Get(router.LatestPing).Handler(trace.Route(http.HandlerFunc(latestPingHandler(db))))
|
||||
|
||||
// Sourcegraph Cloud GitHub App setup
|
||||
r.Get(router.SetupGitHubAppCloud).Handler(trace.Route(githubAppCloudSetupHandler))
|
||||
|
||||
r.Get(router.Editor).Handler(trace.Route(errorutil.Handler(serveEditor(db))))
|
||||
|
||||
r.Get(router.DebugHeaders).Handler(trace.Route(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -37,6 +37,8 @@ const (
|
||||
|
||||
LatestPing = "pings.latest"
|
||||
|
||||
SetupGitHubAppCloud = "setup.github.app.cloud"
|
||||
|
||||
OldToolsRedirect = "old-tools-redirect"
|
||||
OldTreeRedirect = "old-tree-redirect"
|
||||
|
||||
@ -94,6 +96,8 @@ func newRouter() *mux.Router {
|
||||
|
||||
base.Path("/site-admin/pings/latest").Methods("GET").Name(LatestPing)
|
||||
|
||||
base.Path("/setup/github/app/cloud").Methods("GET").Name(SetupGitHubAppCloud)
|
||||
|
||||
repoPath := `/` + routevar.Repo
|
||||
repo := base.PathPrefix(repoPath + "/" + routevar.RepoPathDelim + "/").Subrouter()
|
||||
repo.Path("/badge.svg").Methods("GET").Name(RepoBadge)
|
||||
|
||||
@ -34,7 +34,16 @@ import (
|
||||
|
||||
// newExternalHTTPHandler creates and returns the HTTP handler that serves the app and API pages to
|
||||
// external clients.
|
||||
func newExternalHTTPHandler(db database.DB, schema *graphql.Schema, gitHubWebhook webhooks.Registerer, gitLabWebhook, bitbucketServerWebhook http.Handler, newCodeIntelUploadHandler enterprise.NewCodeIntelUploadHandler, newExecutorProxyHandler enterprise.NewExecutorProxyHandler, rateLimitWatcher graphqlbackend.LimitWatcher) (http.Handler, error) {
|
||||
func newExternalHTTPHandler(
|
||||
db database.DB,
|
||||
schema *graphql.Schema,
|
||||
gitHubWebhook webhooks.Registerer,
|
||||
gitLabWebhook, bitbucketServerWebhook http.Handler,
|
||||
newCodeIntelUploadHandler enterprise.NewCodeIntelUploadHandler,
|
||||
newExecutorProxyHandler enterprise.NewExecutorProxyHandler,
|
||||
newGitHubAppCloudSetupHandler enterprise.NewGitHubAppCloudSetupHandler,
|
||||
rateLimitWatcher graphqlbackend.LimitWatcher,
|
||||
) (http.Handler, error) {
|
||||
// Each auth middleware determines on a per-request basis whether it should be enabled (if not, it
|
||||
// immediately delegates the request to the next middleware in the chain).
|
||||
authMiddlewares := auth.AuthMiddleware()
|
||||
@ -60,8 +69,10 @@ func newExternalHTTPHandler(db database.DB, schema *graphql.Schema, gitHubWebhoo
|
||||
// 🚨 SECURITY: This handler implements its own token auth inside enterprise
|
||||
executorProxyHandler := newExecutorProxyHandler()
|
||||
|
||||
githubAppCloudSetupHandler := newGitHubAppCloudSetupHandler()
|
||||
|
||||
// App handler (HTML pages), the call order of middleware is LIFO.
|
||||
appHandler := app.NewHandler(db)
|
||||
appHandler := app.NewHandler(db, githubAppCloudSetupHandler)
|
||||
if hooks.PostAuthMiddleware != nil {
|
||||
// 🚨 SECURITY: These all run after the auth handler so the client is authenticated.
|
||||
appHandler = hooks.PostAuthMiddleware(appHandler)
|
||||
|
||||
@ -315,6 +315,7 @@ func makeExternalAPI(db database.DB, schema *graphql.Schema, enterprise enterpri
|
||||
enterprise.BitbucketServerWebhook,
|
||||
enterprise.NewCodeIntelUploadHandler,
|
||||
enterprise.NewExecutorProxyHandler,
|
||||
enterprise.NewGitHubAppCloudSetupHandler,
|
||||
rateLimiter,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
190
enterprise/cmd/frontend/internal/app/app.go
Normal file
190
enterprise/cmd/frontend/internal/app/app.go
Normal file
@ -0,0 +1,190 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
gogithub "github.com/google/go-github/v41/github"
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
"github.com/inconshreveable/log15"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/enterprise"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf/conftypes"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/auth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/github"
|
||||
"github.com/sourcegraph/sourcegraph/internal/jsonc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
)
|
||||
|
||||
// Init initializes the app endpoints.
|
||||
func Init(
|
||||
db database.DB,
|
||||
conf conftypes.UnifiedWatchable,
|
||||
enterpriseServices *enterprise.Services,
|
||||
) error {
|
||||
if !envvar.SourcegraphDotComMode() {
|
||||
enterpriseServices.NewGitHubAppCloudSetupHandler = func() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte("Sourcegraph Cloud GitHub App setup is only available on sourcegraph.com"))
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
appConfig := conf.SiteConfig().Dotcom.GithubAppCloud
|
||||
if appConfig.AppID == "" {
|
||||
enterpriseServices.NewGitHubAppCloudSetupHandler = func() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte("Sourcegraph Cloud GitHub App setup is not enabled"))
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
privateKey, err := base64.StdEncoding.DecodeString(appConfig.PrivateKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "decode private key")
|
||||
}
|
||||
|
||||
auther, err := auth.NewOAuthBearerTokenWithGitHubApp(appConfig.AppID, privateKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "new authenticator with GitHub App")
|
||||
}
|
||||
|
||||
apiURL, err := url.Parse("https://github.com")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse github.com")
|
||||
}
|
||||
client := github.NewV3Client(apiURL, auther, nil)
|
||||
|
||||
enterpriseServices.NewGitHubAppCloudSetupHandler = func() http.Handler {
|
||||
return newGitHubAppCloudSetupHandler(db, apiURL, client)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type githubClient interface {
|
||||
GetAppInstallation(ctx context.Context, installationID int64) (*gogithub.Installation, error)
|
||||
}
|
||||
|
||||
func newGitHubAppCloudSetupHandler(db database.DB, apiURL *url.URL, client githubClient) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !envvar.SourcegraphDotComMode() {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte("Sourcegraph Cloud GitHub App setup is only available on sourcegraph.com"))
|
||||
return
|
||||
}
|
||||
|
||||
state := r.URL.Query().Get("state")
|
||||
orgID, err := graphqlbackend.UnmarshalOrgID(graphql.ID(state))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`The "state" is not a valid graphql.ID of an organization`))
|
||||
return
|
||||
}
|
||||
|
||||
installationID, err := strconv.ParseInt(r.URL.Query().Get("installation_id"), 10, 64)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`The "installation_id" is not a valid integer`))
|
||||
return
|
||||
}
|
||||
|
||||
err = backend.CheckOrgAccess(r.Context(), db, orgID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("the authenticated user does not belong to the organization requested"))
|
||||
return
|
||||
}
|
||||
|
||||
responseServerError := func(msg string, err error) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(msg))
|
||||
log15.Error(msg, "error", err)
|
||||
}
|
||||
|
||||
org, err := db.Orgs().GetByID(r.Context(), orgID)
|
||||
if err != nil {
|
||||
responseServerError("Failed to get organization", err)
|
||||
return
|
||||
}
|
||||
|
||||
externalServices := db.ExternalServices()
|
||||
svcs, err := externalServices.List(r.Context(),
|
||||
database.ExternalServicesListOptions{
|
||||
NamespaceOrgID: org.ID,
|
||||
Kinds: []string{extsvc.KindGitHub},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
responseServerError("Failed to list organization code host connections", err)
|
||||
return
|
||||
}
|
||||
|
||||
ins, err := client.GetAppInstallation(r.Context(), installationID)
|
||||
if err != nil {
|
||||
responseServerError(`Failed to get the installation information using the "installation_id"`, err)
|
||||
return
|
||||
}
|
||||
|
||||
displayName := "GitHub"
|
||||
if ins.Account.Login != nil {
|
||||
displayName = fmt.Sprintf("GitHub (%s)", *ins.Account.Login)
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
var svc *types.ExternalService
|
||||
if len(svcs) == 0 {
|
||||
svc = &types.ExternalService{
|
||||
Kind: extsvc.KindGitHub,
|
||||
DisplayName: displayName,
|
||||
Config: fmt.Sprintf(`
|
||||
{
|
||||
"url": "%s",
|
||||
"githubAppInstallationID": "%d",
|
||||
"repos": []
|
||||
}
|
||||
`, apiURL.String(), installationID),
|
||||
NamespaceOrgID: org.ID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
} else if len(svcs) == 1 {
|
||||
// We have an existing github service, update it
|
||||
svc = svcs[0]
|
||||
svc.DisplayName = displayName
|
||||
newConfig, err := jsonc.Edit(svc.Config, strconv.FormatInt(installationID, 10), "githubAppInstallationID")
|
||||
if err != nil {
|
||||
responseServerError("Failed to edit config", err)
|
||||
return
|
||||
}
|
||||
svc.Config = newConfig
|
||||
svc.UpdatedAt = now
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("Multiple code host connections of same kind found"))
|
||||
return
|
||||
}
|
||||
|
||||
err = db.ExternalServices().Upsert(r.Context(), svc)
|
||||
if err != nil {
|
||||
responseServerError("Failed to upsert code host connection", err)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/organizations/%s/settings/code-hosts", org.Name), http.StatusFound)
|
||||
})
|
||||
}
|
||||
143
enterprise/cmd/frontend/internal/app/app_test.go
Normal file
143
enterprise/cmd/frontend/internal/app/app_test.go
Normal file
@ -0,0 +1,143 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
mockrequire "github.com/derision-test/go-mockgen/testutil/require"
|
||||
gogithub "github.com/google/go-github/v41/github"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
)
|
||||
|
||||
func TestNewGitHubAppCloudSetupHandler(t *testing.T) {
|
||||
orig := envvar.SourcegraphDotComMode()
|
||||
envvar.MockSourcegraphDotComMode(true)
|
||||
defer envvar.MockSourcegraphDotComMode(orig)
|
||||
|
||||
users := database.NewMockUserStore()
|
||||
users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{}, nil)
|
||||
orgMembers := database.NewMockOrgMemberStore()
|
||||
orgs := database.NewMockOrgStore()
|
||||
orgs.GetByIDFunc.SetDefaultHook(func(ctx context.Context, id int32) (*types.Org, error) {
|
||||
return &types.Org{
|
||||
ID: id,
|
||||
Name: "abc-org",
|
||||
}, nil
|
||||
})
|
||||
externalServices := database.NewMockExternalServiceStore()
|
||||
db := database.NewMockDB()
|
||||
db.UsersFunc.SetDefaultReturn(users)
|
||||
db.OrgMembersFunc.SetDefaultReturn(orgMembers)
|
||||
db.OrgsFunc.SetDefaultReturn(orgs)
|
||||
db.ExternalServicesFunc.SetDefaultReturn(externalServices)
|
||||
|
||||
apiURL, err := url.Parse("https://github.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
client := NewMockGithubClient()
|
||||
client.GetAppInstallationFunc.SetDefaultReturn(
|
||||
&gogithub.Installation{
|
||||
Account: &gogithub.User{
|
||||
Login: gogithub.String("abc-org"),
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/.setup/github-app-cloud?installation_id=21994992&setup_action=install&state=T3JnOjE%3D", nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
h := newGitHubAppCloudSetupHandler(db, apiURL, client)
|
||||
|
||||
t.Run("not an organization member", func(t *testing.T) {
|
||||
orgMembers.GetByOrgIDAndUserIDFunc.SetDefaultReturn(nil, nil)
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
h.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
assert.Equal(t, "the authenticated user does not belong to the organization requested", resp.Body.String())
|
||||
})
|
||||
|
||||
t.Run("create new", func(t *testing.T) {
|
||||
orgMembers.GetByOrgIDAndUserIDFunc.SetDefaultReturn(&types.OrgMembership{}, nil)
|
||||
externalServices.UpsertFunc.SetDefaultHook(func(ctx context.Context, svcs ...*types.ExternalService) error {
|
||||
require.Len(t, svcs, 1)
|
||||
|
||||
svc := svcs[0]
|
||||
assert.Equal(t, extsvc.KindGitHub, svc.Kind)
|
||||
assert.Equal(t, "GitHub (abc-org)", svc.DisplayName)
|
||||
|
||||
wantConfig := `
|
||||
{
|
||||
"url": "https://github.com",
|
||||
"githubAppInstallationID": "21994992",
|
||||
"repos": []
|
||||
}
|
||||
`
|
||||
assert.Equal(t, wantConfig, svc.Config)
|
||||
return nil
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
h.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, resp.Code)
|
||||
assert.Equal(t, "/organizations/abc-org/settings/code-hosts", resp.Header().Get("Location"))
|
||||
|
||||
mockrequire.Called(t, externalServices.UpsertFunc)
|
||||
})
|
||||
|
||||
t.Run("update existing", func(t *testing.T) {
|
||||
externalServices.ListFunc.SetDefaultReturn(
|
||||
[]*types.ExternalService{
|
||||
{
|
||||
Kind: extsvc.KindGitHub,
|
||||
DisplayName: "GitHub (old)",
|
||||
Config: `
|
||||
{
|
||||
"url": "https://github.com",
|
||||
"repos": []
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
externalServices.UpsertFunc.SetDefaultHook(func(ctx context.Context, svcs ...*types.ExternalService) error {
|
||||
require.Len(t, svcs, 1)
|
||||
|
||||
svc := svcs[0]
|
||||
assert.Equal(t, extsvc.KindGitHub, svc.Kind)
|
||||
assert.Equal(t, "GitHub (abc-org)", svc.DisplayName)
|
||||
|
||||
wantConfig := `
|
||||
{
|
||||
"url": "https://github.com",
|
||||
"repos": [],
|
||||
"githubAppInstallationID": "21994992"
|
||||
}
|
||||
`
|
||||
assert.Equal(t, wantConfig, svc.Config)
|
||||
return nil
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
h.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, resp.Code)
|
||||
assert.Equal(t, "/organizations/abc-org/settings/code-hosts", resp.Header().Get("Location"))
|
||||
|
||||
mockrequire.Called(t, externalServices.ListFunc)
|
||||
mockrequire.Called(t, externalServices.UpsertFunc)
|
||||
})
|
||||
}
|
||||
3
enterprise/cmd/frontend/internal/app/gen.go
Normal file
3
enterprise/cmd/frontend/internal/app/gen.go
Normal file
@ -0,0 +1,3 @@
|
||||
package app
|
||||
|
||||
//go:generate ../../../../../dev/mockgen.sh github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/app -i githubClient -o mock_github_client_test.go
|
||||
174
enterprise/cmd/frontend/internal/app/mock_github_client_test.go
Normal file
174
enterprise/cmd/frontend/internal/app/mock_github_client_test.go
Normal file
@ -0,0 +1,174 @@
|
||||
// Code generated by go-mockgen 1.1.2; DO NOT EDIT.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
github "github.com/google/go-github/v41/github"
|
||||
)
|
||||
|
||||
// MockGithubClient is a mock implementation of the githubClient interface
|
||||
// (from the package
|
||||
// github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/app)
|
||||
// used for unit testing.
|
||||
type MockGithubClient struct {
|
||||
// GetAppInstallationFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method GetAppInstallation.
|
||||
GetAppInstallationFunc *GithubClientGetAppInstallationFunc
|
||||
}
|
||||
|
||||
// NewMockGithubClient creates a new mock of the githubClient interface. All
|
||||
// methods return zero values for all results, unless overwritten.
|
||||
func NewMockGithubClient() *MockGithubClient {
|
||||
return &MockGithubClient{
|
||||
GetAppInstallationFunc: &GithubClientGetAppInstallationFunc{
|
||||
defaultHook: func(context.Context, int64) (*github.Installation, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewStrictMockGithubClient creates a new mock of the githubClient
|
||||
// interface. All methods panic on invocation, unless overwritten.
|
||||
func NewStrictMockGithubClient() *MockGithubClient {
|
||||
return &MockGithubClient{
|
||||
GetAppInstallationFunc: &GithubClientGetAppInstallationFunc{
|
||||
defaultHook: func(context.Context, int64) (*github.Installation, error) {
|
||||
panic("unexpected invocation of MockGithubClient.GetAppInstallation")
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// surrogateMockGithubClient is a copy of the githubClient interface (from
|
||||
// the package
|
||||
// github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/app).
|
||||
// It is redefined here as it is unexported in the source package.
|
||||
type surrogateMockGithubClient interface {
|
||||
GetAppInstallation(context.Context, int64) (*github.Installation, error)
|
||||
}
|
||||
|
||||
// NewMockGithubClientFrom creates a new mock of the MockGithubClient
|
||||
// interface. All methods delegate to the given implementation, unless
|
||||
// overwritten.
|
||||
func NewMockGithubClientFrom(i surrogateMockGithubClient) *MockGithubClient {
|
||||
return &MockGithubClient{
|
||||
GetAppInstallationFunc: &GithubClientGetAppInstallationFunc{
|
||||
defaultHook: i.GetAppInstallation,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GithubClientGetAppInstallationFunc describes the behavior when the
|
||||
// GetAppInstallation method of the parent MockGithubClient instance is
|
||||
// invoked.
|
||||
type GithubClientGetAppInstallationFunc struct {
|
||||
defaultHook func(context.Context, int64) (*github.Installation, error)
|
||||
hooks []func(context.Context, int64) (*github.Installation, error)
|
||||
history []GithubClientGetAppInstallationFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// GetAppInstallation delegates to the next hook function in the queue and
|
||||
// stores the parameter and result values of this invocation.
|
||||
func (m *MockGithubClient) GetAppInstallation(v0 context.Context, v1 int64) (*github.Installation, error) {
|
||||
r0, r1 := m.GetAppInstallationFunc.nextHook()(v0, v1)
|
||||
m.GetAppInstallationFunc.appendCall(GithubClientGetAppInstallationFuncCall{v0, v1, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the GetAppInstallation
|
||||
// method of the parent MockGithubClient instance is invoked and the hook
|
||||
// queue is empty.
|
||||
func (f *GithubClientGetAppInstallationFunc) SetDefaultHook(hook func(context.Context, int64) (*github.Installation, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// GetAppInstallation method of the parent MockGithubClient instance invokes
|
||||
// the hook at the front of the queue and discards it. After the queue is
|
||||
// empty, the default hook function is invoked for any future action.
|
||||
func (f *GithubClientGetAppInstallationFunc) PushHook(hook func(context.Context, int64) (*github.Installation, error)) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultDefaultHook with a function that returns
|
||||
// the given values.
|
||||
func (f *GithubClientGetAppInstallationFunc) SetDefaultReturn(r0 *github.Installation, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, int64) (*github.Installation, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushDefaultHook with a function that returns the given
|
||||
// values.
|
||||
func (f *GithubClientGetAppInstallationFunc) PushReturn(r0 *github.Installation, r1 error) {
|
||||
f.PushHook(func(context.Context, int64) (*github.Installation, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *GithubClientGetAppInstallationFunc) nextHook() func(context.Context, int64) (*github.Installation, error) {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *GithubClientGetAppInstallationFunc) appendCall(r0 GithubClientGetAppInstallationFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of GithubClientGetAppInstallationFuncCall
|
||||
// objects describing the invocations of this function.
|
||||
func (f *GithubClientGetAppInstallationFunc) History() []GithubClientGetAppInstallationFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]GithubClientGetAppInstallationFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// GithubClientGetAppInstallationFuncCall is an object that describes an
|
||||
// invocation of method GetAppInstallation on an instance of
|
||||
// MockGithubClient.
|
||||
type GithubClientGetAppInstallationFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 int64
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 *github.Installation
|
||||
// Result1 is the value of the 2nd result returned from this method
|
||||
// invocation.
|
||||
Result1 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c GithubClientGetAppInstallationFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c GithubClientGetAppInstallationFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
@ -7,19 +7,17 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/oobmigration"
|
||||
|
||||
"github.com/inconshreveable/log15"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/enterprise"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/shared"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/app"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/authz"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/batches"
|
||||
@ -36,6 +34,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf/conftypes"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
"github.com/sourcegraph/sourcegraph/internal/oobmigration"
|
||||
"github.com/sourcegraph/sourcegraph/internal/trace"
|
||||
)
|
||||
|
||||
@ -84,18 +83,22 @@ func enterpriseSetupHook(db database.DB, conf conftypes.UnifiedWatchable) enterp
|
||||
}
|
||||
|
||||
if err := codeintel.Init(ctx, db, conf, &enterpriseServices, observationContext, services); err != nil {
|
||||
log.Fatal(fmt.Sprintf("failed to initialize codeintel: %s", err))
|
||||
log.Fatalf("failed to initialize codeintel: %s", err)
|
||||
}
|
||||
|
||||
// Initialize executor-specific services with the code-intel services.
|
||||
if err := executor.Init(ctx, db, conf, &enterpriseServices, observationContext, services.InternalUploadHandler); err != nil {
|
||||
log.Fatal(fmt.Sprintf("failed to initialize executor: %s", err))
|
||||
log.Fatalf("failed to initialize executor: %s", err)
|
||||
}
|
||||
|
||||
if err := app.Init(db, conf, &enterpriseServices); err != nil {
|
||||
log.Fatalf("failed to initialize app: %s", err)
|
||||
}
|
||||
|
||||
// Initialize all the enterprise-specific services that do not need the codeintel-specific services.
|
||||
for name, fn := range initFunctions {
|
||||
if err := fn(ctx, db, conf, &enterpriseServices, observationContext); err != nil {
|
||||
log.Fatal(fmt.Sprintf("failed to initialize %s: %s", name, err))
|
||||
log.Fatalf("failed to initialize %s: %s", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -681,11 +681,11 @@ func TestExternalServices_ValidateConfig(t *testing.T) {
|
||||
},
|
||||
{
|
||||
kind: extsvc.KindGitHub,
|
||||
desc: "without url, token, repositoryQuery, repos nor orgs",
|
||||
desc: "without url, token, githubAppInstallationID, repositoryQuery, repos nor orgs",
|
||||
config: `{}`,
|
||||
assert: includes(
|
||||
"url is required",
|
||||
"token is required",
|
||||
"at least one of token or githubAppInstallationID must be set",
|
||||
"at least one of repositoryQuery, repos or orgs must be set",
|
||||
),
|
||||
},
|
||||
@ -700,6 +700,17 @@ func TestExternalServices_ValidateConfig(t *testing.T) {
|
||||
}`,
|
||||
assert: equals(`<nil>`),
|
||||
},
|
||||
{
|
||||
kind: extsvc.KindGitHub,
|
||||
desc: "with url, githubAppInstallationID, repos",
|
||||
config: `
|
||||
{
|
||||
"url": "https://github.corp.com",
|
||||
"githubAppInstallationID": "21994992",
|
||||
"repos": [],
|
||||
}`,
|
||||
assert: equals(`<nil>`),
|
||||
},
|
||||
{
|
||||
kind: extsvc.KindGitHub,
|
||||
desc: "with url, token, repos",
|
||||
|
||||
1
go.mod
1
go.mod
@ -244,6 +244,7 @@ require (
|
||||
github.com/go-openapi/validate v0.20.3 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
|
||||
|
||||
1
go.sum
1
go.sum
@ -645,6 +645,7 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=
|
||||
|
||||
@ -509,6 +509,9 @@ func (e *externalServiceStore) validateGitHubConnection(ctx context.Context, id
|
||||
err = multierror.Append(err, validate(c))
|
||||
}
|
||||
|
||||
if c.Token == "" && c.GithubAppInstallationID == "" {
|
||||
err = multierror.Append(err, errors.New("at least one of token or githubAppInstallationID must be set"))
|
||||
}
|
||||
if c.Repos == nil && c.RepositoryQuery == nil && c.Orgs == nil {
|
||||
err = multierror.Append(err, errors.New("at least one of repositoryQuery, repos or orgs must be set"))
|
||||
}
|
||||
|
||||
@ -157,14 +157,14 @@ func TestExternalServicesStore_ValidateConfig(t *testing.T) {
|
||||
{
|
||||
name: "1 error",
|
||||
kind: extsvc.KindGitHub,
|
||||
config: `{"url": "https://github.com", "repositoryQuery": ["none"], "token": ""}`,
|
||||
wantErr: "1 error occurred:\n\t* token: String length must be greater than or equal to 1\n\n",
|
||||
config: `{"repositoryQuery": ["none"], "token": "fake"}`,
|
||||
wantErr: "1 error occurred:\n\t* url is required\n\n",
|
||||
},
|
||||
{
|
||||
name: "2 errors",
|
||||
kind: extsvc.KindGitHub,
|
||||
config: `{"url": "https://github.com", "repositoryQuery": ["none"], "token": "", "x": 123}`,
|
||||
wantErr: "2 errors occurred:\n\t* Additional property x is not allowed\n\t* token: String length must be greater than or equal to 1\n\n",
|
||||
config: `{"url": "https://github.com", "repositoryQuery": ["none"], "token": ""}`,
|
||||
wantErr: "2 errors occurred:\n\t* token: String length must be greater than or equal to 1\n\t* at least one of token or githubAppInstallationID must be set\n\n",
|
||||
},
|
||||
{
|
||||
name: "no conflicting rate limit",
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
// OAuthBearerToken implements OAuth Bearer Token authentication for extsvc
|
||||
@ -49,3 +54,57 @@ func (token *OAuthBearerTokenWithSSH) Hash() string {
|
||||
shaSum := sha256.Sum256([]byte(token.Token + token.PrivateKey + token.Passphrase + token.PublicKey))
|
||||
return hex.EncodeToString(shaSum[:])
|
||||
}
|
||||
|
||||
// oauthBearerTokenWithGitHubApp implements OAuth Bearer Token authentication for
|
||||
// GitHub Apps.
|
||||
type oauthBearerTokenWithGitHubApp struct {
|
||||
appID string
|
||||
key *rsa.PrivateKey
|
||||
rawKey []byte
|
||||
}
|
||||
|
||||
// NewOAuthBearerTokenWithGitHubApp constructs a new OAuth Bearer Token
|
||||
// authenticator for GitHub Apps using given appID and private key.
|
||||
func NewOAuthBearerTokenWithGitHubApp(appID string, privateKey []byte) (Authenticator, error) {
|
||||
key, err := jwt.ParseRSAPrivateKeyFromPEM(privateKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parse private key")
|
||||
}
|
||||
return &oauthBearerTokenWithGitHubApp{
|
||||
appID: appID,
|
||||
key: key,
|
||||
rawKey: privateKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Authenticate is a modified version of
|
||||
// https://github.com/bradleyfalzon/ghinstallation/blob/24e56b3fb7669f209134a01eff731d7e2ef72a5c/appsTransport.go#L66.
|
||||
func (token *oauthBearerTokenWithGitHubApp) Authenticate(r *http.Request) error {
|
||||
// The payload computation is following GitHub App's Ruby example shown in
|
||||
// https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-a-github-app.
|
||||
//
|
||||
// NOTE: GitHub rejects expiry and issue timestamps that are not an integer,
|
||||
// while the jwt-go library serializes to fractional timestamps. Truncate them
|
||||
// before passing to jwt-go.
|
||||
iss := time.Now().Add(-time.Minute).Truncate(time.Second)
|
||||
exp := iss.Add(10 * time.Minute)
|
||||
claims := &jwt.StandardClaims{
|
||||
IssuedAt: iss.Unix(),
|
||||
ExpiresAt: exp.Unix(),
|
||||
Issuer: token.appID,
|
||||
}
|
||||
bearer := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
|
||||
signedString, err := bearer.SignedString(token.key)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "sign JWT")
|
||||
}
|
||||
|
||||
r.Header.Set("Authorization", "Bearer "+signedString)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (token *oauthBearerTokenWithGitHubApp) Hash() string {
|
||||
shaSum := sha256.Sum256(token.rawKey)
|
||||
return hex.EncodeToString(shaSum[:])
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/google/go-github/v41/github"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
@ -725,3 +726,12 @@ func (c *V3Client) Fork(ctx context.Context, owner, repo string, org *string) (*
|
||||
|
||||
return convertRestRepo(restRepo), nil
|
||||
}
|
||||
|
||||
// GetAppInstallation gets information of a GitHub App installation.
|
||||
func (c *V3Client) GetAppInstallation(ctx context.Context, installationID int64) (*github.Installation, error) {
|
||||
var ins github.Installation
|
||||
if err := c.requestGet(ctx, fmt.Sprintf("app/installations/%d", installationID), &ins); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ins, nil
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"allowComments": true,
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["url", "token"],
|
||||
"required": ["url"],
|
||||
"properties": {
|
||||
"url": {
|
||||
"description": "URL of a GitHub instance, such as https://github.com or https://github-enterprise.example.com.",
|
||||
@ -171,6 +171,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"githubAppInstallationID": {
|
||||
"description": "The installation ID of the GitHub App.",
|
||||
"type": "string"
|
||||
},
|
||||
"cloudGlobal": {
|
||||
"title": "CloudGlobal",
|
||||
"description": "When set to true, this external service will be chosen as our 'Global' GitHub service. Only valid on Sourcegraph.com. Only one service can have this flag set.",
|
||||
|
||||
@ -436,6 +436,8 @@ type DebugLog struct {
|
||||
|
||||
// Dotcom description: Configuration options for Sourcegraph.com only.
|
||||
type Dotcom struct {
|
||||
// GithubAppCloud description: The config options for Sourcegraph Cloud GitHub App.
|
||||
GithubAppCloud *GithubAppCloud `json:"githubApp.cloud,omitempty"`
|
||||
// SlackLicenseExpirationWebhook description: Slack webhook for upcoming license expiration notifications.
|
||||
SlackLicenseExpirationWebhook string `json:"slackLicenseExpirationWebhook,omitempty"`
|
||||
}
|
||||
@ -715,6 +717,8 @@ type GitHubConnection struct {
|
||||
//
|
||||
// If "ssh", Sourcegraph will access GitHub repositories using Git URLs of the form git@github.com:myteam/myproject.git. See the documentation for how to provide SSH private keys and known_hosts: https://docs.sourcegraph.com/admin/repo/auth#repositories-that-need-http-s-or-ssh-authentication.
|
||||
GitURLType string `json:"gitURLType,omitempty"`
|
||||
// GithubAppInstallationID description: The installation ID of the GitHub App.
|
||||
GithubAppInstallationID string `json:"githubAppInstallationID,omitempty"`
|
||||
// InitialRepositoryEnablement description: Deprecated and ignored field which will be removed entirely in the next release. GitHub repositories can no longer be enabled or disabled explicitly. Configure repositories to be mirrored via "repos", "exclude" and "repositoryQuery" instead.
|
||||
InitialRepositoryEnablement bool `json:"initialRepositoryEnablement,omitempty"`
|
||||
// Orgs description: An array of organization names identifying GitHub organizations whose repositories should be mirrored on Sourcegraph.
|
||||
@ -747,7 +751,7 @@ type GitHubConnection struct {
|
||||
// If you need to narrow the set of mirrored repositories further (and don't want to enumerate it with a list or query set as above), create a new bot/machine user on GitHub or GitHub Enterprise that is only affiliated with the desired repositories.
|
||||
RepositoryQuery []string `json:"repositoryQuery,omitempty"`
|
||||
// Token description: A GitHub personal access token. Create one for GitHub.com at https://github.com/settings/tokens/new?description=Sourcegraph (for GitHub Enterprise, replace github.com with your instance's hostname). See https://docs.sourcegraph.com/admin/external_service/github#github-api-token-and-access for which scopes are required for which use cases.
|
||||
Token string `json:"token"`
|
||||
Token string `json:"token,omitempty"`
|
||||
// Url description: URL of a GitHub instance, such as https://github.com or https://github-enterprise.example.com.
|
||||
Url string `json:"url"`
|
||||
// Webhooks description: An array of configurations defining existing GitHub webhooks that send updates back to Sourcegraph.
|
||||
@ -858,6 +862,14 @@ type GitLabWebhook struct {
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// GithubAppCloud description: The config options for Sourcegraph Cloud GitHub App.
|
||||
type GithubAppCloud struct {
|
||||
// AppID description: The app ID of the GitHub App for Sourcegraph Cloud.
|
||||
AppID string `json:"appID,omitempty"`
|
||||
// PrivateKey description: The base64-encoded private key of the GitHub App for Sourcegraph Cloud.
|
||||
PrivateKey string `json:"privateKey,omitempty"`
|
||||
}
|
||||
|
||||
// GitoliteConnection description: Configuration for a connection to Gitolite.
|
||||
type GitoliteConnection struct {
|
||||
// Exclude description: A list of repositories to never mirror from this Gitolite instance. Supports excluding by exact name ({"name": "foo"}).
|
||||
|
||||
@ -1086,6 +1086,20 @@
|
||||
"description": "Slack webhook for upcoming license expiration notifications.",
|
||||
"type": "string",
|
||||
"group": "Sourcegraph.com"
|
||||
},
|
||||
"githubApp.cloud": {
|
||||
"description": "The config options for Sourcegraph Cloud GitHub App.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"appID": {
|
||||
"description": "The app ID of the GitHub App for Sourcegraph Cloud.",
|
||||
"type": "string"
|
||||
},
|
||||
"privateKey": {
|
||||
"description": "The base64-encoded private key of the GitHub App for Sourcegraph Cloud.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": "Sourcegraph.com"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user