mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
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:
parent
0e958d19d6
commit
702b346986
@ -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",
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
154
cmd/frontend/graphqlbackend/settings_subject_test.go
Normal file
154
cmd/frontend/graphqlbackend/settings_subject_test.go
Normal 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"
|
||||
}
|
||||
@ -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") }
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user