diff --git a/cmd/frontend/auth/BUILD.bazel b/cmd/frontend/auth/BUILD.bazel index 65fac3b6f73e..b38e106a74ec 100644 --- a/cmd/frontend/auth/BUILD.bazel +++ b/cmd/frontend/auth/BUILD.bazel @@ -15,7 +15,6 @@ go_library( visibility = ["//visibility:public"], deps = [ "//cmd/frontend/backend", - "//cmd/frontend/globals", "//cmd/frontend/internal/app/router", "//cmd/frontend/internal/app/ui/router", "//internal/actor", diff --git a/cmd/frontend/auth/reset_password.go b/cmd/frontend/auth/reset_password.go index f5b7fe3c4fed..a6f98001214c 100644 --- a/cmd/frontend/auth/reset_password.go +++ b/cmd/frontend/auth/reset_password.go @@ -2,11 +2,11 @@ package auth import ( "context" + "net/url" "github.com/sourcegraph/log" "github.com/sourcegraph/sourcegraph/cmd/frontend/backend" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/internal/auth/userpasswd" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" @@ -41,6 +41,13 @@ func ResetPasswordURL(ctx context.Context, db database.DB, logger log.Logger, us return nil, errors.Wrap(err, msg) } - ru := globals.ExternalURL().ResolveReference(resetURL).String() + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + msg := "failed to parse External URL" + logger.Error(msg, log.Error(err)) + return nil, errors.Wrap(err, msg) + } + + ru := externalURL.ResolveReference(resetURL).String() return &ru, nil } diff --git a/cmd/frontend/backend/BUILD.bazel b/cmd/frontend/backend/BUILD.bazel index 913bf87621da..66a5c873bce6 100644 --- a/cmd/frontend/backend/BUILD.bazel +++ b/cmd/frontend/backend/BUILD.bazel @@ -23,7 +23,6 @@ go_library( visibility = ["//visibility:public"], deps = [ "//cmd/frontend/envvar", - "//cmd/frontend/globals", "//cmd/frontend/internal/app/router", "//internal/actor", "//internal/api", diff --git a/cmd/frontend/backend/user_emails.go b/cmd/frontend/backend/user_emails.go index 8a7260fd3375..89b5e5a480b2 100644 --- a/cmd/frontend/backend/user_emails.go +++ b/cmd/frontend/backend/user_emails.go @@ -10,7 +10,6 @@ import ( "github.com/sourcegraph/log" "github.com/sourcegraph/sourcegraph/cmd/frontend/envvar" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/app/router" "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/auth" @@ -330,6 +329,11 @@ func (e *userEmails) SendUserEmailOnFieldUpdate(ctx context.Context, id int32, c return err } + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return err + } + return txemail.Send(ctx, "user_account_update", txemail.Message{ To: []string{email}, Template: updateAccountEmailTemplate, @@ -342,7 +346,7 @@ func (e *userEmails) SendUserEmailOnFieldUpdate(ctx context.Context, id int32, c Email: email, Change: change, Username: usr.Username, - Host: globals.ExternalURL().Host, + Host: externalURL.Host, }, }) } @@ -371,6 +375,11 @@ func (e *userEmails) SendUserEmailOnAccessTokenChange(ctx context.Context, id in return err } + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return err + } + var tmpl txtypes.Templates if deleted { tmpl = accessTokenDeletedEmailTemplate @@ -389,7 +398,7 @@ func (e *userEmails) SendUserEmailOnAccessTokenChange(ctx context.Context, id in Email: email, TokenName: tokenName, Username: usr.Username, - Host: globals.ExternalURL().Host, + Host: externalURL.Host, }, }) } @@ -522,6 +531,12 @@ func SendUserEmailVerificationEmail(ctx context.Context, username, email, code s q.Set("code", code) q.Set("email", email) verifyEmailPath, _ := router.Router().Get(router.VerifyEmail).URLPath() + + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return err + } + return txemail.Send(ctx, "user_email_verification", txemail.Message{ To: []string{email}, Template: verifyEmailTemplates, @@ -531,11 +546,11 @@ func SendUserEmailVerificationEmail(ctx context.Context, username, email, code s Host string }{ Username: username, - URL: globals.ExternalURL().ResolveReference(&url.URL{ + URL: externalURL.ResolveReference(&url.URL{ Path: verifyEmailPath.Path, RawQuery: q.Encode(), }).String(), - Host: globals.ExternalURL().Host, + Host: externalURL.Host, }, }) } diff --git a/cmd/frontend/backend/user_emails_test.go b/cmd/frontend/backend/user_emails_test.go index 9df275a7661a..9d733a5d947e 100644 --- a/cmd/frontend/backend/user_emails_test.go +++ b/cmd/frontend/backend/user_emails_test.go @@ -139,6 +139,13 @@ func TestCheckEmailAbuse(t *testing.T) { } func TestSendUserEmailVerificationEmail(t *testing.T) { + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) + var sent *txemail.Message txemail.MockSend = func(ctx context.Context, message txemail.Message) error { sent = &message @@ -177,6 +184,13 @@ func TestSendUserEmailOnFieldUpdate(t *testing.T) { } defer func() { txemail.MockSend = nil }() + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) + userEmails := dbmocks.NewMockUserEmailsStore() userEmails.GetPrimaryEmailFunc.SetDefaultReturn("a@example.com", true, nil) @@ -225,6 +239,13 @@ func TestSendUserEmailOnTokenChange(t *testing.T) { } defer func() { txemail.MockSend = nil }() + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) + userEmails := dbmocks.NewMockUserEmailsStore() userEmails.GetPrimaryEmailFunc.SetDefaultReturn("a@example.com", true, nil) diff --git a/cmd/frontend/external/globals/BUILD.bazel b/cmd/frontend/external/globals/BUILD.bazel deleted file mode 100644 index 2307a09d848d..000000000000 --- a/cmd/frontend/external/globals/BUILD.bazel +++ /dev/null @@ -1,9 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") - -go_library( - name = "globals", - srcs = ["globals.go"], - importpath = "github.com/sourcegraph/sourcegraph/cmd/frontend/external/globals", - visibility = ["//visibility:public"], - deps = ["//cmd/frontend/globals"], -) diff --git a/cmd/frontend/external/globals/globals.go b/cmd/frontend/external/globals/globals.go deleted file mode 100644 index 679806157538..000000000000 --- a/cmd/frontend/external/globals/globals.go +++ /dev/null @@ -1,13 +0,0 @@ -// Package globals exports symbols from frontend/globals. See the parent -// package godoc for more information. -package globals - -import ( - "net/url" - - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" -) - -func ExternalURL() *url.URL { - return globals.ExternalURL() -} diff --git a/cmd/frontend/globals/globals.go b/cmd/frontend/globals/globals.go index 91ddbc63ac48..989b0812ca47 100644 --- a/cmd/frontend/globals/globals.go +++ b/cmd/frontend/globals/globals.go @@ -2,7 +2,6 @@ package globals import ( - "net/url" "reflect" "sync" "sync/atomic" @@ -13,59 +12,6 @@ import ( "github.com/sourcegraph/sourcegraph/schema" ) -var defaultExternalURL = &url.URL{ - Scheme: "http", - Host: "example.com", -} - -var externalURL = func() atomic.Value { - var v atomic.Value - v.Store(defaultExternalURL) - return v -}() - -var watchExternalURLOnce sync.Once - -// WatchExternalURL watches for changes in the `externalURL` site configuration -// so that changes are reflected in what is returned by the ExternalURL function. -func WatchExternalURL() { - watchExternalURLOnce.Do(func() { - conf.Watch(func() { - after := defaultExternalURL - if val := conf.Get().ExternalURL; val != "" { - var err error - if after, err = url.Parse(val); err != nil { - log15.Error("globals.ExternalURL", "value", val, "error", err) - return - } - } - - if before := ExternalURL(); !reflect.DeepEqual(before, after) { - SetExternalURL(after) - if before.Host != "example.com" { - log15.Info( - "globals.ExternalURL", - "updated", true, - "before", before, - "after", after, - ) - } - } - }) - }) -} - -// ExternalURL returns the fully-resolved, externally accessible frontend URL. -// Callers must not mutate the returned pointer. -func ExternalURL() *url.URL { - return externalURL.Load().(*url.URL) -} - -// SetExternalURL sets the fully-resolved, externally accessible frontend URL. -func SetExternalURL(u *url.URL) { - externalURL.Store(u) -} - var defaultPermissionsUserMapping = &schema.PermissionsUserMapping{ Enabled: false, BindID: "email", diff --git a/cmd/frontend/graphqlbackend/email_invitation.go b/cmd/frontend/graphqlbackend/email_invitation.go index 71b7fc9df818..9879797fcd13 100644 --- a/cmd/frontend/graphqlbackend/email_invitation.go +++ b/cmd/frontend/graphqlbackend/email_invitation.go @@ -10,8 +10,8 @@ import ( "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/cmd/frontend/envvar" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/goroutine" "github.com/sourcegraph/sourcegraph/internal/rcache" @@ -68,6 +68,14 @@ func (r *schemaResolver) InviteEmailToSourcegraph(ctx context.Context, args *str } urlSignUp, _ := url.Parse("/sign-up?invitedBy=" + invitedBy.Username) + + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + log15.Warn("email invites: failed parse external url", "error", err) + invitedEmailsLimiter.Delete(args.Email) // allow attempting to invite this email again without waiting 24h + return + } + if err := txemail.Send(ctx, "user_invite", txemail.Message{ To: []string{args.Email}, Template: emailTemplateEmailInvitation, @@ -76,7 +84,7 @@ func (r *schemaResolver) InviteEmailToSourcegraph(ctx context.Context, args *str URL string }{ FromName: invitedBy.Username, - URL: globals.ExternalURL().ResolveReference(urlSignUp).String(), + URL: externalURL.ResolveReference(urlSignUp).String(), }, }); err != nil { log15.Warn("email invites: failed to send email", "error", err) diff --git a/cmd/frontend/graphqlbackend/git_tree_entry.go b/cmd/frontend/graphqlbackend/git_tree_entry.go index 0355d059acae..c1ba3d97650f 100644 --- a/cmd/frontend/graphqlbackend/git_tree_entry.go +++ b/cmd/frontend/graphqlbackend/git_tree_entry.go @@ -14,13 +14,13 @@ import ( "github.com/inconshreveable/log15" "go.opentelemetry.io/otel/attribute" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/externallink" "github.com/sourcegraph/sourcegraph/internal/api" "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/binary" "github.com/sourcegraph/sourcegraph/internal/cloneurls" resolverstubs "github.com/sourcegraph/sourcegraph/internal/codeintel/resolvers" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/gitserver" "github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain" @@ -280,7 +280,12 @@ func (r *GitTreeEntryResolver) ExternalURLs(ctx context.Context) ([]*externallin } func (r *GitTreeEntryResolver) RawZipArchiveURL() string { - return globals.ExternalURL().ResolveReference(&url.URL{ + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return "" + } + + return externalURL.ResolveReference(&url.URL{ Path: path.Join(r.Repository().URL(), "-/raw/", r.Path()), RawQuery: "format=zip", }).String() diff --git a/cmd/frontend/graphqlbackend/git_tree_entry_test.go b/cmd/frontend/graphqlbackend/git_tree_entry_test.go index 25439939831b..03c98558e28a 100644 --- a/cmd/frontend/graphqlbackend/git_tree_entry_test.go +++ b/cmd/frontend/graphqlbackend/git_tree_entry_test.go @@ -6,8 +6,11 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/sourcegraph/internal/authz" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database/dbmocks" + "github.com/sourcegraph/sourcegraph/schema" "github.com/sourcegraph/sourcegraph/internal/api" "github.com/sourcegraph/sourcegraph/internal/gitserver" @@ -23,6 +26,11 @@ func TestGitTreeEntry_RawZipArchiveURL(t *testing.T) { }, Stat: CreateFileInfo("a/b", true), } + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) got := NewGitTreeEntryResolver(db, gitserverClient, opts).RawZipArchiveURL() want := "http://example.com/my/repo/-/raw/a/b?format=zip" if got != want { @@ -36,6 +44,11 @@ func TestGitTreeEntry_Content(t *testing.T) { db := dbmocks.NewMockDB() gitserverClient := gitserver.NewMockClient() + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) gitserverClient.ReadFileFunc.SetDefaultHook(func(_ context.Context, _ authz.SubRepoPermissionChecker, _ api.RepoName, _ api.CommitID, name string) ([]byte, error) { if name != wantPath { diff --git a/cmd/frontend/graphqlbackend/org_invitations.go b/cmd/frontend/graphqlbackend/org_invitations.go index 720a5e80a801..4e83295caa9e 100644 --- a/cmd/frontend/graphqlbackend/org_invitations.go +++ b/cmd/frontend/graphqlbackend/org_invitations.go @@ -15,7 +15,6 @@ import ( "github.com/graph-gophers/graphql-go/relay" "github.com/sourcegraph/sourcegraph/cmd/frontend/envvar" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" sgactor "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/auth" "github.com/sourcegraph/sourcegraph/internal/authz/permssync" @@ -453,7 +452,12 @@ func orgInvitationURLLegacy(org *types.Org, relative bool) string { if relative { return path } - return globals.ExternalURL().ResolveReference(&url.URL{Path: path}).String() + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return "" + } + + return externalURL.ResolveReference(&url.URL{Path: path}).String() } func orgInvitationURL(invitation database.OrgInvitation, relative bool) (string, error) { @@ -468,7 +472,12 @@ func orgInvitationURL(invitation database.OrgInvitation, relative bool) (string, if relative { return path, nil } - return globals.ExternalURL().ResolveReference(&url.URL{Path: path}).String(), nil + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return "", err + } + + return externalURL.ResolveReference(&url.URL{Path: path}).String(), nil } func createInvitationJWT(orgID int32, invitationID int64, senderID int32, expiryTime time.Time) (string, error) { @@ -479,7 +488,7 @@ func createInvitationJWT(orgID int32, invitationID int64, senderID int32, expiry token := jwt.NewWithClaims(jwt.SigningMethodHS512, &orgInvitationClaims{ RegisteredClaims: jwt.RegisteredClaims{ - Issuer: globals.ExternalURL().String(), + Issuer: conf.Get().ExternalURL, ExpiresAt: jwt.NewNumericDate(expiryTime), Subject: strconv.FormatInt(int64(orgID), 10), }, diff --git a/cmd/frontend/graphqlbackend/org_invitations_test.go b/cmd/frontend/graphqlbackend/org_invitations_test.go index a006f82ea04c..a89a229a239c 100644 --- a/cmd/frontend/graphqlbackend/org_invitations_test.go +++ b/cmd/frontend/graphqlbackend/org_invitations_test.go @@ -38,6 +38,7 @@ func mockSiteConfigSigningKey(withEmails *bool) string { signingKey := "Zm9v" siteConfig := schema.SiteConfiguration{ + ExternalURL: "http://example.com", OrganizationInvitations: &schema.OrganizationInvitations{ SigningKey: signingKey, }, @@ -165,6 +166,12 @@ func TestInviteUserToOrganization(t *testing.T) { defer func() { timeNow = time.Now }() + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) users := dbmocks.NewMockUserStore() users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{ID: 1}, nil) users.GetByUsernameFunc.SetDefaultReturn(&types.User{ID: 2, Username: "foo"}, nil) diff --git a/cmd/frontend/graphqlbackend/users_create_test.go b/cmd/frontend/graphqlbackend/users_create_test.go index 723dd3cb241f..7dd0ee9707cc 100644 --- a/cmd/frontend/graphqlbackend/users_create_test.go +++ b/cmd/frontend/graphqlbackend/users_create_test.go @@ -95,7 +95,8 @@ func TestCreateUserResetPasswordURL(t *testing.T) { conf.Mock(&conf.Unified{ SiteConfiguration: schema.SiteConfiguration{ - EmailSmtp: nil, + ExternalURL: "http://example.com", + EmailSmtp: nil, }, }) t.Cleanup(func() { conf.Mock(nil) }) @@ -132,7 +133,8 @@ func TestCreateUserResetPasswordURL(t *testing.T) { t.Run("with SMTP enabled", func(t *testing.T) { conf.Mock(&conf.Unified{ SiteConfiguration: schema.SiteConfiguration{ - EmailSmtp: &schema.SMTPServerConfig{}, + ExternalURL: "http://example.com", + EmailSmtp: &schema.SMTPServerConfig{}, }, }) @@ -186,7 +188,8 @@ func TestCreateUserResetPasswordURL(t *testing.T) { t.Run("with SMTP enabled, without verifiedEmail", func(t *testing.T) { conf.Mock(&conf.Unified{ SiteConfiguration: schema.SiteConfiguration{ - EmailSmtp: &schema.SMTPServerConfig{}, + EmailSmtp: &schema.SMTPServerConfig{}, + ExternalURL: "http://example.com", }, }) diff --git a/cmd/frontend/graphqlbackend/users_randomize_password.go b/cmd/frontend/graphqlbackend/users_randomize_password.go index 24ae1e88139d..18e9d5453dd1 100644 --- a/cmd/frontend/graphqlbackend/users_randomize_password.go +++ b/cmd/frontend/graphqlbackend/users_randomize_password.go @@ -10,7 +10,6 @@ import ( "github.com/sourcegraph/sourcegraph/cmd/frontend/backend" "github.com/sourcegraph/sourcegraph/cmd/frontend/envvar" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/internal/auth" "github.com/sourcegraph/sourcegraph/internal/auth/userpasswd" "github.com/sourcegraph/sourcegraph/internal/conf" @@ -27,7 +26,13 @@ func (r *randomizeUserPasswordResult) ResetPasswordURL() *string { if r.resetURL == nil { return nil } - urlStr := globals.ExternalURL().ResolveReference(r.resetURL).String() + + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return nil + } + + urlStr := externalURL.ResolveReference(r.resetURL).String() return &urlStr } diff --git a/cmd/frontend/graphqlbackend/users_randomize_password_test.go b/cmd/frontend/graphqlbackend/users_randomize_password_test.go index 57abcb9a5a1f..f7d06e7f1882 100644 --- a/cmd/frontend/graphqlbackend/users_randomize_password_test.go +++ b/cmd/frontend/graphqlbackend/users_randomize_password_test.go @@ -24,11 +24,15 @@ func TestRandomizeUserPassword(t *testing.T) { SiteConfiguration: schema.SiteConfiguration{ AuthProviders: []schema.AuthProviders{{Builtin: &schema.BuiltinAuthProvider{}}}, EmailSmtp: &schema.SMTPServerConfig{}, - }} + ExternalURL: "http://example.com", + }, + } smtpDisabledConf = &conf.Unified{ SiteConfiguration: schema.SiteConfiguration{ AuthProviders: []schema.AuthProviders{{Builtin: &schema.BuiltinAuthProvider{}}}, - }} + ExternalURL: "http://example.com", + }, + } ) db := dbmocks.NewMockDB() diff --git a/cmd/frontend/internal/app/app.go b/cmd/frontend/internal/app/app.go index 117c44c71e35..0c400ee190c8 100644 --- a/cmd/frontend/internal/app/app.go +++ b/cmd/frontend/internal/app/app.go @@ -2,10 +2,10 @@ package app import ( "net/http" + "net/url" "github.com/sourcegraph/log" - "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" @@ -33,7 +33,13 @@ func NewHandler(db database.DB, logger log.Logger, githubAppSetupHandler http.Ha // Sourcegraph using Safari/WebKit. return false } - return globals.ExternalURL().Scheme == "https" + + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return false + } + + return externalURL.Scheme == "https" })) logger = logger.Scoped("appHandler", "handles routes for all app related requests") diff --git a/cmd/frontend/internal/app/jscontext/jscontext.go b/cmd/frontend/internal/app/jscontext/jscontext.go index bf956eafaab4..cc37f27a1758 100644 --- a/cmd/frontend/internal/app/jscontext/jscontext.go +++ b/cmd/frontend/internal/app/jscontext/jscontext.go @@ -5,6 +5,7 @@ package jscontext import ( "context" "net/http" + "net/url" "runtime" "strings" @@ -232,8 +233,16 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext { ctx := req.Context() a := sgactor.FromContext(ctx) + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + externalURL = &url.URL{ + Scheme: "http", + Host: "example.com", + } + } + headers := make(map[string]string) - headers["x-sourcegraph-client"] = globals.ExternalURL().String() + headers["x-sourcegraph-client"] = externalURL.String() headers["X-Requested-With"] = "Sourcegraph" // required for httpapi to use cookie auth // Propagate Cache-Control no-cache and max-age=0 directives @@ -324,7 +333,7 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext { // authentication above, but do not include e.g. hard-coded secrets about // the server instance here as they would be sent to anonymous users. return JSContext{ - ExternalURL: globals.ExternalURL().String(), + ExternalURL: externalURL.String(), XHRHeaders: headers, UserAgentIsBot: isBot(req.UserAgent()), AssetsRoot: assetsutil.URL("").String(), diff --git a/cmd/frontend/internal/app/opensearch.go b/cmd/frontend/internal/app/opensearch.go index 784e382dd411..e6382ca40eca 100644 --- a/cmd/frontend/internal/app/opensearch.go +++ b/cmd/frontend/internal/app/opensearch.go @@ -4,10 +4,11 @@ import ( "bytes" "html/template" "net/http" + "net/url" "github.com/inconshreveable/log15" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" + "github.com/sourcegraph/sourcegraph/internal/conf" ) var openSearchDescription = template.Must(template.New("").Parse(` @@ -26,7 +27,11 @@ func openSearch(w http.ResponseWriter, r *http.Request) { BaseURL string SearchURL string } - externalURL := globals.ExternalURL() + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + log15.Error("Failed to parse External URL", "err", err) + return + } externalURLStr := externalURL.String() data := vars{ BaseURL: externalURLStr, diff --git a/cmd/frontend/internal/auth/oauth/BUILD.bazel b/cmd/frontend/internal/auth/oauth/BUILD.bazel index 28da187c08b4..a1b1d01eeae0 100644 --- a/cmd/frontend/internal/auth/oauth/BUILD.bazel +++ b/cmd/frontend/internal/auth/oauth/BUILD.bazel @@ -15,7 +15,6 @@ go_library( deps = [ "//cmd/frontend/auth", "//cmd/frontend/external/session", - "//cmd/frontend/globals", "//internal/actor", "//internal/auth/providers", "//internal/conf", @@ -45,4 +44,8 @@ go_test( timeout = "short", srcs = ["provider_test.go"], embed = [":oauth"], + deps = [ + "//internal/conf", + "//schema", + ], ) diff --git a/cmd/frontend/internal/auth/oauth/provider.go b/cmd/frontend/internal/auth/oauth/provider.go index ae3e4dcf17a8..b148d6b4c13b 100644 --- a/cmd/frontend/internal/auth/oauth/provider.go +++ b/cmd/frontend/internal/auth/oauth/provider.go @@ -14,8 +14,8 @@ import ( "github.com/inconshreveable/log15" "golang.org/x/oauth2" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/internal/auth/providers" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/extsvc" "github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops" "github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud" @@ -256,8 +256,12 @@ func canRedirect(redirect string) bool { if err != nil { return false } + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return conf.Get().ExperimentalFeatures.InsightsAlternateLoadingStrategy + } // if we have a non-relative url, make sure it's the same host as the sourcegraph instance - if redirectURL.Host != "" && redirectURL.Host != globals.ExternalURL().Host { + if redirectURL.Host != "" && redirectURL.Host != externalURL.Host { return false } // TODO: do we want to exclude any internal paths here? diff --git a/cmd/frontend/internal/auth/oauth/provider_test.go b/cmd/frontend/internal/auth/oauth/provider_test.go index 34ac98b05303..a9af39b07a47 100644 --- a/cmd/frontend/internal/auth/oauth/provider_test.go +++ b/cmd/frontend/internal/auth/oauth/provider_test.go @@ -3,9 +3,19 @@ package oauth import ( "net/url" "testing" + + "github.com/sourcegraph/sourcegraph/internal/conf" + "github.com/sourcegraph/sourcegraph/schema" ) func TestCanRedirect(t *testing.T) { + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) + tc := map[string]bool{ "https://evilhost.com/nasty-stuff": false, "/search?foo=bar": true, diff --git a/cmd/frontend/internal/auth/openidconnect/BUILD.bazel b/cmd/frontend/internal/auth/openidconnect/BUILD.bazel index 67136a8547f7..bf88984db2b0 100644 --- a/cmd/frontend/internal/auth/openidconnect/BUILD.bazel +++ b/cmd/frontend/internal/auth/openidconnect/BUILD.bazel @@ -14,7 +14,6 @@ go_library( visibility = ["//cmd/frontend:__subpackages__"], deps = [ "//cmd/frontend/auth", - "//cmd/frontend/external/globals", "//cmd/frontend/external/session", "//cmd/frontend/hubspot", "//cmd/frontend/hubspot/hubspotutil", diff --git a/cmd/frontend/internal/auth/openidconnect/middleware_test.go b/cmd/frontend/internal/auth/openidconnect/middleware_test.go index ccc368184ec1..6799b149d253 100644 --- a/cmd/frontend/internal/auth/openidconnect/middleware_test.go +++ b/cmd/frontend/internal/auth/openidconnect/middleware_test.go @@ -20,6 +20,7 @@ import ( "github.com/sourcegraph/sourcegraph/cmd/frontend/external/session" "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/auth/providers" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/database/dbmocks" "github.com/sourcegraph/sourcegraph/internal/licensing" @@ -144,6 +145,13 @@ func TestMiddleware(t *testing.T) { return &types.User{ID: id, CreatedAt: time.Now()}, nil }) + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) + db := dbmocks.NewStrictMockDB() db.UsersFunc.SetDefaultReturn(users) @@ -324,6 +332,13 @@ func TestMiddleware_NoOpenRedirect(t *testing.T) { defer licensing.TestingSkipFeatureChecks()() + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) + mockGetProviderValue = &Provider{ config: schema.OpenIDConnectAuthProvider{ ClientID: testClientID, diff --git a/cmd/frontend/internal/auth/openidconnect/provider.go b/cmd/frontend/internal/auth/openidconnect/provider.go index 115a9ff1dbab..899d65716243 100644 --- a/cmd/frontend/internal/auth/openidconnect/provider.go +++ b/cmd/frontend/internal/auth/openidconnect/provider.go @@ -11,8 +11,8 @@ import ( "github.com/coreos/go-oidc" "golang.org/x/oauth2" - "github.com/sourcegraph/sourcegraph/cmd/frontend/external/globals" "github.com/sourcegraph/sourcegraph/internal/auth/providers" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/extsvc" "github.com/sourcegraph/sourcegraph/internal/httpcli" "github.com/sourcegraph/sourcegraph/lib/errors" @@ -120,6 +120,11 @@ func (p *Provider) CachedInfo() *providers.Info { // oauth2Config constructs and returns an *oauth2.Config from the provider. func (p *Provider) oauth2Config() *oauth2.Config { + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return nil + } + return &oauth2.Config{ ClientID: p.config.ClientID, ClientSecret: p.config.ClientSecret, @@ -127,7 +132,7 @@ func (p *Provider) oauth2Config() *oauth2.Config { // It would be nice if this was "/.auth/openidconnect/callback" not "/.auth/callback", but // many instances have the "/.auth/callback" value hardcoded in their external auth // provider, so we can't change it easily - RedirectURL: globals.ExternalURL(). + RedirectURL: externalURL. ResolveReference(&url.URL{Path: p.callbackUrl}). String(), diff --git a/cmd/frontend/internal/auth/sourcegraphoperator/middleware_test.go b/cmd/frontend/internal/auth/sourcegraphoperator/middleware_test.go index 65381b38c56f..7e2a2d13af07 100644 --- a/cmd/frontend/internal/auth/sourcegraphoperator/middleware_test.go +++ b/cmd/frontend/internal/auth/sourcegraphoperator/middleware_test.go @@ -25,11 +25,13 @@ import ( internalauth "github.com/sourcegraph/sourcegraph/internal/auth" "github.com/sourcegraph/sourcegraph/internal/auth/providers" "github.com/sourcegraph/sourcegraph/internal/cloud" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/database/dbmocks" "github.com/sourcegraph/sourcegraph/internal/extsvc" "github.com/sourcegraph/sourcegraph/internal/types" "github.com/sourcegraph/sourcegraph/lib/errors" + "github.com/sourcegraph/sourcegraph/schema" ) const ( @@ -187,6 +189,13 @@ func TestMiddleware(t *testing.T) { providers.MockProviders = []providers.Provider{mockProvider} defer func() { providers.MockProviders = nil }() + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) + t.Run("refresh", func(t *testing.T) { err := mockProvider.Refresh(context.Background()) require.NoError(t, err) diff --git a/cmd/frontend/internal/batches/resolvers/BUILD.bazel b/cmd/frontend/internal/batches/resolvers/BUILD.bazel index bb4c986e62b4..26ff77dd5928 100644 --- a/cmd/frontend/internal/batches/resolvers/BUILD.bazel +++ b/cmd/frontend/internal/batches/resolvers/BUILD.bazel @@ -40,7 +40,6 @@ go_library( visibility = ["//cmd/frontend:__subpackages__"], deps = [ "//cmd/frontend/enterprise", - "//cmd/frontend/globals", "//cmd/frontend/graphqlbackend", "//cmd/frontend/graphqlbackend/externallink", "//cmd/frontend/graphqlbackend/graphqlutil", @@ -128,7 +127,6 @@ go_test( ], deps = [ "//cmd/frontend/backend", - "//cmd/frontend/globals", "//cmd/frontend/graphqlbackend", "//cmd/frontend/graphqlbackend/externallink", "//cmd/frontend/internal/auth/githubappauth", diff --git a/cmd/frontend/internal/batches/resolvers/credential.go b/cmd/frontend/internal/batches/resolvers/credential.go index 22d8ea622e81..f7632a55323e 100644 --- a/cmd/frontend/internal/batches/resolvers/credential.go +++ b/cmd/frontend/internal/batches/resolvers/credential.go @@ -3,15 +3,16 @@ package resolvers import ( "context" "fmt" + "net/url" "strconv" "strings" "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" btypes "github.com/sourcegraph/sourcegraph/internal/batches/types" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/extsvc" "github.com/sourcegraph/sourcegraph/internal/extsvc/auth" @@ -61,7 +62,11 @@ func unmarshalBatchChangesCredentialID(id graphql.ID) (credentialID int64, isSit } func commentSSHKey(ssh auth.AuthenticatorWithSSH) string { - url := globals.ExternalURL() + url, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return "" + } + if url != nil && url.Host != "" { return strings.TrimRight(ssh.SSHPublicKey(), "\n") + " Sourcegraph " + url.Host } diff --git a/cmd/frontend/internal/batches/resolvers/credential_test.go b/cmd/frontend/internal/batches/resolvers/credential_test.go index 1a5f08ce315c..261f05832537 100644 --- a/cmd/frontend/internal/batches/resolvers/credential_test.go +++ b/cmd/frontend/internal/batches/resolvers/credential_test.go @@ -1,13 +1,15 @@ package resolvers import ( + "net/url" "testing" "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/extsvc/auth" + "github.com/sourcegraph/sourcegraph/schema" ) func TestUnmarshalBatchChangesCredentialID(t *testing.T) { @@ -65,9 +67,21 @@ func TestUnmarshalBatchChangesCredentialID(t *testing.T) { } func TestCommentSSHKey(t *testing.T) { + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "https://www.sourcegraph.com", + }, + }) + defer conf.Mock(nil) + + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + t.Errorf("could not parse external url: %v", err) + } + publicKey := "public\n" sshKey := commentSSHKey(&auth.BasicAuthWithSSH{BasicAuth: auth.BasicAuth{Username: "foo", Password: "bar"}, PrivateKey: "private", PublicKey: publicKey, Passphrase: "pass"}) - expectedKey := "public Sourcegraph " + globals.ExternalURL().Host + expectedKey := "public Sourcegraph " + externalURL.Host if sshKey != expectedKey { t.Errorf("found wrong ssh key: want=%q, have=%q", expectedKey, sshKey) diff --git a/cmd/frontend/internal/bg/BUILD.bazel b/cmd/frontend/internal/bg/BUILD.bazel index e4e4ea76fedf..456f7f7c35ec 100644 --- a/cmd/frontend/internal/bg/BUILD.bazel +++ b/cmd/frontend/internal/bg/BUILD.bazel @@ -14,7 +14,6 @@ go_library( importpath = "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/bg", visibility = ["//cmd/frontend:__subpackages__"], deps = [ - "//cmd/frontend/globals", "//internal/auth/userpasswd", "//internal/collections", "//internal/conf", diff --git a/cmd/frontend/internal/bg/app_ready.go b/cmd/frontend/internal/bg/app_ready.go index 8befad2d3e34..2199f4714e99 100644 --- a/cmd/frontend/internal/bg/app_ready.go +++ b/cmd/frontend/internal/bg/app_ready.go @@ -3,14 +3,15 @@ package bg import ( "context" "fmt" + "net/url" "os" "strings" "github.com/fatih/color" "github.com/sourcegraph/log" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/internal/auth/userpasswd" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/conf/deploy" "github.com/sourcegraph/sourcegraph/internal/database" ) @@ -26,7 +27,11 @@ func AppReady(db database.DB, logger log.Logger) { // Our goal is to open the browser to a special sign-in URL containing a in-memory // secret (signInURL). - displayURL := globals.ExternalURL().String() + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + logger.Error("failed to parse external url", log.Error(err)) + } + displayURL := externalURL.String() browserURL := displayURL if signInURL, err := userpasswd.AppSiteInit(ctx, logger, db); err != nil { diff --git a/cmd/frontend/internal/cli/middleware/BUILD.bazel b/cmd/frontend/internal/cli/middleware/BUILD.bazel index 8bbafa4b185d..86bdbf38a67f 100644 --- a/cmd/frontend/internal/cli/middleware/BUILD.bazel +++ b/cmd/frontend/internal/cli/middleware/BUILD.bazel @@ -15,11 +15,11 @@ go_library( visibility = ["//cmd/frontend:__subpackages__"], deps = [ "//cmd/frontend/envvar", - "//cmd/frontend/globals", "//cmd/frontend/internal/app/router", "//cmd/frontend/internal/app/ui", "//cmd/frontend/internal/app/ui/router", "//internal/actor", + "//internal/conf", "//internal/env", "//internal/trace", "//lib/errors", @@ -31,5 +31,9 @@ go_test( name = "middleware_test", timeout = "short", srcs = ["goimportpath_test.go"], - deps = [":middleware"], + deps = [ + ":middleware", + "//internal/conf", + "//schema", + ], ) diff --git a/cmd/frontend/internal/cli/middleware/goimportpath.go b/cmd/frontend/internal/cli/middleware/goimportpath.go index 62039bc24921..1ed9a7f09e36 100644 --- a/cmd/frontend/internal/cli/middleware/goimportpath.go +++ b/cmd/frontend/internal/cli/middleware/goimportpath.go @@ -4,10 +4,11 @@ import ( "html/template" "log" "net/http" + "net/url" "path" "strings" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/trace" "github.com/sourcegraph/sourcegraph/lib/errors" ) @@ -61,12 +62,19 @@ func SourcegraphComGoGetHandler(next http.Handler) http.Handler { // It's a vanity import path that maps to "github.com/{sourcegraph,sqs}/*" clone URLs. pathElements := strings.Split(req.URL.Path[1:], "/") if len(pathElements) >= 2 && (pathElements[0] == "sourcegraph" || pathElements[0] == "sqs") { - host := globals.ExternalURL().Host + host := "" + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + log.Println("error parsing external url:", err) + } + if externalURL != nil { + host = externalURL.Host + } user := pathElements[0] repo := pathElements[1] - err := goImportMetaTagTemplate.Execute(w, goImportMetaTag{ + err = goImportMetaTagTemplate.Execute(w, goImportMetaTag{ ImportPrefix: path.Join(host, user, repo), VCS: "git", RepoRoot: "https://github.com/" + user + "/" + repo, diff --git a/cmd/frontend/internal/cli/middleware/goimportpath_test.go b/cmd/frontend/internal/cli/middleware/goimportpath_test.go index 8e162b1522a5..6ff3a9532d53 100644 --- a/cmd/frontend/internal/cli/middleware/goimportpath_test.go +++ b/cmd/frontend/internal/cli/middleware/goimportpath_test.go @@ -7,9 +7,18 @@ import ( "testing" "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/cli/middleware" + "github.com/sourcegraph/sourcegraph/internal/conf" + "github.com/sourcegraph/sourcegraph/schema" ) func TestGoImportPath(t *testing.T) { + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) + tests := []struct { path string wantStatus int diff --git a/cmd/frontend/internal/cli/serve_cmd.go b/cmd/frontend/internal/cli/serve_cmd.go index f37b2e4145dc..0dfa10a04b29 100644 --- a/cmd/frontend/internal/cli/serve_cmd.go +++ b/cmd/frontend/internal/cli/serve_cmd.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "net/http" + "net/url" "os" "time" @@ -206,7 +207,6 @@ func Main(ctx context.Context, observationCtx *observation.Context, ready servic } globals.WatchBranding() - globals.WatchExternalURL() globals.WatchPermissionsUserMapping() goroutine.Go(func() { bg.CheckRedisCacheEvictionPolicy() }) @@ -253,7 +253,11 @@ func Main(ctx context.Context, observationCtx *observation.Context, ready servic // This is not a log entry and is usually disabled println(fmt.Sprintf("\n\n%s\n\n", logoColor)) } - logger.Info(fmt.Sprintf("✱ Sourcegraph is ready at: %s", globals.ExternalURL())) + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return err + } + logger.Info(fmt.Sprintf("✱ Sourcegraph is ready at: %s", externalURL)) ready() // We only want to run this task once Sourcegraph is ready to serve user requests. diff --git a/cmd/frontend/internal/repos/webhooks/BUILD.bazel b/cmd/frontend/internal/repos/webhooks/BUILD.bazel index 2ac53d2a5b1b..367a8368b68d 100644 --- a/cmd/frontend/internal/repos/webhooks/BUILD.bazel +++ b/cmd/frontend/internal/repos/webhooks/BUILD.bazel @@ -38,9 +38,9 @@ go_test( "requires-network", ], deps = [ - "//cmd/frontend/external/globals", "//cmd/frontend/webhooks", "//internal/api", + "//internal/conf", "//internal/database", "//internal/database/dbmocks", "//internal/database/dbtest", diff --git a/cmd/frontend/internal/repos/webhooks/handlers_test.go b/cmd/frontend/internal/repos/webhooks/handlers_test.go index 666c98b8102c..879d9daf90bd 100644 --- a/cmd/frontend/internal/repos/webhooks/handlers_test.go +++ b/cmd/frontend/internal/repos/webhooks/handlers_test.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "os" "path/filepath" "testing" @@ -21,9 +22,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/sourcegraph/sourcegraph/cmd/frontend/external/globals" "github.com/sourcegraph/sourcegraph/cmd/frontend/webhooks" "github.com/sourcegraph/sourcegraph/internal/api" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/database/dbmocks" "github.com/sourcegraph/sourcegraph/internal/database/dbtest" @@ -61,6 +62,13 @@ func TestGitHubHandler(t *testing.T) { repoStore := store.RepoStore() esStore := store.ExternalServiceStore() + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "https://www.sourcegraph.com", + }, + }) + defer conf.Mock(nil) + repo := &types.Repo{ ID: 1, Name: "ghe.sgdev.org/milton/test", @@ -166,7 +174,12 @@ func TestGitHubHandler(t *testing.T) { t.Fatal(err) } - targetURL := fmt.Sprintf("%s/github-webhooks", globals.ExternalURL()) + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + t.Fatal(err) + } + + targetURL := fmt.Sprintf("%s/github-webhooks", externalURL) req, err := http.NewRequest("POST", targetURL, bytes.NewReader(payload)) if err != nil { t.Fatal(err) diff --git a/cmd/repo-updater/shared/main.go b/cmd/repo-updater/shared/main.go index 75d0cb597957..72976eb13f3c 100644 --- a/cmd/repo-updater/shared/main.go +++ b/cmd/repo-updater/shared/main.go @@ -8,6 +8,7 @@ import ( "html/template" "net" "net/http" + "net/url" "os" "strconv" "time" @@ -126,7 +127,6 @@ func Main(ctx context.Context, observationCtx *observation.Context, ready servic server.ChangesetSyncRegistry = syncRegistry } - go globals.WatchExternalURL() go watchAuthzProviders(ctx, db) go watchSyncer(ctx, logger, syncer, updateScheduler, server.ChangesetSyncRegistry) @@ -331,6 +331,12 @@ func listAuthzProvidersHandler() http.HandlerFunc { ExternalServiceURL string `json:"external_service_url"` } + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + http.Error(w, "failed to parse external url: "+err.Error(), http.StatusInternalServerError) + return + } + _, providers := ossAuthz.GetProviders() infos := make([]providerInfo, len(providers)) for i, p := range providers { @@ -341,7 +347,7 @@ func listAuthzProvidersHandler() http.HandlerFunc { infos[i] = providerInfo{ ServiceType: p.ServiceType(), ServiceID: p.ServiceID(), - ExternalServiceURL: fmt.Sprintf("%s/site-admin/external-services/%s", globals.ExternalURL(), relay.MarshalID("ExternalService", id)), + ExternalServiceURL: fmt.Sprintf("%s/site-admin/external-services/%s", externalURL, relay.MarshalID("ExternalService", id)), } } diff --git a/internal/auth/userpasswd/BUILD.bazel b/internal/auth/userpasswd/BUILD.bazel index d0353ae41d3d..53e29ec6c7bf 100644 --- a/internal/auth/userpasswd/BUILD.bazel +++ b/internal/auth/userpasswd/BUILD.bazel @@ -21,7 +21,6 @@ go_library( visibility = ["//:__subpackages__"], deps = [ "//cmd/frontend/backend", - "//cmd/frontend/globals", "//cmd/frontend/hubspot", "//cmd/frontend/hubspot/hubspotutil", "//internal/actor", diff --git a/internal/auth/userpasswd/app.go b/internal/auth/userpasswd/app.go index 9cee619fb104..5278af620c32 100644 --- a/internal/auth/userpasswd/app.go +++ b/internal/auth/userpasswd/app.go @@ -12,9 +12,9 @@ import ( "github.com/sourcegraph/log" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" sgactor "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/apptoken" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/conf/deploy" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/env" @@ -166,7 +166,7 @@ func generatePassword() (string, error) { } func appSignInURL() string { - externalURL := globals.ExternalURL().String() + externalURL := conf.Get().ExternalURL u, err := url.Parse(externalURL) if err != nil { return externalURL diff --git a/internal/auth/userpasswd/lockout.go b/internal/auth/userpasswd/lockout.go index 1cbb2dcfa7a3..4bc38b0c9eb4 100644 --- a/internal/auth/userpasswd/lockout.go +++ b/internal/auth/userpasswd/lockout.go @@ -10,7 +10,6 @@ import ( "github.com/golang-jwt/jwt/v4" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/rcache" "github.com/sourcegraph/sourcegraph/internal/txemail" @@ -121,9 +120,19 @@ func (s *lockoutStore) GenerateUnlockAccountURL(userID int32) (string, string, e effectiveTTL := effectiveUnlockTTL(ttl) expiryTime := time.Now().Add(time.Second * time.Duration(effectiveTTL)) + u := conf.Get().ExternalURL + if u == "" { + u = "http://example.com" + } + + externalURL, err := url.Parse(u) + if err != nil { + return "", "", err + } + token := jwt.NewWithClaims(jwt.SigningMethodHS512, &unlockAccountClaims{ RegisteredClaims: jwt.RegisteredClaims{ - Issuer: globals.ExternalURL().String(), + Issuer: externalURL.String(), ExpiresAt: jwt.NewNumericDate(expiryTime), Subject: strconv.FormatInt(int64(userID), 10), }, @@ -144,7 +153,7 @@ func (s *lockoutStore) GenerateUnlockAccountURL(userID int32) (string, string, e path := fmt.Sprintf("/unlock-account/%s", tokenString) - return globals.ExternalURL().ResolveReference(&url.URL{Path: path}).String(), tokenString, nil + return externalURL.ResolveReference(&url.URL{Path: path}).String(), tokenString, nil } // take site config link expiry into account as well when setting unlock expiry diff --git a/internal/auth/userpasswd/lockout_test.go b/internal/auth/userpasswd/lockout_test.go index 3be0e2048873..46ee5088d0b2 100644 --- a/internal/auth/userpasswd/lockout_test.go +++ b/internal/auth/userpasswd/lockout_test.go @@ -40,6 +40,13 @@ func TestLockoutStore(t *testing.T) { t.Skip() } + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) + t.Run("explicit reset", func(t *testing.T) { rcache.SetupForTest(t) @@ -105,7 +112,6 @@ func TestLockoutStore(t *testing.T) { assert.EqualError(t, err, `signing key not provided, cannot validate JWT on unlock account URL. Please add "auth.unlockAccountLinkSigningKey" to site configuration.`) assert.Empty(t, path) - }) t.Run("generates an account unlock url", func(t *testing.T) { @@ -122,7 +128,6 @@ func TestLockoutStore(t *testing.T) { assert.Empty(t, err) assert.Contains(t, path, "http://example.com/unlock-account") - }) t.Run("generates an expected jwt token", func(t *testing.T) { @@ -146,7 +151,6 @@ func TestLockoutStore(t *testing.T) { return base64.StdEncoding.DecodeString(signingKey) }) - if err != nil { t.Fatal(err) } @@ -194,7 +198,6 @@ func TestLockoutStore(t *testing.T) { if !valid { t.Fatalf("provided token is invalid") } - }) t.Run("fails verification on unlock account token", func(t *testing.T) { diff --git a/internal/auth/userpasswd/reset_password.go b/internal/auth/userpasswd/reset_password.go index 2e9385f5c2f7..022d38ebe13f 100644 --- a/internal/auth/userpasswd/reset_password.go +++ b/internal/auth/userpasswd/reset_password.go @@ -10,7 +10,6 @@ import ( "github.com/sourcegraph/log" "github.com/sourcegraph/sourcegraph/cmd/frontend/backend" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/cookie" @@ -26,13 +25,18 @@ func SendResetPasswordURLEmail(ctx context.Context, email, username string, rese emailTemplate = txemail.FromSiteConfigTemplateWithDefault(customTemplates.ResetPassword, emailTemplate) } + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return err + } + return txemail.Send(ctx, "password_reset", txemail.Message{ To: []string{email}, Template: emailTemplate, Data: SetPasswordEmailTemplateData{ Username: username, - URL: globals.ExternalURL().ResolveReference(resetURL).String(), - Host: globals.ExternalURL().Host, + URL: externalURL.ResolveReference(resetURL).String(), + Host: externalURL.Host, }, }) } diff --git a/internal/auth/userpasswd/set_password.go b/internal/auth/userpasswd/set_password.go index c652284299cc..a94317262114 100644 --- a/internal/auth/userpasswd/set_password.go +++ b/internal/auth/userpasswd/set_password.go @@ -2,9 +2,9 @@ package userpasswd import ( "context" + "net/url" "github.com/sourcegraph/sourcegraph/cmd/frontend/backend" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/txemail" @@ -24,7 +24,12 @@ func HandleSetPasswordEmail(ctx context.Context, db database.DB, id int32, usern return "", errors.Wrap(err, "make password reset URL") } - shareableResetURL := globals.ExternalURL().ResolveReference(resetURL).String() + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + return "", errors.Wrap(err, "parse external URL") + } + + shareableResetURL := externalURL.ResolveReference(resetURL).String() emailedResetURL := shareableResetURL if !emailVerified { @@ -32,7 +37,7 @@ func HandleSetPasswordEmail(ctx context.Context, db database.DB, id int32, usern if err != nil { return shareableResetURL, errors.Wrap(err, "attach email verification") } - emailedResetURL = globals.ExternalURL().ResolveReference(newURL).String() + emailedResetURL = externalURL.ResolveReference(newURL).String() } // Configure the template @@ -47,7 +52,7 @@ func HandleSetPasswordEmail(ctx context.Context, db database.DB, id int32, usern Data: SetPasswordEmailTemplateData{ Username: username, URL: emailedResetURL, - Host: globals.ExternalURL().Host, + Host: externalURL.Host, }, }); err != nil { return shareableResetURL, err diff --git a/internal/auth/userpasswd/set_password_test.go b/internal/auth/userpasswd/set_password_test.go index b19f346c11a4..6f6b98594650 100644 --- a/internal/auth/userpasswd/set_password_test.go +++ b/internal/auth/userpasswd/set_password_test.go @@ -12,8 +12,10 @@ import ( "github.com/sourcegraph/sourcegraph/cmd/frontend/backend" "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database/dbmocks" "github.com/sourcegraph/sourcegraph/internal/txemail" + "github.com/sourcegraph/sourcegraph/schema" ) func TestHandleSetPasswordEmail(t *testing.T) { @@ -22,6 +24,13 @@ func TestHandleSetPasswordEmail(t *testing.T) { defer func() { backend.MockMakePasswordResetURL = nil }() + conf.Mock(&conf.Unified{ + SiteConfiguration: schema.SiteConfiguration{ + ExternalURL: "http://example.com", + }, + }) + defer conf.Mock(nil) + backend.MockMakePasswordResetURL = func(context.Context, int32) (*url.URL, error) { query := url.Values{} query.Set("userID", "1") diff --git a/internal/conf/computed.go b/internal/conf/computed.go index 3494739553fa..47f253804d3f 100644 --- a/internal/conf/computed.go +++ b/internal/conf/computed.go @@ -3,6 +3,7 @@ package conf import ( "encoding/hex" "log" + "net/url" "strings" "time" @@ -26,9 +27,22 @@ func init() { log.Fatalf("The 'DEPLOY_TYPE' environment variable is invalid. Expected one of: %q, %q, %q, %q, %q, %q, %q. Got: %q", deploy.Kubernetes, deploy.DockerCompose, deploy.PureDocker, deploy.SingleDocker, deploy.Dev, deploy.Helm, deploy.App, deployType) } + ContributeValidator(validateExternalURLConfig) confdefaults.Default = defaultConfigForDeployment() } +func validateExternalURLConfig(cfg conftypes.SiteConfigQuerier) (problems Problems) { + if val := cfg.SiteConfig().ExternalURL; val != "" { + eURL, err := url.Parse(val) + if err != nil { + problems = append(problems, NewSiteProblem("Could not parse `externalURL`.")) + } else if eURL.Path != "/" && eURL.Path != "" { + problems = append(problems, NewSiteProblem("externalURL must not be a non-root URL.")) + } + } + return problems +} + func defaultConfigForDeployment() conftypes.RawUnified { deployType := deploy.Type() switch { diff --git a/internal/deviceid/BUILD.bazel b/internal/deviceid/BUILD.bazel index e6e1f6159133..3bb904d4b111 100644 --- a/internal/deviceid/BUILD.bazel +++ b/internal/deviceid/BUILD.bazel @@ -6,7 +6,7 @@ go_library( importpath = "github.com/sourcegraph/sourcegraph/internal/deviceid", visibility = ["//:__subpackages__"], deps = [ - "//cmd/frontend/globals", + "//internal/conf", "//internal/cookie", "@com_github_google_uuid//:uuid", ], diff --git a/internal/deviceid/middleware.go b/internal/deviceid/middleware.go index 30768f958012..d2ee73e8812f 100644 --- a/internal/deviceid/middleware.go +++ b/internal/deviceid/middleware.go @@ -3,11 +3,12 @@ package deviceid import ( "context" "net/http" + "net/url" "time" "github.com/google/uuid" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" + "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/cookie" ) @@ -17,12 +18,18 @@ func Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Vary", "Cookie") if _, ok := cookie.DeviceID(r); !ok { + + externalURL, err := url.Parse(conf.Get().ExternalURL) + if err != nil { + externalURL = &url.URL{Scheme: "http", Host: "example.com"} + } + newDeviceId, _ := uuid.NewRandom() http.SetCookie(w, &http.Cookie{ Name: "sourcegraphDeviceId", Value: newDeviceId.String(), Expires: time.Now().AddDate(1, 0, 0), - Secure: globals.ExternalURL().Scheme == "https", + Secure: externalURL.Scheme == "https", Domain: r.URL.Host, }) } diff --git a/internal/scim/BUILD.bazel b/internal/scim/BUILD.bazel index f978aea58c9b..2d114ff817b9 100644 --- a/internal/scim/BUILD.bazel +++ b/internal/scim/BUILD.bazel @@ -22,7 +22,6 @@ go_library( visibility = ["//:__subpackages__"], deps = [ "//cmd/frontend/auth", - "//cmd/frontend/globals", "//internal/authz", "//internal/conf", "//internal/database", diff --git a/internal/scim/user_service.go b/internal/scim/user_service.go index 3ae743a1e453..9dec37761afb 100644 --- a/internal/scim/user_service.go +++ b/internal/scim/user_service.go @@ -14,7 +14,6 @@ import ( "github.com/sourcegraph/log" "github.com/sourcegraph/sourcegraph/cmd/frontend/auth" - "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" @@ -105,8 +104,8 @@ func (u *UserSCIMService) Get(ctx context.Context, id string) (scim.Resource, er return scim.Resource{}, err } return user.ToResource(), nil - } + func (u *UserSCIMService) GetAll(ctx context.Context, start int, count *int) (totalCount int, entities []scim.Resource, err error) { return getAllUsersFromDB(ctx, u.db.Users(), start, count) } @@ -132,7 +131,6 @@ func (u *UserSCIMService) Update(ctx context.Context, id string, applySCIMUpdate txErr = updateUser.Update(ctx, &resourceBeforeUpdate, &resourceAfterUpdate) return txErr }) - if err != nil { multiErr, ok := err.(errors.MultiError) if !ok || len(multiErr.Errors()) == 0 { @@ -185,7 +183,7 @@ func (u *UserSCIMService) Create(ctx context.Context, attributes scim.ResourceAt // The user exists, but is not SCIM-controlled, so we'll update the user with the new attributes, // and make the user SCIM-controlled (which is the same as a replace) return u.Update(ctx, strconv.Itoa(int(userID)), func(getResource func() scim.Resource) (updated scim.Resource, _ error) { - var now = time.Now() + now := time.Now() return scim.Resource{ ID: strconv.Itoa(int(userID)), ExternalID: getOptionalExternalID(attributes), @@ -263,10 +261,10 @@ func (u *UserSCIMService) Create(ctx context.Context, attributes scim.ResourceAt // Attempt to send emails in the background. goroutine.Go(func() { _ = sendPasswordResetEmail(u.getLogger(), u.db, user, primaryEmail) - _ = sendWelcomeEmail(primaryEmail, globals.ExternalURL().String(), u.getLogger()) + _ = sendWelcomeEmail(primaryEmail, conf.Get().ExternalURL, u.getLogger()) }) - var now = time.Now() + now := time.Now() return scim.Resource{ ID: strconv.Itoa(int(user.ID)), ExternalID: getOptionalExternalID(attributes), @@ -277,6 +275,7 @@ func (u *UserSCIMService) Create(ctx context.Context, attributes scim.ResourceAt }, }, nil } + func (u *UserSCIMService) Delete(ctx context.Context, id string) error { idInt, err := strconv.Atoi(id) if err != nil { @@ -347,7 +346,7 @@ func getAllUsersFromDB(ctx context.Context, store database.UserStore, startIndex } // Get users and convert them to SCIM resources - var opt = &database.UsersListOptions{} + opt := &database.UsersListOptions{} if count != nil { opt = &database.UsersListOptions{ LimitOffset: &database.LimitOffset{Limit: *count, Offset: offset},