mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
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:
parent
cb7ef85cd6
commit
9cc170eb51
@ -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))
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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)},
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()...) }()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user