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

Commit 702b346

Browse files
authored
explicitly check viewer access to settings in GraphQL API (#63945)
Previously, a user's or org's settings were protected from unauthorized access in the GraphQL API by access checks far from the actual `SettingCascade` and `LatestSettings` implementations in most cases. This did not present a security issue because on Sourcegraph.com we prevented users from getting a reference to an org they aren't in, and user settings had the right manual access checks. But the access checks are too far away from the actual resolver methods (so it'd be easy to make a mistake) and were not consistently implemented. **Now, all checks for view-settings-of-subject access go through the same function, `settingsSubjectForNodeAndCheckAccess`.** One substantive security change is that now site admins may NOT view the settings of an org they are not a member of. This is in line with site admins on dotcom not being able to see user settings. This is important because settings might contain secrets in the future (e.g., OpenCtx provider config). Site admins may still add themselves to the org and then view the settings, but that creates more of an audit trail and and we may lock down that as well. ## Test plan Unit tests, also browse around the UI and ensure there are no code paths where our assertion triggers.
1 parent 0e958d1 commit 702b346

File tree

12 files changed

+384
-132
lines changed

12 files changed

+384
-132
lines changed

cmd/frontend/graphqlbackend/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ go_test(
450450
"search_test.go",
451451
"settings_cascade_test.go",
452452
"settings_mutation_test.go",
453+
"settings_subject_test.go",
453454
"site_admin_test.go",
454455
"site_alerts_test.go",
455456
"site_config_change_connection_test.go",

cmd/frontend/graphqlbackend/default_settings.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@ func marshalDefaultSettingsGQLID(defaultSettingsID string) graphql.ID {
3030

3131
func (r *defaultSettingsResolver) ID() graphql.ID { return marshalDefaultSettingsGQLID(r.gqlID) }
3232

33-
func (r *defaultSettingsResolver) LatestSettings(_ context.Context) (*settingsResolver, error) {
33+
func (r *defaultSettingsResolver) LatestSettings(ctx context.Context) (*settingsResolver, error) {
34+
// 🚨 SECURITY: Check that the viewer can access these settings.
35+
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, r)
36+
if err != nil {
37+
return nil, err
38+
}
39+
3440
settings := &api.Settings{
3541
Subject: api.SettingsSubject{Default: true},
3642
Contents: `{"experimentalFeatures": {}}`,
3743
}
38-
return &settingsResolver{db: r.db, subject: &settingsSubjectResolver{defaultSettings: r}, settings: settings}, nil
44+
return &settingsResolver{db: r.db, subject: subject, settings: settings}, nil
3945
}
4046

4147
func (r *defaultSettingsResolver) SettingsURL() *string { return nil }
@@ -44,8 +50,15 @@ func (r *defaultSettingsResolver) ViewerCanAdminister(_ context.Context) (bool,
4450
return false, nil
4551
}
4652

47-
func (r *defaultSettingsResolver) SettingsCascade() *settingsCascade {
48-
return &settingsCascade{db: r.db, subject: &settingsSubjectResolver{defaultSettings: r}}
53+
func (r *defaultSettingsResolver) SettingsCascade(ctx context.Context) (*settingsCascade, error) {
54+
// 🚨 SECURITY: Check that the viewer can access these settings.
55+
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, r)
56+
if err != nil {
57+
return nil, err
58+
}
59+
return &settingsCascade{db: r.db, subject: subject}, nil
4960
}
5061

51-
func (r *defaultSettingsResolver) ConfigurationCascade() *settingsCascade { return r.SettingsCascade() }
62+
func (r *defaultSettingsResolver) ConfigurationCascade(ctx context.Context) (*settingsCascade, error) {
63+
return r.SettingsCascade(ctx)
64+
}

cmd/frontend/graphqlbackend/org.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
1212
"github.com/sourcegraph/sourcegraph/internal/actor"
1313
sgactor "github.com/sourcegraph/sourcegraph/internal/actor"
14-
"github.com/sourcegraph/sourcegraph/internal/api"
1514
"github.com/sourcegraph/sourcegraph/internal/auth"
1615
"github.com/sourcegraph/sourcegraph/internal/authz/permssync"
1716
"github.com/sourcegraph/sourcegraph/internal/database"
@@ -234,33 +233,36 @@ func (s *membersConnectionStore) UnmarshalCursor(cursor string, _ database.Order
234233
return []any{nodeID}, nil
235234
}
236235

237-
func (o *OrgResolver) settingsSubject() api.SettingsSubject {
238-
return api.SettingsSubject{Org: &o.org.ID}
239-
}
240-
241236
func (o *OrgResolver) LatestSettings(ctx context.Context) (*settingsResolver, error) {
242-
// 🚨 SECURITY: Only organization members and site admins (not on cloud) may access the settings,
243-
// because they may contain secrets or other sensitive data.
244-
if err := auth.CheckOrgAccessOrSiteAdmin(ctx, o.db, o.org.ID); err != nil {
237+
// 🚨 SECURITY: Check that the viewer can access these settings.
238+
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, o)
239+
if err != nil {
245240
return nil, err
246241
}
247242

248-
settings, err := o.db.Settings().GetLatest(ctx, o.settingsSubject())
243+
settings, err := o.db.Settings().GetLatest(ctx, subject.toSubject())
249244
if err != nil {
250245
return nil, err
251246
}
252247
if settings == nil {
253248
return nil, nil
254249
}
255250

256-
return &settingsResolver{db: o.db, subject: &settingsSubjectResolver{org: o}, settings: settings}, nil
251+
return &settingsResolver{db: o.db, subject: subject, settings: settings}, nil
257252
}
258253

259-
func (o *OrgResolver) SettingsCascade() *settingsCascade {
260-
return &settingsCascade{db: o.db, subject: &settingsSubjectResolver{org: o}}
254+
func (o *OrgResolver) SettingsCascade(ctx context.Context) (*settingsCascade, error) {
255+
// 🚨 SECURITY: Check that the viewer can access these settings.
256+
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, o)
257+
if err != nil {
258+
return nil, err
259+
}
260+
return &settingsCascade{db: o.db, subject: subject}, nil
261261
}
262262

263-
func (o *OrgResolver) ConfigurationCascade() *settingsCascade { return o.SettingsCascade() }
263+
func (o *OrgResolver) ConfigurationCascade(ctx context.Context) (*settingsCascade, error) {
264+
return o.SettingsCascade(ctx)
265+
}
264266

265267
func (o *OrgResolver) ViewerPendingInvitation(ctx context.Context) (*organizationInvitationResolver, error) {
266268
if actor := sgactor.FromContext(ctx); actor.IsAuthenticated() {

cmd/frontend/graphqlbackend/settings.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ var globalSettingsAllowEdits, _ = strconv.ParseBool(env.Get("GLOBAL_SETTINGS_ALL
6464
// like database.Settings.CreateIfUpToDate, except it handles notifying the
6565
// query-runner if any saved queries have changed.
6666
func settingsCreateIfUpToDate(ctx context.Context, db database.DB, subject *settingsSubjectResolver, lastID *int32, authorUserID int32, contents string) (latestSetting *api.Settings, err error) {
67+
// 🚨 SECURITY: Ensure that we've already checked the viewer's access to the subject's settings.
68+
subject.assertCheckedAccess()
69+
6770
if os.Getenv("GLOBAL_SETTINGS_FILE") != "" && subject.site != nil && !globalSettingsAllowEdits {
6871
return nil, errors.New("Updating global settings not allowed when using GLOBAL_SETTINGS_FILE")
6972
}

cmd/frontend/graphqlbackend/settings_cascade.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ type settingsCascade struct {
2525
}
2626

2727
func (r *settingsCascade) Subjects(ctx context.Context) ([]*settingsSubjectResolver, error) {
28+
// 🚨 SECURITY: Ensure that we've already checked the viewer's access to the subject's settings.
29+
r.subject.assertCheckedAccess()
30+
2831
subjects, err := settings.RelevantSubjects(ctx, r.db, r.subject.toSubject())
2932
if err != nil {
3033
return nil, err
@@ -34,6 +37,9 @@ func (r *settingsCascade) Subjects(ctx context.Context) ([]*settingsSubjectResol
3437
}
3538

3639
func (r *settingsCascade) Final(ctx context.Context) (string, error) {
40+
// 🚨 SECURITY: Ensure that we've already checked the viewer's access to the subject's settings.
41+
r.subject.assertCheckedAccess()
42+
3743
settingsTyped, err := settings.Final(ctx, r.db, r.subject.toSubject())
3844
if err != nil {
3945
return "", err
@@ -48,6 +54,9 @@ func (r *settingsCascade) Merged(ctx context.Context) (_ *configurationResolver,
4854
tr, ctx := trace.New(ctx, "SettingsCascade.Merged")
4955
defer tr.EndWithErr(&err)
5056

57+
// 🚨 SECURITY: Ensure that we've already checked the viewer's access to the subject's settings.
58+
r.subject.assertCheckedAccess()
59+
5160
var messages []string
5261
s, err := r.Final(ctx)
5362
if err != nil {
@@ -61,10 +70,19 @@ func (r *schemaResolver) ViewerSettings(ctx context.Context) (*settingsCascade,
6170
if err != nil {
6271
return nil, err
6372
}
64-
if user == nil {
65-
return &settingsCascade{db: r.db, subject: &settingsSubjectResolver{site: NewSiteResolver(log.Scoped("settings"), r.db)}}, nil
73+
74+
var viewerNode Node
75+
if user != nil {
76+
viewerNode = user
77+
} else {
78+
viewerNode = NewSiteResolver(log.Scoped("settings"), r.db)
79+
}
80+
81+
settingsSubject, err := settingsSubjectForNodeAndCheckAccess(ctx, viewerNode)
82+
if err != nil {
83+
return nil, err
6684
}
67-
return &settingsCascade{db: r.db, subject: &settingsSubjectResolver{user: user}}, nil
85+
return &settingsCascade{db: r.db, subject: settingsSubject}, nil
6886
}
6987

7088
// Deprecated: in the GraphQL API

cmd/frontend/graphqlbackend/settings_cascade_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99

1010
func TestSubjects(t *testing.T) {
1111
t.Run("Default settings are included", func(t *testing.T) {
12-
cascade := &settingsCascade{db: dbmocks.NewMockDB(), subject: &settingsSubjectResolver{site: NewSiteResolver(nil, nil)}}
12+
subject := &settingsSubjectResolver{site: NewSiteResolver(nil, nil)}
13+
subject.mockCheckedAccessForTest()
14+
cascade := &settingsCascade{db: dbmocks.NewMockDB(), subject: subject}
1315
subjects, err := cascade.Subjects(context.Background())
1416
if err != nil {
1517
t.Fatal(err)

cmd/frontend/graphqlbackend/settings_mutation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (r *schemaResolver) SettingsMutation(ctx context.Context, args *settingsMut
5353
return nil, err
5454
}
5555

56-
subject, err := settingsSubjectForNode(ctx, n)
56+
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, n)
5757
if err != nil {
5858
return nil, err
5959
}

cmd/frontend/graphqlbackend/settings_subject.go

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func (r *schemaResolver) SettingsSubject(ctx context.Context, args *struct{ ID g
2020
return nil, err
2121
}
2222

23-
return settingsSubjectForNode(ctx, n)
23+
return settingsSubjectForNodeAndCheckAccess(ctx, n)
2424
}
2525

2626
var errUnknownSettingsSubject = errors.New("unknown settings subject")
@@ -31,29 +31,46 @@ type settingsSubjectResolver struct {
3131
site *siteResolver
3232
org *OrgResolver
3333
user *UserResolver
34+
35+
// 🚨 SECURITY: Only the settingsSubjectForNodeAndCheckAccess function can set this. It is used
36+
// to ensure that access checks have been run on this value, so that we don't leak settings to
37+
// an unauthorized viewer by an accidental bypass of access checks. This struct type is
38+
// naturally constructed all over the place (because many types of nodes have settings), and it
39+
// was too easy to bypass the access check accidentally.
40+
checkedAccess_DO_NOT_SET_THIS_MANUALLY_OR_YOU_WILL_LEAK_SECRETS bool
41+
}
42+
43+
func (r *settingsSubjectResolver) assertCheckedAccess() {
44+
if !r.checkedAccess_DO_NOT_SET_THIS_MANUALLY_OR_YOU_WILL_LEAK_SECRETS {
45+
panic("settingsSubjectResolver.assertCheckedAccess: access checks have not been run on this value")
46+
}
3447
}
3548

3649
func resolverForSubject(ctx context.Context, logger log.Logger, db database.DB, subject api.SettingsSubject) (*settingsSubjectResolver, error) {
37-
switch {
38-
case subject.Default:
50+
if subject.Default {
3951
return &settingsSubjectResolver{defaultSettings: newDefaultSettingsResolver(db)}, nil
52+
}
53+
54+
var (
55+
node Node
56+
err error
57+
)
58+
switch {
4059
case subject.Site:
41-
return &settingsSubjectResolver{site: NewSiteResolver(logger, db)}, nil
60+
node = NewSiteResolver(logger, db)
4261
case subject.Org != nil:
43-
org, err := OrgByIDInt32(ctx, db, *subject.Org)
44-
if err != nil {
45-
return nil, err
46-
}
47-
return &settingsSubjectResolver{org: org}, nil
62+
node, err = OrgByIDInt32(ctx, db, *subject.Org)
4863
case subject.User != nil:
49-
user, err := UserByIDInt32(ctx, db, *subject.User)
50-
if err != nil {
51-
return nil, err
52-
}
53-
return &settingsSubjectResolver{user: user}, nil
64+
node, err = UserByIDInt32(ctx, db, *subject.User)
5465
default:
55-
return nil, errors.New("subject must have exactly one field set")
66+
panic("subject must have exactly one field set")
67+
}
68+
if err != nil {
69+
return nil, err
5670
}
71+
72+
// 🚨 SECURITY: Call settingsSubjectForNode to reuse the security checks implemented there.
73+
return settingsSubjectForNodeAndCheckAccess(ctx, node)
5774
}
5875

5976
func resolversForSubjects(ctx context.Context, logger log.Logger, db database.DB, subjects []api.SettingsSubject) (_ []*settingsSubjectResolver, err error) {
@@ -67,12 +84,22 @@ func resolversForSubjects(ctx context.Context, logger log.Logger, db database.DB
6784
return res, nil
6885
}
6986

70-
// settingsSubjectForNode fetches the settings subject for the given Node. If
71-
// the node is not a valid settings subject, an error is returned.
72-
func settingsSubjectForNode(ctx context.Context, n Node) (*settingsSubjectResolver, error) {
87+
// settingsSubjectForNodeAndCheckAccess fetches the settings subject for the given Node. If the node
88+
// is not a valid settings subject, an error is returned.
89+
//
90+
// 🚨 SECURITY: This function also ensures that the actor is permitted to view the node's settings.
91+
// It is the ONLY place that the
92+
// (settingsSubjectResolver).checkedAccess_DO_NOT_SET_THIS_MANUALLY_OR_YOU_WILL_LEAK_SECRETS field
93+
// can be set.
94+
func settingsSubjectForNodeAndCheckAccess(ctx context.Context, n Node) (*settingsSubjectResolver, error) {
95+
var subject settingsSubjectResolver
96+
7397
switch s := n.(type) {
98+
case *defaultSettingsResolver:
99+
subject.defaultSettings = s
100+
74101
case *siteResolver:
75-
return &settingsSubjectResolver{site: s}, nil
102+
subject.site = s
76103

77104
case *UserResolver:
78105
// 🚨 SECURITY: Only the authenticated user can view their settings on
@@ -82,23 +109,35 @@ func settingsSubjectForNode(ctx context.Context, n Node) (*settingsSubjectResolv
82109
return nil, err
83110
}
84111
} else {
85-
// 🚨 SECURITY: Only the user and site admins are allowed to view the user's settings.
112+
// 🚨 SECURITY: The user and site admins are allowed to view the user's settings otherwise.
86113
if err := auth.CheckSiteAdminOrSameUser(ctx, s.db, s.user.ID); err != nil {
87114
return nil, err
88115
}
89116
}
90-
return &settingsSubjectResolver{user: s}, nil
117+
subject.user = s
91118

92119
case *OrgResolver:
93-
// 🚨 SECURITY: Check that the current user is a member of the org.
94-
if err := auth.CheckOrgAccessOrSiteAdmin(ctx, s.db, s.org.ID); err != nil {
95-
return nil, err
120+
if dotcom.SourcegraphDotComMode() {
121+
// 🚨 SECURITY: Only org members (not any site admin) can view org settings on Sourcegraph.com.
122+
if err := auth.CheckOrgAccess(ctx, s.db, s.org.ID); err != nil {
123+
return nil, err
124+
}
125+
} else {
126+
// 🚨 SECURITY: Org members or site admins can view the org settings otherwise.
127+
if err := auth.CheckOrgAccessOrSiteAdmin(ctx, s.db, s.org.ID); err != nil {
128+
return nil, err
129+
}
96130
}
97-
return &settingsSubjectResolver{org: s}, nil
131+
subject.org = s
98132

99133
default:
100134
return nil, errUnknownSettingsSubject
101135
}
136+
137+
// 🚨 SECURITY: This is the ONLY place that this field can be set.
138+
subject.checkedAccess_DO_NOT_SET_THIS_MANUALLY_OR_YOU_WILL_LEAK_SECRETS = true
139+
140+
return &subject, nil
102141
}
103142

104143
func (s *settingsSubjectResolver) ToDefaultSettings() (*defaultSettingsResolver, bool) {
@@ -115,6 +154,8 @@ func (s *settingsSubjectResolver) ToUser() (*UserResolver, bool) { return s.user
115154

116155
func (s *settingsSubjectResolver) toSubject() api.SettingsSubject {
117156
switch {
157+
case s.defaultSettings != nil:
158+
return api.SettingsSubject{Default: true}
118159
case s.site != nil:
119160
return api.SettingsSubject{Site: true}
120161
case s.org != nil:
@@ -186,21 +227,21 @@ func (s *settingsSubjectResolver) ViewerCanAdminister(ctx context.Context) (bool
186227
}
187228
}
188229

189-
func (s *settingsSubjectResolver) SettingsCascade() (*settingsCascade, error) {
230+
func (s *settingsSubjectResolver) SettingsCascade(ctx context.Context) (*settingsCascade, error) {
190231
switch {
191232
case s.defaultSettings != nil:
192-
return s.defaultSettings.SettingsCascade(), nil
233+
return s.defaultSettings.SettingsCascade(ctx)
193234
case s.site != nil:
194-
return s.site.SettingsCascade(), nil
235+
return s.site.SettingsCascade(ctx)
195236
case s.org != nil:
196-
return s.org.SettingsCascade(), nil
237+
return s.org.SettingsCascade(ctx)
197238
case s.user != nil:
198-
return s.user.SettingsCascade(), nil
239+
return s.user.SettingsCascade(ctx)
199240
default:
200241
return nil, errUnknownSettingsSubject
201242
}
202243
}
203244

204-
func (s *settingsSubjectResolver) ConfigurationCascade() (*settingsCascade, error) {
205-
return s.SettingsCascade()
245+
func (s *settingsSubjectResolver) ConfigurationCascade(ctx context.Context) (*settingsCascade, error) {
246+
return s.SettingsCascade(ctx)
206247
}

0 commit comments

Comments
 (0)