authz: Gracefully handle temporary provider errors (#37107)

We now return the previously synced user permissions when we hit a temporary error reading
the current permissions from a provider. The previous logic would return no permissions which
meant that actual permissions would flip on and off when we hit temporary issues communicating
with the auth provider.
This commit is contained in:
Ryan Slade 2022-06-15 16:30:05 +02:00 committed by GitHub
parent cb7ef85cd6
commit 9cc170eb51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 638 additions and 7 deletions

View File

@ -430,13 +430,13 @@ func (s *PermsSyncer) fetchUserPermsViaExternalAccounts(ctx context.Context, use
if err != nil {
// The "401 Unauthorized" is returned by code hosts when the token is revoked
unauthorized := errcode.IsUnauthorized(err)
forbidden := errcode.IsForbidden(err)
// Detect GitHub account suspension error
accountSuspended := errcode.IsAccountSuspended(err)
if unauthorized || accountSuspended || forbidden {
// These are fatal errors that mean we should continue as if the account no
// longer has any access.
err = accounts.TouchExpired(ctx, acct.ID)
if err != nil {
return nil, nil, errors.Wrapf(err, "set expired for external account %d", acct.ID)
@ -463,6 +463,37 @@ func (s *PermsSyncer) fetchUserPermsViaExternalAccounts(ctx context.Context, use
continue
}
if errcode.IsTemporary(err) {
// If we have a temporary issue, we should instead return any permissions we
// already know about to ensure that we don't temporarily remove access for the
// user because of intermittent errors.
acctLogger.Warn("temporary error, returning previously synced permissions", log.Error(err))
extPerms = new(authz.ExternalUserPermissions)
// Load last synced sub-repo perms for this user and provider
currentSubRepoPerms, err := s.db.SubRepoPerms().GetByUserAndService(ctx, user.ID, provider.ServiceType(), provider.ServiceID())
if err != nil {
return nil, nil, errors.Wrap(err, "fetching existing sub-repo permissions")
}
extPerms.SubRepoPermissions = make(map[extsvc.RepoID]*authz.SubRepoPermissions, len(currentSubRepoPerms))
for k := range currentSubRepoPerms {
v := currentSubRepoPerms[k]
extPerms.SubRepoPermissions[extsvc.RepoID(k.ID)] = &v
}
// Load last synced repos for this user and provider
currentRepos, err := s.permsStore.FetchReposByUserAndExternalService(ctx, user.ID, provider.ServiceType(), provider.ServiceID())
if err != nil {
return nil, nil, errors.Wrap(err, "fetching existing repo permissions")
}
for _, id := range currentRepos {
repoIDs = append(repoIDs, uint32(id))
}
continue
}
// Process partial results if this is an initial fetch.
if !noPerms {
return nil, nil, errors.Wrapf(err, "fetch user permissions for external account %d", acct.ID)
@ -492,6 +523,8 @@ func (s *PermsSyncer) fetchUserPermsViaExternalAccounts(ctx context.Context, use
// Record any sub-repository permissions
for repoID := range extPerms.SubRepoPermissions {
spec := api.ExternalRepoSpec{
// This is safe since repoID is an extsvc.RepoID which represents the external id
// of the repo.
ID: string(repoID),
ServiceType: provider.ServiceType(),
ServiceID: provider.ServiceID(),
@ -508,6 +541,7 @@ func (s *PermsSyncer) fetchUserPermsViaExternalAccounts(ctx context.Context, use
},
)
}
for _, excludePrefix := range extPerms.ExcludeContains {
excludeContainsSpecs = append(excludeContainsSpecs,
api.ExternalRepoSpec{
@ -542,7 +576,11 @@ func (s *PermsSyncer) fetchUserPermsViaExternalAccounts(ctx context.Context, use
}
// repoIDs represents repos the user is allowed to read
repoIDs = make([]uint32, 0, len(repoNames))
if len(repoIDs) == 0 {
// We may already have some repos if we hit a temporary error above in which case
// we don't want to clear it out
repoIDs = make([]uint32, 0, len(repoNames))
}
for _, r := range repoNames {
repoIDs = append(repoIDs, uint32(r.ID))
}

View File

@ -12,9 +12,8 @@ import (
mockrequire "github.com/derision-test/go-mockgen/testutil/require"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/sourcegraph/log/logtest"
"github.com/stretchr/testify/assert"
eauthz "github.com/sourcegraph/sourcegraph/enterprise/internal/authz"
edb "github.com/sourcegraph/sourcegraph/enterprise/internal/database"
@ -200,6 +199,111 @@ func TestPermsSyncer_syncUserPerms(t *testing.T) {
}
}
// If we hit a temporary error from the provider we should fetch existing
// permissions from the database
func TestPermsSyncer_syncUserPermsTemporaryProviderError(t *testing.T) {
p := &mockProvider{
id: 1,
serviceType: extsvc.TypeGitLab,
serviceID: "https://gitlab.com/",
}
authz.SetProviders(false, []authz.Provider{p})
defer authz.SetProviders(true, nil)
extAccount := extsvc.Account{
AccountSpec: extsvc.AccountSpec{
ServiceType: p.ServiceType(),
ServiceID: p.ServiceID(),
},
}
extService := &types.ExternalService{
ID: 1,
Kind: extsvc.KindGitLab,
DisplayName: "GITLAB1",
Config: `{"token": "limited", "authorization": {}}`,
NamespaceUserID: 1,
}
users := database.NewMockUserStore()
users.GetByIDFunc.SetDefaultHook(func(ctx context.Context, id int32) (*types.User, error) {
return &types.User{ID: id}, nil
})
mockRepos := database.NewMockRepoStore()
mockRepos.ListMinimalReposFunc.SetDefaultHook(func(ctx context.Context, opt database.ReposListOptions) ([]types.MinimalRepo, error) {
if !opt.OnlyPrivate {
return nil, errors.New("OnlyPrivate want true but got false")
}
names := make([]types.MinimalRepo, 0, len(opt.ExternalRepos))
for _, r := range opt.ExternalRepos {
id, _ := strconv.Atoi(r.ID)
names = append(names, types.MinimalRepo{ID: api.RepoID(id)})
}
return names, nil
})
externalServices := database.NewMockExternalServiceStore()
externalServices.ListFunc.SetDefaultReturn([]*types.ExternalService{extService}, nil)
userEmails := database.NewMockUserEmailsStore()
externalAccounts := database.NewMockUserExternalAccountsStore()
externalAccounts.ListFunc.SetDefaultReturn([]*extsvc.Account{&extAccount}, nil)
subRepoPerms := database.NewMockSubRepoPermsStore()
subRepoPerms.GetByUserAndServiceFunc.SetDefaultReturn(nil, nil)
db := database.NewMockDB()
db.UsersFunc.SetDefaultReturn(users)
db.ReposFunc.SetDefaultReturn(mockRepos)
db.ExternalServicesFunc.SetDefaultReturn(externalServices)
db.UserEmailsFunc.SetDefaultReturn(userEmails)
db.UserExternalAccountsFunc.SetDefaultReturn(externalAccounts)
db.SubRepoPermsFunc.SetDefaultReturn(subRepoPerms)
reposStore := repos.NewMockStoreFrom(repos.NewStore(logtest.Scoped(t), db))
reposStore.ListExternalServiceUserIDsByRepoIDFunc.SetDefaultReturn([]int32{1}, nil)
reposStore.ListExternalServicePrivateRepoIDsByUserIDFunc.SetDefaultReturn([]api.RepoID{2, 3, 4}, nil)
reposStore.ExternalServiceStoreFunc.SetDefaultReturn(externalServices)
reposStore.RepoStoreFunc.SetDefaultReturn(mockRepos)
perms := edb.NewMockPermsStore()
perms.SetUserPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.UserPermissions) error {
wantIDs := []int32{1, 2, 3, 4, 5}
assert.Equal(t, wantIDs, p.GenerateSortedIDsSlice())
return nil
})
perms.UserIsMemberOfOrgHasCodeHostConnectionFunc.SetDefaultReturn(true, nil)
perms.FetchReposByUserAndExternalServiceFunc.SetDefaultHook(func(ctx context.Context, i int32, s string, s2 string) ([]api.RepoID, error) {
return []api.RepoID{1}, nil
})
eauthz.MockProviderFromExternalService = func(siteConfig schema.SiteConfiguration, svc *types.ExternalService) (authz.Provider, error) {
return p, nil
}
defer func() {
eauthz.MockProviderFromExternalService = nil
}()
s := NewPermsSyncer(logtest.Scoped(t), db, reposStore, perms, timeutil.Now, nil)
p.fetchUserPerms = func(context.Context, *extsvc.Account) (*authz.ExternalUserPermissions, error) {
// DeadlineExceeded implements the Temporary interface
return nil, context.DeadlineExceeded
}
p.fetchUserPermsByToken = func(ctx context.Context, s string) (*authz.ExternalUserPermissions, error) {
return &authz.ExternalUserPermissions{
Exacts: []extsvc.RepoID{"5"},
}, nil
}
err := s.syncUserPerms(context.Background(), 1, true, authz.FetchPermsOptions{})
if err != nil {
t.Fatal(err)
}
}
func TestPermsSyncer_syncUserPerms_noPerms(t *testing.T) {
p := &mockProvider{
id: 1,

View File

@ -49,7 +49,7 @@ type p4Execer interface {
// NewProvider returns a new Perforce authorization provider that uses the given
// host, user and password to talk to a Perforce Server that is the source of
// truth for permissions. It assumes emails of Sourcegraph accounts match 1-1
// with emails of Perforce Server users. It uses our default gitserver client.
// with emails of Perforce Server users.
func NewProvider(urn, host, user, password string, depots []extsvc.RepoID, db database.DB) *Provider {
baseURL, _ := url.Parse(host)
return &Provider{

View File

@ -36,6 +36,7 @@ func TestIntegration_PermsStore(t *testing.T) {
test func(*testing.T)
}{
{"LoadUserPermissions", testPermsStore_LoadUserPermissions(db)},
{"FetchReposByUserAndExternalService", testPermsStore_FetchReposByUserAndExternalService(db)},
{"LoadRepoPermissions", testPermsStore_LoadRepoPermissions(db)},
{"SetUserPermissions", testPermsStore_SetUserPermissions(db)},
{"SetRepoPermissions", testPermsStore_SetRepoPermissions(db)},

View File

@ -11310,6 +11310,10 @@ type MockPermsStore struct {
// DoneFunc is an instance of a mock function object controlling the
// behavior of the method Done.
DoneFunc *PermsStoreDoneFunc
// FetchReposByUserAndExternalServiceFunc is an instance of a mock
// function object controlling the behavior of the method
// FetchReposByUserAndExternalService.
FetchReposByUserAndExternalServiceFunc *PermsStoreFetchReposByUserAndExternalServiceFunc
// GetUserIDsByExternalAccountsFunc is an instance of a mock function
// object controlling the behavior of the method
// GetUserIDsByExternalAccounts.
@ -11402,6 +11406,11 @@ func NewMockPermsStore() *MockPermsStore {
return
},
},
FetchReposByUserAndExternalServiceFunc: &PermsStoreFetchReposByUserAndExternalServiceFunc{
defaultHook: func(context.Context, int32, string, string) (r0 []api.RepoID, r1 error) {
return
},
},
GetUserIDsByExternalAccountsFunc: &PermsStoreGetUserIDsByExternalAccountsFunc{
defaultHook: func(context.Context, *extsvc.Accounts) (r0 map[string]int32, r1 error) {
return
@ -11534,6 +11543,11 @@ func NewStrictMockPermsStore() *MockPermsStore {
panic("unexpected invocation of MockPermsStore.Done")
},
},
FetchReposByUserAndExternalServiceFunc: &PermsStoreFetchReposByUserAndExternalServiceFunc{
defaultHook: func(context.Context, int32, string, string) ([]api.RepoID, error) {
panic("unexpected invocation of MockPermsStore.FetchReposByUserAndExternalService")
},
},
GetUserIDsByExternalAccountsFunc: &PermsStoreGetUserIDsByExternalAccountsFunc{
defaultHook: func(context.Context, *extsvc.Accounts) (map[string]int32, error) {
panic("unexpected invocation of MockPermsStore.GetUserIDsByExternalAccounts")
@ -11660,6 +11674,9 @@ func NewMockPermsStoreFrom(i PermsStore) *MockPermsStore {
DoneFunc: &PermsStoreDoneFunc{
defaultHook: i.Done,
},
FetchReposByUserAndExternalServiceFunc: &PermsStoreFetchReposByUserAndExternalServiceFunc{
defaultHook: i.FetchReposByUserAndExternalService,
},
GetUserIDsByExternalAccountsFunc: &PermsStoreGetUserIDsByExternalAccountsFunc{
defaultHook: i.GetUserIDsByExternalAccounts,
},
@ -12047,6 +12064,124 @@ func (c PermsStoreDoneFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// PermsStoreFetchReposByUserAndExternalServiceFunc describes the behavior
// when the FetchReposByUserAndExternalService method of the parent
// MockPermsStore instance is invoked.
type PermsStoreFetchReposByUserAndExternalServiceFunc struct {
defaultHook func(context.Context, int32, string, string) ([]api.RepoID, error)
hooks []func(context.Context, int32, string, string) ([]api.RepoID, error)
history []PermsStoreFetchReposByUserAndExternalServiceFuncCall
mutex sync.Mutex
}
// FetchReposByUserAndExternalService delegates to the next hook function in
// the queue and stores the parameter and result values of this invocation.
func (m *MockPermsStore) FetchReposByUserAndExternalService(v0 context.Context, v1 int32, v2 string, v3 string) ([]api.RepoID, error) {
r0, r1 := m.FetchReposByUserAndExternalServiceFunc.nextHook()(v0, v1, v2, v3)
m.FetchReposByUserAndExternalServiceFunc.appendCall(PermsStoreFetchReposByUserAndExternalServiceFuncCall{v0, v1, v2, v3, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the
// FetchReposByUserAndExternalService method of the parent MockPermsStore
// instance is invoked and the hook queue is empty.
func (f *PermsStoreFetchReposByUserAndExternalServiceFunc) SetDefaultHook(hook func(context.Context, int32, string, string) ([]api.RepoID, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// FetchReposByUserAndExternalService method of the parent MockPermsStore
// instance invokes the hook at the front of the queue and discards it.
// After the queue is empty, the default hook function is invoked for any
// future action.
func (f *PermsStoreFetchReposByUserAndExternalServiceFunc) PushHook(hook func(context.Context, int32, string, string) ([]api.RepoID, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *PermsStoreFetchReposByUserAndExternalServiceFunc) SetDefaultReturn(r0 []api.RepoID, r1 error) {
f.SetDefaultHook(func(context.Context, int32, string, string) ([]api.RepoID, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *PermsStoreFetchReposByUserAndExternalServiceFunc) PushReturn(r0 []api.RepoID, r1 error) {
f.PushHook(func(context.Context, int32, string, string) ([]api.RepoID, error) {
return r0, r1
})
}
func (f *PermsStoreFetchReposByUserAndExternalServiceFunc) nextHook() func(context.Context, int32, string, string) ([]api.RepoID, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *PermsStoreFetchReposByUserAndExternalServiceFunc) appendCall(r0 PermsStoreFetchReposByUserAndExternalServiceFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of
// PermsStoreFetchReposByUserAndExternalServiceFuncCall objects describing
// the invocations of this function.
func (f *PermsStoreFetchReposByUserAndExternalServiceFunc) History() []PermsStoreFetchReposByUserAndExternalServiceFuncCall {
f.mutex.Lock()
history := make([]PermsStoreFetchReposByUserAndExternalServiceFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// PermsStoreFetchReposByUserAndExternalServiceFuncCall is an object that
// describes an invocation of method FetchReposByUserAndExternalService on
// an instance of MockPermsStore.
type PermsStoreFetchReposByUserAndExternalServiceFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 int32
// Arg2 is the value of the 3rd argument passed to this method
// invocation.
Arg2 string
// Arg3 is the value of the 4th argument passed to this method
// invocation.
Arg3 string
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 []api.RepoID
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c PermsStoreFetchReposByUserAndExternalServiceFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c PermsStoreFetchReposByUserAndExternalServiceFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// PermsStoreGetUserIDsByExternalAccountsFunc describes the behavior when
// the GetUserIDsByExternalAccounts method of the parent MockPermsStore
// instance is invoked.

View File

@ -39,6 +39,9 @@ type PermsStore interface {
// LoadUserPermissions loads stored user permissions into p. An ErrPermsNotFound
// is returned when there are no valid permissions available.
LoadUserPermissions(ctx context.Context, p *authz.UserPermissions) error
// FetchReposByUserAndExternalService fetches repo ids that the given user can
// read and that originate from the given external service.
FetchReposByUserAndExternalService(ctx context.Context, userID int32, serviceType, serviceID string) ([]api.RepoID, error)
// LoadRepoPermissions loads stored repository permissions into p. An
// ErrPermsNotFound is returned when there are no valid permissions available.
LoadRepoPermissions(ctx context.Context, p *authz.RepoPermissions) error
@ -249,6 +252,45 @@ func (s *permsStore) LoadUserPermissions(ctx context.Context, p *authz.UserPermi
return nil
}
func (s *permsStore) FetchReposByUserAndExternalService(ctx context.Context, userID int32, serviceType, serviceID string) (ids []api.RepoID, err error) {
const format = `
-- source: enterprise/internal/database/perms_store.go:FetchReposByUserAndExternalService
SELECT id
FROM repo
WHERE external_service_id = %s
AND external_service_type = %s
AND id = ANY (ARRAY(SELECT object_ids_ints
FROM user_permissions
WHERE user_id = %s
AND permission = 'read'
AND object_type = 'repos'))
`
q := sqlf.Sprintf(
format,
serviceID,
serviceType,
userID,
)
ctx, save := s.observe(ctx, "FetchReposByUserAndExternalService", "")
defer func() {
save(&err)
}()
repos, err := basestore.ScanInt32s(s.Query(ctx, q))
if err != nil {
return nil, errors.Wrap(err, "scanning repo ids")
}
ids = make([]api.RepoID, 0, len(repos))
for _, id := range repos {
ids = append(ids, api.RepoID(id))
}
return ids, nil
}
func (s *permsStore) LoadRepoPermissions(ctx context.Context, p *authz.RepoPermissions) (err error) {
ctx, save := s.observe(ctx, "LoadRepoPermissions", "")
defer func() { save(&err, p.TracingFields()...) }()

View File

@ -257,6 +257,63 @@ func testPermsStore_LoadRepoPermissions(db database.DB) func(*testing.T) {
}
}
func testPermsStore_FetchReposByUserAndExternalService(db database.DB) func(*testing.T) {
return func(t *testing.T) {
t.Run("found matching", func(t *testing.T) {
ctx := context.Background()
s := perms(db, clock)
if _, err := db.ExecContext(ctx, `INSERT into repo (name, external_service_type, external_service_id) values ('github.com/test/test', 'github', 'https://github.com/')`); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
cleanupReposTable(t, s)
cleanupPermsTables(t, s)
})
rp := &authz.RepoPermissions{
RepoID: 1,
Perm: authz.Read,
UserIDs: toMapset(2),
}
if err := s.SetRepoPermissions(context.Background(), rp); err != nil {
t.Fatal(err)
}
repos, err := s.FetchReposByUserAndExternalService(ctx, 2, "github", "https://github.com/")
if err != nil {
t.Fatal(err)
}
equal(t, "repos", []api.RepoID{1}, repos)
})
t.Run("skips non matching", func(t *testing.T) {
ctx := context.Background()
s := perms(db, clock)
if _, err := db.ExecContext(ctx, `INSERT into repo (name, external_service_type, external_service_id) values ('github.com/test/test', 'github', 'https://github.com/')`); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
cleanupReposTable(t, s)
cleanupPermsTables(t, s)
})
rp := &authz.RepoPermissions{
RepoID: 1,
Perm: authz.Read,
UserIDs: toMapset(2),
}
if err := s.SetRepoPermissions(context.Background(), rp); err != nil {
t.Fatal(err)
}
repos, err := s.FetchReposByUserAndExternalService(ctx, 2, "gitlab", "https://gitlab.com/")
if err != nil {
t.Fatal(err)
}
equal(t, "repos", []api.RepoID{}, repos)
})
}
}
func checkRegularPermsTable(s *permsStore, sql string, expects map[int32][]uint32) error {
rows, err := s.Handle().QueryContext(context.Background(), sql)
if err != nil {

View File

@ -51,7 +51,7 @@ type SubRepoPermissions struct {
}
// ExternalUserPermissions is a collection of accessible repository/project IDs
// (on code host). It contains exact IDs, as well as prefixes to both include
// (on the code host). It contains exact IDs, as well as prefixes to both include
// and exclude IDs.
//
// 🚨 SECURITY: Every call site should evaluate all fields of this struct to

View File

@ -33615,6 +33615,9 @@ type MockSubRepoPermsStore struct {
// GetByUserFunc is an instance of a mock function object controlling
// the behavior of the method GetByUser.
GetByUserFunc *SubRepoPermsStoreGetByUserFunc
// GetByUserAndServiceFunc is an instance of a mock function object
// controlling the behavior of the method GetByUserAndService.
GetByUserAndServiceFunc *SubRepoPermsStoreGetByUserAndServiceFunc
// RepoIdSupportedFunc is an instance of a mock function object
// controlling the behavior of the method RepoIdSupported.
RepoIdSupportedFunc *SubRepoPermsStoreRepoIdSupportedFunc
@ -33655,6 +33658,11 @@ func NewMockSubRepoPermsStore() *MockSubRepoPermsStore {
return
},
},
GetByUserAndServiceFunc: &SubRepoPermsStoreGetByUserAndServiceFunc{
defaultHook: func(context.Context, int32, string, string) (r0 map[api.ExternalRepoSpec]authz.SubRepoPermissions, r1 error) {
return
},
},
RepoIdSupportedFunc: &SubRepoPermsStoreRepoIdSupportedFunc{
defaultHook: func(context.Context, api.RepoID) (r0 bool, r1 error) {
return
@ -33708,6 +33716,11 @@ func NewStrictMockSubRepoPermsStore() *MockSubRepoPermsStore {
panic("unexpected invocation of MockSubRepoPermsStore.GetByUser")
},
},
GetByUserAndServiceFunc: &SubRepoPermsStoreGetByUserAndServiceFunc{
defaultHook: func(context.Context, int32, string, string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error) {
panic("unexpected invocation of MockSubRepoPermsStore.GetByUserAndService")
},
},
RepoIdSupportedFunc: &SubRepoPermsStoreRepoIdSupportedFunc{
defaultHook: func(context.Context, api.RepoID) (bool, error) {
panic("unexpected invocation of MockSubRepoPermsStore.RepoIdSupported")
@ -33755,6 +33768,9 @@ func NewMockSubRepoPermsStoreFrom(i SubRepoPermsStore) *MockSubRepoPermsStore {
GetByUserFunc: &SubRepoPermsStoreGetByUserFunc{
defaultHook: i.GetByUser,
},
GetByUserAndServiceFunc: &SubRepoPermsStoreGetByUserAndServiceFunc{
defaultHook: i.GetByUserAndService,
},
RepoIdSupportedFunc: &SubRepoPermsStoreRepoIdSupportedFunc{
defaultHook: i.RepoIdSupported,
},
@ -34097,6 +34113,124 @@ func (c SubRepoPermsStoreGetByUserFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// SubRepoPermsStoreGetByUserAndServiceFunc describes the behavior when the
// GetByUserAndService method of the parent MockSubRepoPermsStore instance
// is invoked.
type SubRepoPermsStoreGetByUserAndServiceFunc struct {
defaultHook func(context.Context, int32, string, string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error)
hooks []func(context.Context, int32, string, string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error)
history []SubRepoPermsStoreGetByUserAndServiceFuncCall
mutex sync.Mutex
}
// GetByUserAndService delegates to the next hook function in the queue and
// stores the parameter and result values of this invocation.
func (m *MockSubRepoPermsStore) GetByUserAndService(v0 context.Context, v1 int32, v2 string, v3 string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error) {
r0, r1 := m.GetByUserAndServiceFunc.nextHook()(v0, v1, v2, v3)
m.GetByUserAndServiceFunc.appendCall(SubRepoPermsStoreGetByUserAndServiceFuncCall{v0, v1, v2, v3, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the GetByUserAndService
// method of the parent MockSubRepoPermsStore instance is invoked and the
// hook queue is empty.
func (f *SubRepoPermsStoreGetByUserAndServiceFunc) SetDefaultHook(hook func(context.Context, int32, string, string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// GetByUserAndService method of the parent MockSubRepoPermsStore instance
// invokes the hook at the front of the queue and discards it. After the
// queue is empty, the default hook function is invoked for any future
// action.
func (f *SubRepoPermsStoreGetByUserAndServiceFunc) PushHook(hook func(context.Context, int32, string, string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *SubRepoPermsStoreGetByUserAndServiceFunc) SetDefaultReturn(r0 map[api.ExternalRepoSpec]authz.SubRepoPermissions, r1 error) {
f.SetDefaultHook(func(context.Context, int32, string, string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *SubRepoPermsStoreGetByUserAndServiceFunc) PushReturn(r0 map[api.ExternalRepoSpec]authz.SubRepoPermissions, r1 error) {
f.PushHook(func(context.Context, int32, string, string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error) {
return r0, r1
})
}
func (f *SubRepoPermsStoreGetByUserAndServiceFunc) nextHook() func(context.Context, int32, string, string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *SubRepoPermsStoreGetByUserAndServiceFunc) appendCall(r0 SubRepoPermsStoreGetByUserAndServiceFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of
// SubRepoPermsStoreGetByUserAndServiceFuncCall objects describing the
// invocations of this function.
func (f *SubRepoPermsStoreGetByUserAndServiceFunc) History() []SubRepoPermsStoreGetByUserAndServiceFuncCall {
f.mutex.Lock()
history := make([]SubRepoPermsStoreGetByUserAndServiceFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// SubRepoPermsStoreGetByUserAndServiceFuncCall is an object that describes
// an invocation of method GetByUserAndService on an instance of
// MockSubRepoPermsStore.
type SubRepoPermsStoreGetByUserAndServiceFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 int32
// Arg2 is the value of the 3rd argument passed to this method
// invocation.
Arg2 string
// Arg3 is the value of the 4th argument passed to this method
// invocation.
Arg3 string
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 map[api.ExternalRepoSpec]authz.SubRepoPermissions
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c SubRepoPermsStoreGetByUserAndServiceFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c SubRepoPermsStoreGetByUserAndServiceFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// SubRepoPermsStoreRepoIdSupportedFunc describes the behavior when the
// RepoIdSupported method of the parent MockSubRepoPermsStore instance is
// invoked.

View File

@ -36,6 +36,9 @@ type SubRepoPermsStore interface {
UpsertWithSpec(ctx context.Context, userID int32, spec api.ExternalRepoSpec, perms authz.SubRepoPermissions) error
Get(ctx context.Context, userID int32, repoID api.RepoID) (*authz.SubRepoPermissions, error)
GetByUser(ctx context.Context, userID int32) (map[api.RepoName]authz.SubRepoPermissions, error)
// GetByUserAndService gets the sub repo permissions for a user, but filters down
// to only repos that come from a specific external service.
GetByUserAndService(ctx context.Context, userID int32, serviceType string, serviceID string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error)
RepoIdSupported(ctx context.Context, repoId api.RepoID) (bool, error)
RepoSupported(ctx context.Context, repo api.RepoName) (bool, error)
}
@ -173,6 +176,42 @@ WHERE user_id = %s
return result, nil
}
func (s *subRepoPermsStore) GetByUserAndService(ctx context.Context, userID int32, serviceType string, serviceID string) (map[api.ExternalRepoSpec]authz.SubRepoPermissions, error) {
q := sqlf.Sprintf(`
SELECT r.external_id, path_includes, path_excludes
FROM sub_repo_permissions
JOIN repo r on r.id = repo_id
WHERE user_id = %s
AND version = %s
AND r.external_service_type = %s
AND r.external_service_id = %s
`, userID, SubRepoPermsVersion, serviceType, serviceID)
rows, err := s.Query(ctx, q)
if err != nil {
return nil, errors.Wrap(err, "getting sub repo permissions by user")
}
result := make(map[api.ExternalRepoSpec]authz.SubRepoPermissions)
for rows.Next() {
var perms authz.SubRepoPermissions
spec := api.ExternalRepoSpec{
ServiceType: serviceType,
ServiceID: serviceID,
}
if err := rows.Scan(&spec.ID, pq.Array(&perms.PathIncludes), pq.Array(&perms.PathExcludes)); err != nil {
return nil, errors.Wrap(err, "scanning row")
}
result[spec] = perms
}
if err := rows.Close(); err != nil {
return nil, errors.Wrap(err, "closing rows")
}
return result, nil
}
// RepoIdSupported returns true if repo with the given ID has sub-repo permissions
// (i.e. it is private and its type is one of the SubRepoSupportedCodeHostTypes)
func (s *subRepoPermsStore) RepoIdSupported(ctx context.Context, repoId api.RepoID) (bool, error) {

View File

@ -184,6 +184,87 @@ func TestSubRepoPermsGetByUser(t *testing.T) {
}
}
func TestSubRepoPermsGetByUserAndService(t *testing.T) {
if testing.Short() {
t.Skip()
}
t.Parallel()
db := NewDB(dbtest.NewDB(t))
ctx := context.Background()
s := db.SubRepoPerms()
prepareSubRepoTestData(ctx, t, db)
userID := int32(1)
perms := authz.SubRepoPermissions{
PathIncludes: []string{"/src/foo/*"},
PathExcludes: []string{"/src/bar/*"},
}
if err := s.Upsert(ctx, userID, api.RepoID(1), perms); err != nil {
t.Fatal(err)
}
userID = int32(1)
perms = authz.SubRepoPermissions{
PathIncludes: []string{"/src/foo2/*"},
PathExcludes: []string{"/src/bar2/*"},
}
if err := s.Upsert(ctx, userID, api.RepoID(2), perms); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
userID int32
serviceType string
serviceID string
want map[api.ExternalRepoSpec]authz.SubRepoPermissions
}{
{
name: "Unknown service",
userID: userID,
serviceType: "abc",
serviceID: "xyz",
want: map[api.ExternalRepoSpec]authz.SubRepoPermissions{},
},
{
name: "Known service",
userID: userID,
serviceType: "github",
serviceID: "https://github.com/",
want: map[api.ExternalRepoSpec]authz.SubRepoPermissions{
{
ID: "MDEwOlJlcG9zaXRvcnk0MTI4ODcwOA==",
ServiceType: "github",
ServiceID: "https://github.com/",
}: {
PathIncludes: []string{"/src/foo/*"},
PathExcludes: []string{"/src/bar/*"},
},
{
ID: "MDEwOlJlcG9zaXRvcnk0MTI4ODcwOB==",
ServiceType: "github",
ServiceID: "https://github.com/",
}: {
PathIncludes: []string{"/src/foo2/*"},
PathExcludes: []string{"/src/bar2/*"},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
have, err := s.GetByUserAndService(ctx, userID, tc.serviceType, tc.serviceID)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(tc.want, have); diff != "" {
t.Fatal(diff)
}
})
}
}
func TestSubRepoPermsSupportedForRepoId(t *testing.T) {
if testing.Short() {
t.Skip()