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.
This commit is contained in:
Quinn Slack 2024-07-19 03:26:03 -07:00 committed by GitHub
parent 0e958d19d6
commit 702b346986
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 387 additions and 135 deletions

View File

@ -450,6 +450,7 @@ go_test(
"search_test.go",
"settings_cascade_test.go",
"settings_mutation_test.go",
"settings_subject_test.go",
"site_admin_test.go",
"site_alerts_test.go",
"site_config_change_connection_test.go",

View File

@ -30,12 +30,18 @@ func marshalDefaultSettingsGQLID(defaultSettingsID string) graphql.ID {
func (r *defaultSettingsResolver) ID() graphql.ID { return marshalDefaultSettingsGQLID(r.gqlID) }
func (r *defaultSettingsResolver) LatestSettings(_ context.Context) (*settingsResolver, error) {
func (r *defaultSettingsResolver) LatestSettings(ctx context.Context) (*settingsResolver, error) {
// 🚨 SECURITY: Check that the viewer can access these settings.
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, r)
if err != nil {
return nil, err
}
settings := &api.Settings{
Subject: api.SettingsSubject{Default: true},
Contents: `{"experimentalFeatures": {}}`,
}
return &settingsResolver{db: r.db, subject: &settingsSubjectResolver{defaultSettings: r}, settings: settings}, nil
return &settingsResolver{db: r.db, subject: subject, settings: settings}, nil
}
func (r *defaultSettingsResolver) SettingsURL() *string { return nil }
@ -44,8 +50,15 @@ func (r *defaultSettingsResolver) ViewerCanAdminister(_ context.Context) (bool,
return false, nil
}
func (r *defaultSettingsResolver) SettingsCascade() *settingsCascade {
return &settingsCascade{db: r.db, subject: &settingsSubjectResolver{defaultSettings: r}}
func (r *defaultSettingsResolver) SettingsCascade(ctx context.Context) (*settingsCascade, error) {
// 🚨 SECURITY: Check that the viewer can access these settings.
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, r)
if err != nil {
return nil, err
}
return &settingsCascade{db: r.db, subject: subject}, nil
}
func (r *defaultSettingsResolver) ConfigurationCascade() *settingsCascade { return r.SettingsCascade() }
func (r *defaultSettingsResolver) ConfigurationCascade(ctx context.Context) (*settingsCascade, error) {
return r.SettingsCascade(ctx)
}

View File

@ -11,7 +11,6 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
"github.com/sourcegraph/sourcegraph/internal/actor"
sgactor "github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/auth"
"github.com/sourcegraph/sourcegraph/internal/authz/permssync"
"github.com/sourcegraph/sourcegraph/internal/database"
@ -234,18 +233,14 @@ func (s *membersConnectionStore) UnmarshalCursor(cursor string, _ database.Order
return []any{nodeID}, nil
}
func (o *OrgResolver) settingsSubject() api.SettingsSubject {
return api.SettingsSubject{Org: &o.org.ID}
}
func (o *OrgResolver) LatestSettings(ctx context.Context) (*settingsResolver, error) {
// 🚨 SECURITY: Only organization members and site admins (not on cloud) may access the settings,
// because they may contain secrets or other sensitive data.
if err := auth.CheckOrgAccessOrSiteAdmin(ctx, o.db, o.org.ID); err != nil {
// 🚨 SECURITY: Check that the viewer can access these settings.
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, o)
if err != nil {
return nil, err
}
settings, err := o.db.Settings().GetLatest(ctx, o.settingsSubject())
settings, err := o.db.Settings().GetLatest(ctx, subject.toSubject())
if err != nil {
return nil, err
}
@ -253,14 +248,21 @@ func (o *OrgResolver) LatestSettings(ctx context.Context) (*settingsResolver, er
return nil, nil
}
return &settingsResolver{db: o.db, subject: &settingsSubjectResolver{org: o}, settings: settings}, nil
return &settingsResolver{db: o.db, subject: subject, settings: settings}, nil
}
func (o *OrgResolver) SettingsCascade() *settingsCascade {
return &settingsCascade{db: o.db, subject: &settingsSubjectResolver{org: o}}
func (o *OrgResolver) SettingsCascade(ctx context.Context) (*settingsCascade, error) {
// 🚨 SECURITY: Check that the viewer can access these settings.
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, o)
if err != nil {
return nil, err
}
return &settingsCascade{db: o.db, subject: subject}, nil
}
func (o *OrgResolver) ConfigurationCascade() *settingsCascade { return o.SettingsCascade() }
func (o *OrgResolver) ConfigurationCascade(ctx context.Context) (*settingsCascade, error) {
return o.SettingsCascade(ctx)
}
func (o *OrgResolver) ViewerPendingInvitation(ctx context.Context) (*organizationInvitationResolver, error) {
if actor := sgactor.FromContext(ctx); actor.IsAuthenticated() {

View File

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

View File

@ -25,6 +25,9 @@ type settingsCascade struct {
}
func (r *settingsCascade) Subjects(ctx context.Context) ([]*settingsSubjectResolver, error) {
// 🚨 SECURITY: Ensure that we've already checked the viewer's access to the subject's settings.
r.subject.assertCheckedAccess()
subjects, err := settings.RelevantSubjects(ctx, r.db, r.subject.toSubject())
if err != nil {
return nil, err
@ -34,6 +37,9 @@ func (r *settingsCascade) Subjects(ctx context.Context) ([]*settingsSubjectResol
}
func (r *settingsCascade) Final(ctx context.Context) (string, error) {
// 🚨 SECURITY: Ensure that we've already checked the viewer's access to the subject's settings.
r.subject.assertCheckedAccess()
settingsTyped, err := settings.Final(ctx, r.db, r.subject.toSubject())
if err != nil {
return "", err
@ -48,6 +54,9 @@ func (r *settingsCascade) Merged(ctx context.Context) (_ *configurationResolver,
tr, ctx := trace.New(ctx, "SettingsCascade.Merged")
defer tr.EndWithErr(&err)
// 🚨 SECURITY: Ensure that we've already checked the viewer's access to the subject's settings.
r.subject.assertCheckedAccess()
var messages []string
s, err := r.Final(ctx)
if err != nil {
@ -61,10 +70,19 @@ func (r *schemaResolver) ViewerSettings(ctx context.Context) (*settingsCascade,
if err != nil {
return nil, err
}
if user == nil {
return &settingsCascade{db: r.db, subject: &settingsSubjectResolver{site: NewSiteResolver(log.Scoped("settings"), r.db)}}, nil
var viewerNode Node
if user != nil {
viewerNode = user
} else {
viewerNode = NewSiteResolver(log.Scoped("settings"), r.db)
}
return &settingsCascade{db: r.db, subject: &settingsSubjectResolver{user: user}}, nil
settingsSubject, err := settingsSubjectForNodeAndCheckAccess(ctx, viewerNode)
if err != nil {
return nil, err
}
return &settingsCascade{db: r.db, subject: settingsSubject}, nil
}
// Deprecated: in the GraphQL API

View File

@ -9,7 +9,9 @@ import (
func TestSubjects(t *testing.T) {
t.Run("Default settings are included", func(t *testing.T) {
cascade := &settingsCascade{db: dbmocks.NewMockDB(), subject: &settingsSubjectResolver{site: NewSiteResolver(nil, nil)}}
subject := &settingsSubjectResolver{site: NewSiteResolver(nil, nil)}
subject.mockCheckedAccessForTest()
cascade := &settingsCascade{db: dbmocks.NewMockDB(), subject: subject}
subjects, err := cascade.Subjects(context.Background())
if err != nil {
t.Fatal(err)

View File

@ -53,7 +53,7 @@ func (r *schemaResolver) SettingsMutation(ctx context.Context, args *settingsMut
return nil, err
}
subject, err := settingsSubjectForNode(ctx, n)
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, n)
if err != nil {
return nil, err
}

View File

@ -20,7 +20,7 @@ func (r *schemaResolver) SettingsSubject(ctx context.Context, args *struct{ ID g
return nil, err
}
return settingsSubjectForNode(ctx, n)
return settingsSubjectForNodeAndCheckAccess(ctx, n)
}
var errUnknownSettingsSubject = errors.New("unknown settings subject")
@ -31,29 +31,46 @@ type settingsSubjectResolver struct {
site *siteResolver
org *OrgResolver
user *UserResolver
// 🚨 SECURITY: Only the settingsSubjectForNodeAndCheckAccess function can set this. It is used
// to ensure that access checks have been run on this value, so that we don't leak settings to
// an unauthorized viewer by an accidental bypass of access checks. This struct type is
// naturally constructed all over the place (because many types of nodes have settings), and it
// was too easy to bypass the access check accidentally.
checkedAccess_DO_NOT_SET_THIS_MANUALLY_OR_YOU_WILL_LEAK_SECRETS bool
}
func (r *settingsSubjectResolver) assertCheckedAccess() {
if !r.checkedAccess_DO_NOT_SET_THIS_MANUALLY_OR_YOU_WILL_LEAK_SECRETS {
panic("settingsSubjectResolver.assertCheckedAccess: access checks have not been run on this value")
}
}
func resolverForSubject(ctx context.Context, logger log.Logger, db database.DB, subject api.SettingsSubject) (*settingsSubjectResolver, error) {
switch {
case subject.Default:
if subject.Default {
return &settingsSubjectResolver{defaultSettings: newDefaultSettingsResolver(db)}, nil
case subject.Site:
return &settingsSubjectResolver{site: NewSiteResolver(logger, db)}, nil
case subject.Org != nil:
org, err := OrgByIDInt32(ctx, db, *subject.Org)
if err != nil {
return nil, err
}
return &settingsSubjectResolver{org: org}, nil
case subject.User != nil:
user, err := UserByIDInt32(ctx, db, *subject.User)
if err != nil {
return nil, err
}
return &settingsSubjectResolver{user: user}, nil
default:
return nil, errors.New("subject must have exactly one field set")
}
var (
node Node
err error
)
switch {
case subject.Site:
node = NewSiteResolver(logger, db)
case subject.Org != nil:
node, err = OrgByIDInt32(ctx, db, *subject.Org)
case subject.User != nil:
node, err = UserByIDInt32(ctx, db, *subject.User)
default:
panic("subject must have exactly one field set")
}
if err != nil {
return nil, err
}
// 🚨 SECURITY: Call settingsSubjectForNode to reuse the security checks implemented there.
return settingsSubjectForNodeAndCheckAccess(ctx, node)
}
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
return res, nil
}
// settingsSubjectForNode fetches the settings subject for the given Node. If
// the node is not a valid settings subject, an error is returned.
func settingsSubjectForNode(ctx context.Context, n Node) (*settingsSubjectResolver, error) {
// settingsSubjectForNodeAndCheckAccess fetches the settings subject for the given Node. If the node
// is not a valid settings subject, an error is returned.
//
// 🚨 SECURITY: This function also ensures that the actor is permitted to view the node's settings.
// It is the ONLY place that the
// (settingsSubjectResolver).checkedAccess_DO_NOT_SET_THIS_MANUALLY_OR_YOU_WILL_LEAK_SECRETS field
// can be set.
func settingsSubjectForNodeAndCheckAccess(ctx context.Context, n Node) (*settingsSubjectResolver, error) {
var subject settingsSubjectResolver
switch s := n.(type) {
case *defaultSettingsResolver:
subject.defaultSettings = s
case *siteResolver:
return &settingsSubjectResolver{site: s}, nil
subject.site = s
case *UserResolver:
// 🚨 SECURITY: Only the authenticated user can view their settings on
@ -82,23 +109,35 @@ func settingsSubjectForNode(ctx context.Context, n Node) (*settingsSubjectResolv
return nil, err
}
} else {
// 🚨 SECURITY: Only the user and site admins are allowed to view the user's settings.
// 🚨 SECURITY: The user and site admins are allowed to view the user's settings otherwise.
if err := auth.CheckSiteAdminOrSameUser(ctx, s.db, s.user.ID); err != nil {
return nil, err
}
}
return &settingsSubjectResolver{user: s}, nil
subject.user = s
case *OrgResolver:
// 🚨 SECURITY: Check that the current user is a member of the org.
if err := auth.CheckOrgAccessOrSiteAdmin(ctx, s.db, s.org.ID); err != nil {
return nil, err
if dotcom.SourcegraphDotComMode() {
// 🚨 SECURITY: Only org members (not any site admin) can view org settings on Sourcegraph.com.
if err := auth.CheckOrgAccess(ctx, s.db, s.org.ID); err != nil {
return nil, err
}
} else {
// 🚨 SECURITY: Org members or site admins can view the org settings otherwise.
if err := auth.CheckOrgAccessOrSiteAdmin(ctx, s.db, s.org.ID); err != nil {
return nil, err
}
}
return &settingsSubjectResolver{org: s}, nil
subject.org = s
default:
return nil, errUnknownSettingsSubject
}
// 🚨 SECURITY: This is the ONLY place that this field can be set.
subject.checkedAccess_DO_NOT_SET_THIS_MANUALLY_OR_YOU_WILL_LEAK_SECRETS = true
return &subject, nil
}
func (s *settingsSubjectResolver) ToDefaultSettings() (*defaultSettingsResolver, bool) {
@ -115,6 +154,8 @@ func (s *settingsSubjectResolver) ToUser() (*UserResolver, bool) { return s.user
func (s *settingsSubjectResolver) toSubject() api.SettingsSubject {
switch {
case s.defaultSettings != nil:
return api.SettingsSubject{Default: true}
case s.site != nil:
return api.SettingsSubject{Site: true}
case s.org != nil:
@ -186,21 +227,21 @@ func (s *settingsSubjectResolver) ViewerCanAdminister(ctx context.Context) (bool
}
}
func (s *settingsSubjectResolver) SettingsCascade() (*settingsCascade, error) {
func (s *settingsSubjectResolver) SettingsCascade(ctx context.Context) (*settingsCascade, error) {
switch {
case s.defaultSettings != nil:
return s.defaultSettings.SettingsCascade(), nil
return s.defaultSettings.SettingsCascade(ctx)
case s.site != nil:
return s.site.SettingsCascade(), nil
return s.site.SettingsCascade(ctx)
case s.org != nil:
return s.org.SettingsCascade(), nil
return s.org.SettingsCascade(ctx)
case s.user != nil:
return s.user.SettingsCascade(), nil
return s.user.SettingsCascade(ctx)
default:
return nil, errUnknownSettingsSubject
}
}
func (s *settingsSubjectResolver) ConfigurationCascade() (*settingsCascade, error) {
return s.SettingsCascade()
func (s *settingsSubjectResolver) ConfigurationCascade(ctx context.Context) (*settingsCascade, error) {
return s.SettingsCascade(ctx)
}

View File

@ -0,0 +1,154 @@
package graphqlbackend
import (
"context"
"reflect"
"testing"
"github.com/graph-gophers/graphql-go"
"github.com/stretchr/testify/require"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/auth"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
"github.com/sourcegraph/sourcegraph/internal/dotcom"
"github.com/sourcegraph/sourcegraph/internal/types"
)
func (r *settingsSubjectResolver) mockCheckedAccessForTest() {
// 🚨 SECURITY: For use in test mock values only.
r.checkedAccess_DO_NOT_SET_THIS_MANUALLY_OR_YOU_WILL_LEAK_SECRETS = true
}
func TestSettingsSubjectForNodeAndCheckAccess(t *testing.T) {
userID := int32(1)
otherUserID := int32(2)
orgID := int32(2)
db := dbmocks.NewMockDB()
users := dbmocks.NewMockUserStore()
orgMembers := dbmocks.NewMockOrgMemberStore()
db.UsersFunc.SetDefaultReturn(users)
db.OrgMembersFunc.SetDefaultReturn(orgMembers)
orgMembers.GetByOrgIDAndUserIDFunc.SetDefaultHook(func(ctx context.Context, orgID, userID int32) (*types.OrgMembership, error) {
if orgID == 2 && userID == 1 {
return &types.OrgMembership{}, nil
}
return nil, &database.ErrOrgMemberNotFound{}
})
cases := []struct {
name string
node Node
actor *actor.Actor
isDotcom bool
wantError error
wantSubject *settingsSubjectResolver
}{
{
name: "site settings",
node: &siteResolver{db: db},
actor: actor.FromActualUser(&types.User{ID: userID}),
wantSubject: &settingsSubjectResolver{site: &siteResolver{db: db}},
},
{
name: "user settings - same user",
node: &UserResolver{user: &types.User{ID: userID}, db: db},
actor: actor.FromActualUser(&types.User{ID: userID}),
wantSubject: &settingsSubjectResolver{user: &UserResolver{user: &types.User{ID: userID}, db: db}},
},
{
name: "user settings - site admin",
node: &UserResolver{user: &types.User{ID: userID}, db: db},
actor: actor.FromActualUser(&types.User{ID: otherUserID, SiteAdmin: true}),
wantSubject: &settingsSubjectResolver{user: &UserResolver{user: &types.User{ID: userID}, db: db}},
},
{
name: "user settings - site admin on dotcom",
node: &UserResolver{user: &types.User{ID: userID}, db: db},
actor: actor.FromActualUser(&types.User{ID: otherUserID, SiteAdmin: true}),
isDotcom: true,
wantError: &auth.InsufficientAuthorizationError{},
},
{
name: "user settings - different user",
node: &UserResolver{user: &types.User{ID: otherUserID}, db: db},
actor: actor.FromActualUser(&types.User{ID: userID}),
wantError: &auth.InsufficientAuthorizationError{},
},
{
name: "org settings - member",
node: &OrgResolver{org: &types.Org{ID: orgID}, db: db},
actor: actor.FromActualUser(&types.User{ID: userID}),
wantSubject: &settingsSubjectResolver{org: &OrgResolver{org: &types.Org{ID: orgID}, db: db}},
},
{
name: "org settings - non-member",
node: &OrgResolver{org: &types.Org{ID: orgID}, db: db},
actor: actor.FromActualUser(&types.User{ID: otherUserID}),
wantError: auth.ErrNotAnOrgMember,
},
{
name: "org settings - non-member site admin",
node: &OrgResolver{org: &types.Org{ID: orgID}, db: db},
actor: actor.FromActualUser(&types.User{ID: otherUserID, SiteAdmin: true}),
wantSubject: &settingsSubjectResolver{org: &OrgResolver{org: &types.Org{ID: orgID}, db: db}},
},
{
name: "org settings - non-member site admin on dotcom",
node: &OrgResolver{org: &types.Org{ID: orgID}, db: db},
actor: actor.FromActualUser(&types.User{ID: otherUserID, SiteAdmin: true}),
isDotcom: true,
wantError: auth.ErrNotAnOrgMember,
},
{
name: "unknown node type",
node: &mockNode{},
actor: actor.FromActualUser(&types.User{ID: userID}),
wantError: errUnknownSettingsSubject,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dotcom.MockSourcegraphDotComMode(t, tc.isDotcom)
actorUser, err := tc.actor.User(context.Background(), nil)
if err != nil {
panic(err)
}
users.GetByCurrentAuthUserFunc.SetDefaultReturn(actorUser, nil)
ctx := actor.WithActor(context.Background(), tc.actor)
if tc.wantSubject != nil {
tc.wantSubject.mockCheckedAccessForTest()
}
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, tc.node)
if tc.wantError != nil {
if err == nil {
t.Fatalf("expected error %v, got nil", tc.wantError)
}
require.Error(t, err)
require.IsType(t, tc.wantError, err)
} else {
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(subject, tc.wantSubject) {
t.Fatalf("got %#v, want %#v", subject, tc.wantSubject)
}
}
})
}
}
type mockNode struct{}
func (m *mockNode) ID() graphql.ID {
return "mock"
}

View File

@ -18,7 +18,6 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/cody"
"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/conf"
@ -144,26 +143,35 @@ func (r *siteResolver) ViewerCanAdminister(ctx context.Context) (bool, error) {
return true, nil
}
func (r *siteResolver) settingsSubject() api.SettingsSubject {
return api.SettingsSubject{Site: true}
}
func (r *siteResolver) LatestSettings(ctx context.Context) (*settingsResolver, error) {
settings, err := r.db.Settings().GetLatest(ctx, r.settingsSubject())
// 🚨 SECURITY: Check that the viewer can access these settings.
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, r)
if err != nil {
return nil, err
}
settings, err := r.db.Settings().GetLatest(ctx, subject.toSubject())
if err != nil {
return nil, err
}
if settings == nil {
return nil, nil
}
return &settingsResolver{db: r.db, subject: &settingsSubjectResolver{site: r}, settings: settings}, nil
return &settingsResolver{db: r.db, subject: subject, settings: settings}, nil
}
func (r *siteResolver) SettingsCascade() *settingsCascade {
return &settingsCascade{db: r.db, subject: &settingsSubjectResolver{site: r}}
func (r *siteResolver) SettingsCascade(ctx context.Context) (*settingsCascade, error) {
// 🚨 SECURITY: Check that the viewer can access these settings.
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, r)
if err != nil {
return nil, err
}
return &settingsCascade{db: r.db, subject: subject}, nil
}
func (r *siteResolver) ConfigurationCascade() *settingsCascade { return r.SettingsCascade() }
func (r *siteResolver) ConfigurationCascade(ctx context.Context) (*settingsCascade, error) {
return r.SettingsCascade(ctx)
}
func (r *siteResolver) SettingsURL() *string { return strptr("/site-admin/global-settings") }

View File

@ -18,7 +18,6 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/auth"
"github.com/sourcegraph/sourcegraph/internal/auth/providers"
"github.com/sourcegraph/sourcegraph/internal/conf"
@ -375,32 +374,35 @@ func (r *UserResolver) UpdatedAt() *gqlutil.DateTime {
return &gqlutil.DateTime{Time: r.user.UpdatedAt}
}
func (r *UserResolver) settingsSubject() api.SettingsSubject {
return api.SettingsSubject{User: &r.user.ID}
}
func (r *UserResolver) LatestSettings(ctx context.Context) (*settingsResolver, error) {
// 🚨 SECURITY: Only the user and admins are allowed to access the user's
// settings, because they may contain secrets or other sensitive data.
if err := auth.CheckSiteAdminOrSameUserFromActor(r.actor, r.db, r.user.ID); err != nil {
// 🚨 SECURITY: Check that the viewer can access these settings.
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, r)
if err != nil {
return nil, err
}
settings, err := r.db.Settings().GetLatest(ctx, r.settingsSubject())
settings, err := r.db.Settings().GetLatest(ctx, subject.toSubject())
if err != nil {
return nil, err
}
if settings == nil {
return nil, nil
}
return &settingsResolver{db: r.db, subject: &settingsSubjectResolver{user: r}, settings: settings}, nil
return &settingsResolver{db: r.db, subject: subject, settings: settings}, nil
}
func (r *UserResolver) SettingsCascade() *settingsCascade {
return &settingsCascade{db: r.db, subject: &settingsSubjectResolver{user: r}}
func (r *UserResolver) SettingsCascade(ctx context.Context) (*settingsCascade, error) {
// 🚨 SECURITY: Check that the viewer can access these settings.
subject, err := settingsSubjectForNodeAndCheckAccess(ctx, r)
if err != nil {
return nil, err
}
return &settingsCascade{db: r.db, subject: subject}, nil
}
func (r *UserResolver) ConfigurationCascade() *settingsCascade { return r.SettingsCascade() }
func (r *UserResolver) ConfigurationCascade(ctx context.Context) (*settingsCascade, error) {
return r.SettingsCascade(ctx)
}
func (r *UserResolver) SiteAdmin() (bool, error) {
// 🚨 SECURITY: Only the user and admins are allowed to determine if the user is a site admin.

View File

@ -213,66 +213,74 @@ func TestUser_Email(t *testing.T) {
func TestUser_LatestSettings(t *testing.T) {
db := dbmocks.NewMockDB()
t.Run("only allowed by authenticated user on Sourcegraph.com", func(t *testing.T) {
users := dbmocks.NewMockUserStore()
db.UsersFunc.SetDefaultReturn(users)
db.SettingsFunc.SetDefaultReturn(dbmocks.NewMockSettingsStore())
users := dbmocks.NewMockUserStore()
db.UsersFunc.SetDefaultReturn(users)
db.SettingsFunc.SetDefaultReturn(dbmocks.NewMockSettingsStore())
dotcom.MockSourcegraphDotComMode(t, true)
tests := []struct {
name string
ctx context.Context
shouldFail bool
setup func()
}{
{
name: "unauthenticated",
ctx: context.Background(),
shouldFail: true,
setup: func() {
users.GetByIDFunc.SetDefaultReturn(&types.User{ID: 1}, nil)
},
tests := []struct {
name string
ctx context.Context
isDotcom bool
wantErr string
setup func()
}{
{
name: "unauthenticated",
ctx: context.Background(),
wantErr: auth.ErrMustBeSiteAdminOrSameUser.Error(),
setup: func() {
users.GetByIDFunc.SetDefaultReturn(&types.User{ID: 1}, nil)
},
{
name: "another user",
ctx: actor.WithActor(context.Background(), &actor.Actor{UID: 2}),
shouldFail: true,
setup: func() {
users.GetByIDFunc.SetDefaultHook(func(ctx context.Context, id int32) (*types.User, error) {
return &types.User{ID: id}, nil
})
},
},
{
name: "another user",
ctx: actor.WithActor(context.Background(), &actor.Actor{UID: 2}),
wantErr: auth.ErrMustBeSiteAdminOrSameUser.Error(),
setup: func() {
users.GetByIDFunc.SetDefaultHook(func(ctx context.Context, id int32) (*types.User, error) {
return &types.User{ID: id}, nil
})
},
{
name: "site admin",
ctx: actor.WithActor(context.Background(), &actor.Actor{UID: 2}),
shouldFail: false,
setup: func() {
users.GetByIDFunc.SetDefaultHook(func(ctx context.Context, id int32) (*types.User, error) {
return &types.User{ID: id, SiteAdmin: true}, nil
})
},
},
{
name: "site admin",
ctx: actor.WithActor(context.Background(), &actor.Actor{UID: 2}),
setup: func() {
users.GetByIDFunc.SetDefaultHook(func(ctx context.Context, id int32) (*types.User, error) {
return &types.User{ID: id, SiteAdmin: true}, nil
})
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.setup()
},
{
name: "site admin on dotcom",
isDotcom: true,
ctx: actor.WithActor(context.Background(), &actor.Actor{UID: 2}),
wantErr: "must be authenticated as user with id 1",
setup: func() {
users.GetByIDFunc.SetDefaultHook(func(ctx context.Context, id int32) (*types.User, error) {
return &types.User{ID: id, SiteAdmin: true}, nil
})
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.setup()
dotcom.MockSourcegraphDotComMode(t, test.isDotcom)
_, err := NewUserResolver(test.ctx, db, &types.User{ID: 1}).LatestSettings(test.ctx)
_, err := NewUserResolver(test.ctx, db, &types.User{ID: 1}).LatestSettings(test.ctx)
if test.shouldFail {
got := fmt.Sprintf("%v", err)
want := "must be authenticated as the authorized user or site admin"
assert.Equal(t, want, got)
} else {
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if test.wantErr != "" {
if err.Error() != test.wantErr {
t.Errorf("got error %q, want %q", err, test.wantErr)
}
})
}
})
} else {
if err != nil {
t.Errorf("unexpected error: %s", err)
}
}
})
}
}
func TestUser_ViewerCanAdminister(t *testing.T) {