Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Commit

Permalink
soap: add support for site config allowlist (#59884) (#59958)
Browse files Browse the repository at this point in the history
(cherry picked from commit 736af89)
  • Loading branch information
michaellzc authored Jan 30, 2024
1 parent a527a56 commit 958ca18
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 4 deletions.
1 change: 1 addition & 0 deletions cmd/frontend/graphqlbackend/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ go_library(
"//internal/authz/permssync",
"//internal/binary",
"//internal/cloneurls",
"//internal/cloud",
"//internal/codeintel/dependencies",
"//internal/codeintel/dependencies/shared",
"//internal/codeintel/resolvers",
Expand Down
22 changes: 22 additions & 0 deletions cmd/frontend/graphqlbackend/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/auth"
"github.com/sourcegraph/sourcegraph/internal/cloud"
"github.com/sourcegraph/sourcegraph/internal/cody"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/conf/conftypes"
Expand Down Expand Up @@ -355,6 +356,14 @@ func (r *schemaResolver) UpdateSiteConfiguration(ctx context.Context, args *stru
if err != nil {
return false, errors.Errorf("error unredacting secrets: %s", err)
}

cloudSiteConfig := cloud.SiteConfig()
if cloudSiteConfig.SiteConfigAllowlistEnabled() && !actor.FromContext(ctx).SourcegraphOperator {
if p, ok := allowEdit(prev.Site, unredacted, cloudSiteConfig.SiteConfigAllowlist.Paths); !ok {
return false, cloudSiteConfig.SiteConfigAllowlistOnError(p)
}
}

prev.Site = unredacted

server := globals.ConfigurationServerFrontendOnly
Expand Down Expand Up @@ -669,3 +678,16 @@ func (c *codyLLMConfigurationResolver) CompletionModelMaxTokens() *int32 {
}
return nil
}

func allowEdit(before, after string, allowlist []string) ([]string, bool) {
var notAllowed []string
changes := conf.Diff(before, after)
for key := range changes {
for _, p := range allowlist {
if key != p {
notAllowed = append(notAllowed, key)
}
}
}
return notAllowed, len(notAllowed) == 0
}
75 changes: 75 additions & 0 deletions cmd/frontend/graphqlbackend/site_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hexops/autogold/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
"github.com/sourcegraph/sourcegraph/internal/actor"
Expand Down Expand Up @@ -350,3 +352,76 @@ func TestIsRequiredOutOfBandMigration(t *testing.T) {
})
}
}

func Test_allowEdit(t *testing.T) {
tests := []struct {
name string
before string
after string
allowlist []string
want autogold.Value
ok bool
}{
{
name: "allowed",
before: `{}`,
after: `{"externalURL": "https://sg.local.com"}`,
allowlist: []string{"externalURL"},
ok: true,
},
{
name: "not allowed",
before: `{}`,
after: `
{
"observability.alerts": [
{
"level": "critical",
"notifier": {
"type": "slack",
"url": "some-url",
"username": "username"
}
},
]
}`,
allowlist: []string{"externalURL"},
want: autogold.Expect([]string{"observability.alerts"}),
ok: false,
},
{
name: "nested and mixed",
before: `{}`,
after: `
{
"experimentalFeatures": {
"searchJobs": true,
},
"auth.providers": [
{
"type": "builtin"
},
],
"email.smtp": {
"authentication": "PLAIN",
"host": "smtp.company.local",
"password": "password",
"port": 587,
"username": "username",
},
}`,
allowlist: []string{"auth.providers"},
want: autogold.Expect([]string{"experimentalFeatures::searchJobs", "email.smtp"}),
ok: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := allowEdit(tt.before, tt.after, tt.allowlist)
require.Equal(t, tt.ok, ok)
if !ok {
tt.want.Equal(t, got)
}
})
}
}
61 changes: 57 additions & 4 deletions internal/cloud/site_config.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
package cloud

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"sync"
"testing"
"text/template"

"golang.org/x/crypto/ssh"

"github.com/sourcegraph/sourcegraph/internal/env"
"github.com/sourcegraph/sourcegraph/lib/errors"
)

// rawSiteConfig is the base64-encoded string that is signed by the "Sourcegraph
// Cloud site config singer" private key, which is available at
// https://team-sourcegraph.1password.com/vaults/dnrhbauihkhjs5ag6vszsme45a/allitems/m4rqoaoujjwesf6twwqyr3lpde.
var rawSiteConfig = env.Get("SRC_CLOUD_SITE_CONFIG", "", "The site configuration specifically for Sourcegraph Cloud")
var (
// rawSiteConfig is the base64-encoded string that is signed by the "Sourcegraph
// Cloud site config singer" private key, which is available at
// https://team-sourcegraph.1password.com/vaults/dnrhbauihkhjs5ag6vszsme45a/allitems/m4rqoaoujjwesf6twwqyr3lpde.
rawSiteConfig = env.Get("SRC_CLOUD_SITE_CONFIG", "", "The site configuration specifically for Sourcegraph Cloud")

defaultNotAllowedErrorMessageTmpl = template.Must(template.New("").Parse("Editing {{.Paths}} in site configuration is not allowed on Sourcegraph Cloud. Please contact support."))
)

// sourcegraphCloudSiteConfigSignerPublicKey is the counterpart of the
// "Sourcegraph Cloud site config singer" private key.
Expand Down Expand Up @@ -54,6 +62,17 @@ func parseSiteConfig(raw string) (*SchemaSiteConfig, error) {
if err != nil {
return nil, errors.Wrap(err, "unmarshal verified site config")
}

if siteConfig.SiteConfigAllowlistEnabled() {
if siteConfig.SiteConfigAllowlist.NotAllowedErrorMessage == "" {
siteConfig.SiteConfigAllowlist.errorMessageTmpl = defaultNotAllowedErrorMessageTmpl
} else {
siteConfig.SiteConfigAllowlist.errorMessageTmpl, err = template.New("").Parse(siteConfig.SiteConfigAllowlist.NotAllowedErrorMessage)
if err != nil {
return nil, errors.Wrap(err, "parse error message template")
}
}
}
return &siteConfig, nil
}

Expand Down Expand Up @@ -95,6 +114,21 @@ func SiteConfig() *SchemaSiteConfig {
// SchemaSiteConfig contains the Sourcegraph Cloud site config.
type SchemaSiteConfig struct {
AuthProviders *SchemaAuthProviders `json:"authProviders"`
// SiteConfigAllowlist controls what site config attributes
// Cloud customers are allowed to change
SiteConfigAllowlist SiteConfigAllowlistSpec `json:"siteConfigAllowlist,omitempty"`
}

type SiteConfigAllowlistSpec struct {
// NotAllowedErrorMessage is a go template string to show error message.
// Available variables: {{.Paths}}
NotAllowedErrorMessage string `json:"notAllowedErrorMessage,omitempty"`
// Paths is a list of keys in the site config that are allowed to be changed
// Notes:
// - only top-level keys are supported
Paths []string `json:"paths"`

errorMessageTmpl *template.Template `json:"-"`
}

// SchemaAuthProviders contains the authentication providers for Sourcegraph
Expand All @@ -120,3 +154,22 @@ type SchemaAuthProviderSourcegraphOperator struct {
func (s *SchemaSiteConfig) SourcegraphOperatorAuthProviderEnabled() bool {
return s.AuthProviders != nil && s.AuthProviders.SourcegraphOperator != nil
}

func (s *SchemaSiteConfig) SiteConfigAllowlistEnabled() bool {
return s.SourcegraphOperatorAuthProviderEnabled() && len(s.SiteConfigAllowlist.Paths) > 0
}

func (s *SchemaSiteConfig) SiteConfigAllowlistOnError(paths []string) error {
if !s.SiteConfigAllowlistEnabled() {
return nil
}
var b bytes.Buffer
if err := s.SiteConfigAllowlist.errorMessageTmpl.Execute(&b, struct {
Paths string
}{
Paths: fmt.Sprintf("[%s]", strings.Join(paths, ", ")),
}); err != nil {
return errors.Wrapf(err, "Execute error message template: Editing %q in site configuration is not allowed. Please contact support", paths)
}
return errors.New(b.String())
}
13 changes: 13 additions & 0 deletions internal/conf/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@ import (
"reflect"
"strings"

"github.com/sourcegraph/sourcegraph/internal/conf/conftypes"
"github.com/sourcegraph/sourcegraph/schema"
)

func Diff(before, after string) (fields map[string]struct{}) {
beforeCfg, err := ParseConfig(conftypes.RawUnified{Site: before})
if err != nil {
return nil
}
afterCfg, err := ParseConfig(conftypes.RawUnified{Site: after})
if err != nil {
return nil
}
return diff(beforeCfg, afterCfg)
}

// diff returns names of the Go fields that have different values between the
// two configurations.
func diff(before, after *Unified) (fields map[string]struct{}) {
Expand Down

0 comments on commit 958ca18

Please sign in to comment.