authz: Move sub-repo perms to enterprise (#45659)

This commit is contained in:
Petri-Johan Last 2022-12-28 10:44:07 +02:00 committed by GitHub
parent ae0d88ce8a
commit e4a6c6aa7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 3398 additions and 3216 deletions

View File

@ -15,7 +15,6 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
@ -489,11 +488,6 @@ func TestRemoveStalePerforceAccount(t *testing.T) {
})
require.NoError(t, err)
require.Len(t, accounts, 1)
// We also want to add some fake sub-repo permissions and check that they are
// deleted
err = db.SubRepoPerms().Upsert(ctx, createdUser.ID, createdRepo.ID, authz.SubRepoPermissions{Paths: []string{"**"}})
require.NoError(t, err)
}
assertRemovals := func(t *testing.T) {

View File

@ -232,9 +232,6 @@ func TestSetUserEmailVerified(t *testing.T) {
userExternalAccounts := database.NewMockUserExternalAccountsStore()
userExternalAccounts.DeleteFunc.SetDefaultReturn(nil)
subrepoPerms := database.NewMockSubRepoPermsStore()
subrepoPerms.DeleteByUserFunc.SetDefaultReturn(nil)
db := database.NewMockDB()
db.TransactFunc.SetDefaultReturn(db, nil)
db.DoneFunc.SetDefaultHook(func(err error) error {
@ -245,7 +242,6 @@ func TestSetUserEmailVerified(t *testing.T) {
db.UserEmailsFunc.SetDefaultReturn(userEmails)
db.AuthzFunc.SetDefaultReturn(authz)
db.UserExternalAccountsFunc.SetDefaultReturn(userExternalAccounts)
db.SubRepoPermsFunc.SetDefaultReturn(subrepoPerms)
RunTests(t, test.gqlTests(db))

View File

@ -29,7 +29,6 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/siteid"
oce "github.com/sourcegraph/sourcegraph/cmd/frontend/oneclickexport"
"github.com/sourcegraph/sourcegraph/internal/adminanalytics"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/conf/conftypes"
"github.com/sourcegraph/sourcegraph/internal/conf/deploy"
@ -169,7 +168,6 @@ func Main(enterpriseSetupHook func(database.DB, conftypes.UnifiedWatchable) ente
// Run enterprise setup hook
enterprise := enterpriseSetupHook(db, conf.DefaultClient())
authz.DefaultSubRepoPermsChecker, err = authz.NewSubRepoPermsClient(db.SubRepoPerms())
if err != nil {
return errors.Wrap(err, "Failed to create sub-repo client")
}

View File

@ -10,5 +10,5 @@ func main() {
env.Lock()
env.HandleHelpFlag()
shared.Main()
shared.Main(nil)
}

View File

@ -75,7 +75,9 @@ var (
rateLimitSyncerLimitPerSecond = env.MustGetInt("SRC_REPOS_SYNC_RATE_LIMIT_RATE_PER_SECOND", 80, "Rate limit applied to rate limit syncing")
)
func Main() {
type EnterpriseInit func(db database.DB)
func Main(enterpriseInit EnterpriseInit) {
ctx := context.Background()
logging.Init() //nolint:staticcheck // Deprecated, but logs unmigrated to sourcegraph/log look really bad without this.
@ -127,7 +129,10 @@ func Main() {
logger.Fatal("failed to initialise keyring", log.Error(err))
}
authz.DefaultSubRepoPermsChecker, err = authz.NewSubRepoPermsClient(db.SubRepoPerms())
if enterpriseInit != nil {
enterpriseInit(db)
}
if err != nil {
logger.Fatal("Failed to create sub-repo client", log.Error(err))
}

View File

@ -23,7 +23,7 @@ func main() {
observationCtx := observation.NewContext(logger)
authz.SetProviders(true, []authz.Provider{})
if err := shared.Start(observationCtx, nil, nil); err != nil {
if err := shared.Start(observationCtx, nil, nil, nil); err != nil {
logger.Error(err.Error())
os.Exit(1)
}

View File

@ -3,9 +3,6 @@ package workerdb
import (
"database/sql"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/conf/conftypes"
"github.com/sourcegraph/sourcegraph/internal/database"
@ -33,10 +30,5 @@ var initDatabaseMemo = memo.NewMemoizedConstructorWithArg(func(observationCtx *o
return nil, errors.Errorf("failed to connect to frontend database: %s", err)
}
// ideally we could memoize the LRU cache only for this, and then create new clients on-demand with a passed-in observationCtx
authz.DefaultSubRepoPermsChecker, err = authz.NewSubRepoPermsClient(database.NewDB(log.Scoped("initDatabaseMemo", ""), db).SubRepoPerms())
if err != nil {
return nil, errors.Errorf("Failed to create sub-repo client: %v", err)
}
return db, nil
})

View File

@ -20,7 +20,9 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/worker/internal/webhooks"
"github.com/sourcegraph/sourcegraph/cmd/worker/internal/zoektrepos"
"github.com/sourcegraph/sourcegraph/cmd/worker/job"
workerdb "github.com/sourcegraph/sourcegraph/cmd/worker/shared/init/db"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/debugserver"
"github.com/sourcegraph/sourcegraph/internal/encryption/keyring"
"github.com/sourcegraph/sourcegraph/internal/env"
@ -37,8 +39,10 @@ import (
const addr = ":3189"
type EnterpriseInit = func(ossDB database.DB)
// Start runs the worker.
func Start(observationCtx *observation.Context, additionalJobs map[string]job.Job, registerEnterpriseMigrators oobmigration.RegisterMigratorsFunc) error {
func Start(observationCtx *observation.Context, additionalJobs map[string]job.Job, registerEnterpriseMigrators oobmigration.RegisterMigratorsFunc, enterpriseInit EnterpriseInit) error {
registerMigrators := oobmigration.ComposeRegisterMigratorsFuncs(migrations.RegisterOSSMigrators, registerEnterpriseMigrators)
builtins := map[string]job.Job{
@ -73,6 +77,15 @@ func Start(observationCtx *observation.Context, additionalJobs map[string]job.Jo
return errors.Wrap(err, "Failed to intialise keyring")
}
if enterpriseInit != nil {
db, err := workerdb.InitDB(observationCtx)
if err != nil {
return errors.Wrap(err, "Failed to create database connection")
}
enterpriseInit(db)
}
// Start debug server
ready := make(chan struct{})
go debugserver.NewServerRoutine(ready).Start()

View File

@ -17,6 +17,7 @@ allowed_prefix=(
github.com/sourcegraph/sourcegraph/cmd/repo-updater
github.com/sourcegraph/sourcegraph/cmd/migrator
github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend
github.com/sourcegraph/sourcegraph/enterprise/cmd/gitserver
github.com/sourcegraph/sourcegraph/enterprise/cmd/worker
github.com/sourcegraph/sourcegraph/enterprise/cmd/repo-updater
github.com/sourcegraph/sourcegraph/enterprise/cmd/migrator

View File

@ -108,6 +108,9 @@ Available commands in `sg.config.yaml`:
* loki
* monitoring-generator
* oss-frontend
* oss-gitserver-0
* oss-gitserver-1
* oss-gitserver-template
* oss-repo-updater
* oss-symbols
* oss-web: Open source version of the web app

View File

@ -16,6 +16,7 @@ import (
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/authz/webhooks"
"github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/licensing/enforcement"
eiauthz "github.com/sourcegraph/sourcegraph/enterprise/internal/authz"
srp "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms"
"github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel"
edb "github.com/sourcegraph/sourcegraph/enterprise/internal/database"
"github.com/sourcegraph/sourcegraph/enterprise/internal/licensing"
@ -29,6 +30,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/observation"
"github.com/sourcegraph/sourcegraph/internal/timeutil"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
var clock = timeutil.Now
@ -70,6 +72,12 @@ func Init(
enterpriseServices.PermissionsGitHubWebhook = webhooks.NewGitHubWebhook(log.Scoped("PermissionsGitHubWebhook", "permissions sync webhook handler for GitHub webhooks"))
var err error
authz.DefaultSubRepoPermsChecker, err = srp.NewSubRepoPermsClient(edb.NewEnterpriseDB(db).SubRepoPerms())
if err != nil {
return errors.Wrap(err, "Failed to createe sub-repo client")
}
// Warn about usage of authz providers that are not enabled by the license.
graphqlbackend.AlertFuncs = append(graphqlbackend.AlertFuncs, func(args graphqlbackend.AlertFuncArgs) []*graphqlbackend.Alert {
// Only site admins can act on this alert, so only show it to site admins.

View File

@ -238,11 +238,12 @@ func (r *Resolver) SetSubRepositoryPermissionsForUsers(ctx context.Context, args
return nil, err
}
db, err := r.db.Transact(ctx)
ossDB, err := r.db.Transact(ctx)
if err != nil {
return nil, errors.Wrap(err, "start transaction")
}
defer func() { err = db.Done(err) }()
defer func() { err = ossDB.Done(err) }()
db := edb.NewEnterpriseDB(ossDB)
// Make sure the repo ID is valid.
if _, err = db.Repos().Get(ctx, repoID); err != nil {

View File

@ -1335,7 +1335,7 @@ func TestResolver_SetSubRepositoryPermissionsForUsers(t *testing.T) {
users := database.NewStrictMockUserStore()
users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{}, nil)
subrepos := database.NewStrictMockSubRepoPermsStore()
subrepos := edb.NewStrictMockSubRepoPermsStore()
subrepos.UpsertFunc.SetDefaultHook(func(ctx context.Context, i int32, id api.RepoID, permissions authz.SubRepoPermissions) error {
return nil
})
@ -1373,7 +1373,7 @@ func TestResolver_SetSubRepositoryPermissionsForUsers(t *testing.T) {
}, nil
})
subReposStore := database.NewStrictMockSubRepoPermsStore()
subReposStore := edb.NewStrictMockSubRepoPermsStore()
subReposStore.UpsertFunc.SetDefaultHook(func(ctx context.Context, i int32, id api.RepoID, permissions authz.SubRepoPermissions) error {
return nil
})

View File

@ -0,0 +1,14 @@
package main
import (
"github.com/sourcegraph/sourcegraph/cmd/gitserver/shared"
enterprise_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/gitserver/shared"
"github.com/sourcegraph/sourcegraph/internal/env"
)
func main() {
env.Lock()
env.HandleHelpFlag()
shared.Main(enterprise_shared.EnterpriseInit)
}

View File

@ -0,0 +1,23 @@
// Package shared is the enterprise gitserrver program's shared main entrypoint.
//
// It lets the invoker of the OSS gitserver shared entrypoint inject a few
// proprietary things into it via e.g. blank/underscore imports in this file
// which register side effects with the gitserver package.
package shared
import (
"github.com/sourcegraph/log"
srp "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms"
edb "github.com/sourcegraph/sourcegraph/enterprise/internal/database"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/database"
)
func EnterpriseInit(db database.DB) {
logger := log.Scoped("enterprise", "gitserver enterprise edition")
var err error
authz.DefaultSubRepoPermsChecker, err = srp.NewSubRepoPermsClient(edb.NewEnterpriseDB(db).SubRepoPerms())
if err != nil {
logger.Fatal("Failed to create sub-repo client", log.Error(err))
}
}

View File

@ -10,10 +10,12 @@ import (
"github.com/sourcegraph/log"
eiauthz "github.com/sourcegraph/sourcegraph/enterprise/internal/authz"
srp "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms"
"github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel"
codeintelshared "github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel/shared"
"github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel/shared/lsifuploadstore"
"github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel/uploads"
edb "github.com/sourcegraph/sourcegraph/enterprise/internal/database"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/conf/conftypes"
@ -85,7 +87,7 @@ func Main() {
// Initialize sub-repo permissions client
var err error
authz.DefaultSubRepoPermsChecker, err = authz.NewSubRepoPermsClient(db.SubRepoPerms())
authz.DefaultSubRepoPermsChecker, err = srp.NewSubRepoPermsClient(edb.NewEnterpriseDB(db).SubRepoPerms())
if err != nil {
logger.Fatal("Failed to create sub-repo client", log.Error(err))
}

View File

@ -428,7 +428,7 @@ func (s *PermsSyncer) fetchUserPermsViaExternalAccounts(ctx context.Context, use
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())
currentSubRepoPerms, err := edb.NewEnterpriseDB(s.db).SubRepoPerms().GetByUserAndService(ctx, user.ID, provider.ServiceType(), provider.ServiceID())
if err != nil {
return results, errors.Wrap(err, "fetching existing sub-repo permissions")
}
@ -598,7 +598,7 @@ func (s *PermsSyncer) syncUserPerms(ctx context.Context, userID int32, noPerms b
}
// Set sub-repository permissions
srp := s.db.SubRepoPerms()
srp := edb.NewEnterpriseDB(s.db).SubRepoPerms()
for spec, perm := range results.subRepoPerms {
if err := srp.UpsertWithSpec(ctx, user.ID, spec, *perm); err != nil {
return providerStates, errors.Wrapf(err, "upserting sub repo perms %v for user %q (id: %d)", spec, user.Username, user.ID)

View File

@ -304,10 +304,10 @@ func TestPermsSyncer_syncUserPermsTemporaryProviderError(t *testing.T) {
return []*extsvc.Account{&extAccount}, nil
})
subRepoPerms := database.NewMockSubRepoPermsStore()
subRepoPerms := edb.NewMockSubRepoPermsStore()
subRepoPerms.GetByUserAndServiceFunc.SetDefaultReturn(nil, nil)
db := database.NewMockDB()
db := edb.NewMockEnterpriseDB()
db.UsersFunc.SetDefaultReturn(users)
db.ReposFunc.SetDefaultReturn(mockRepos)
db.UserEmailsFunc.SetDefaultReturn(userEmails)
@ -619,9 +619,9 @@ func TestPermsSyncer_syncUserPerms_subRepoPermissions(t *testing.T) {
externalAccounts := database.NewMockUserExternalAccountsStore()
externalAccounts.ListFunc.SetDefaultReturn([]*extsvc.Account{&extAccount}, nil)
subRepoPerms := database.NewMockSubRepoPermsStore()
subRepoPerms := edb.NewMockSubRepoPermsStore()
db := database.NewMockDB()
db := edb.NewMockEnterpriseDB()
db.UsersFunc.SetDefaultReturn(users)
db.ReposFunc.SetDefaultReturn(mockRepos)
db.ExternalServicesFunc.SetDefaultReturn(externalServices)

View File

@ -70,13 +70,12 @@ func startBackgroundPermsSync(ctx context.Context, syncer *authz.PermsSyncer, db
go func() {
t := time.NewTicker(frontendAuthz.RefreshInterval())
for range t.C {
allowAccessByDefault, authzProviders, _, _, _ :=
frontendAuthz.ProvidersFromConfig(
ctx,
conf.Get(),
db.ExternalServices(),
db,
)
allowAccessByDefault, authzProviders, _, _, _ := frontendAuthz.ProvidersFromConfig(
ctx,
conf.Get(),
db.ExternalServices(),
db,
)
ossAuthz.SetProviders(allowAccessByDefault, authzProviders)
}
}()

View File

@ -5,13 +5,30 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/worker/shared"
enterprise_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/worker/shared"
srp "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms"
edb "github.com/sourcegraph/sourcegraph/enterprise/internal/database"
"github.com/sourcegraph/sourcegraph/enterprise/internal/oobmigration/migrations"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/env"
"github.com/sourcegraph/sourcegraph/internal/observation"
"github.com/sourcegraph/sourcegraph/internal/oobmigration"
"github.com/sourcegraph/sourcegraph/internal/version"
)
func getEnterpriseInit(logger log.Logger) func(database.DB) {
return func(ossDB database.DB) {
enterpriseDB := edb.NewEnterpriseDB(ossDB)
var err error
authz.DefaultSubRepoPermsChecker, err = srp.NewSubRepoPermsClient(enterpriseDB.SubRepoPerms())
if err != nil {
logger.Fatal("Failed to create sub-repo client", log.Error(err))
}
}
}
func main() {
liblog := log.Init(log.Resource{
Name: env.MyName,
@ -24,7 +41,7 @@ func main() {
go enterprise_shared.SetAuthzProviders(observationCtx)
if err := shared.Start(observationCtx, enterprise_shared.AdditionalJobs, migrations.RegisterEnterpriseMigrators); err != nil {
if err := shared.Start(observationCtx, enterprise_shared.AdditionalJobs, migrations.RegisterEnterpriseMigrators, getEnterpriseInit(logger)); err != nil {
logger.Fatal(err.Error())
}
}

View File

@ -13,6 +13,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/sourcegraph/log/logtest"
srp "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
@ -624,7 +625,7 @@ read group Dev1 * //depot/main/.../*.go
} else if ok && tc.noRules {
t.Fatal("expected no rules")
}
checker, err := authz.NewSimpleChecker(api.RepoName(tc.depot), rules.Paths)
checker, err := srp.NewSimpleChecker(api.RepoName(tc.depot), rules.Paths)
if err != nil {
t.Fatal(err)
}

View File

@ -0,0 +1,429 @@
// Code generated by go-mockgen 1.3.7; DO NOT EDIT.
//
// This file was generated by running `sg generate` (or `go-mockgen`) at the root of
// this repository. To add additional mocks to this or another package, add a new entry
// to the mockgen.yaml file in the root of this repository.
package subrepoperms
import (
"context"
"sync"
api "github.com/sourcegraph/sourcegraph/internal/api"
authz "github.com/sourcegraph/sourcegraph/internal/authz"
)
// MockSubRepoPermissionsGetter is a mock implementation of the
// SubRepoPermissionsGetter interface (from the package
// github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms)
// used for unit testing.
type MockSubRepoPermissionsGetter struct {
// GetByUserFunc is an instance of a mock function object controlling
// the behavior of the method GetByUser.
GetByUserFunc *SubRepoPermissionsGetterGetByUserFunc
// RepoIDSupportedFunc is an instance of a mock function object
// controlling the behavior of the method RepoIDSupported.
RepoIDSupportedFunc *SubRepoPermissionsGetterRepoIDSupportedFunc
// RepoSupportedFunc is an instance of a mock function object
// controlling the behavior of the method RepoSupported.
RepoSupportedFunc *SubRepoPermissionsGetterRepoSupportedFunc
}
// NewMockSubRepoPermissionsGetter creates a new mock of the
// SubRepoPermissionsGetter interface. All methods return zero values for
// all results, unless overwritten.
func NewMockSubRepoPermissionsGetter() *MockSubRepoPermissionsGetter {
return &MockSubRepoPermissionsGetter{
GetByUserFunc: &SubRepoPermissionsGetterGetByUserFunc{
defaultHook: func(context.Context, int32) (r0 map[api.RepoName]authz.SubRepoPermissions, r1 error) {
return
},
},
RepoIDSupportedFunc: &SubRepoPermissionsGetterRepoIDSupportedFunc{
defaultHook: func(context.Context, api.RepoID) (r0 bool, r1 error) {
return
},
},
RepoSupportedFunc: &SubRepoPermissionsGetterRepoSupportedFunc{
defaultHook: func(context.Context, api.RepoName) (r0 bool, r1 error) {
return
},
},
}
}
// NewStrictMockSubRepoPermissionsGetter creates a new mock of the
// SubRepoPermissionsGetter interface. All methods panic on invocation,
// unless overwritten.
func NewStrictMockSubRepoPermissionsGetter() *MockSubRepoPermissionsGetter {
return &MockSubRepoPermissionsGetter{
GetByUserFunc: &SubRepoPermissionsGetterGetByUserFunc{
defaultHook: func(context.Context, int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
panic("unexpected invocation of MockSubRepoPermissionsGetter.GetByUser")
},
},
RepoIDSupportedFunc: &SubRepoPermissionsGetterRepoIDSupportedFunc{
defaultHook: func(context.Context, api.RepoID) (bool, error) {
panic("unexpected invocation of MockSubRepoPermissionsGetter.RepoIDSupported")
},
},
RepoSupportedFunc: &SubRepoPermissionsGetterRepoSupportedFunc{
defaultHook: func(context.Context, api.RepoName) (bool, error) {
panic("unexpected invocation of MockSubRepoPermissionsGetter.RepoSupported")
},
},
}
}
// NewMockSubRepoPermissionsGetterFrom creates a new mock of the
// MockSubRepoPermissionsGetter interface. All methods delegate to the given
// implementation, unless overwritten.
func NewMockSubRepoPermissionsGetterFrom(i SubRepoPermissionsGetter) *MockSubRepoPermissionsGetter {
return &MockSubRepoPermissionsGetter{
GetByUserFunc: &SubRepoPermissionsGetterGetByUserFunc{
defaultHook: i.GetByUser,
},
RepoIDSupportedFunc: &SubRepoPermissionsGetterRepoIDSupportedFunc{
defaultHook: i.RepoIDSupported,
},
RepoSupportedFunc: &SubRepoPermissionsGetterRepoSupportedFunc{
defaultHook: i.RepoSupported,
},
}
}
// SubRepoPermissionsGetterGetByUserFunc describes the behavior when the
// GetByUser method of the parent MockSubRepoPermissionsGetter instance is
// invoked.
type SubRepoPermissionsGetterGetByUserFunc struct {
defaultHook func(context.Context, int32) (map[api.RepoName]authz.SubRepoPermissions, error)
hooks []func(context.Context, int32) (map[api.RepoName]authz.SubRepoPermissions, error)
history []SubRepoPermissionsGetterGetByUserFuncCall
mutex sync.Mutex
}
// GetByUser delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockSubRepoPermissionsGetter) GetByUser(v0 context.Context, v1 int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
r0, r1 := m.GetByUserFunc.nextHook()(v0, v1)
m.GetByUserFunc.appendCall(SubRepoPermissionsGetterGetByUserFuncCall{v0, v1, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the GetByUser method of
// the parent MockSubRepoPermissionsGetter instance is invoked and the hook
// queue is empty.
func (f *SubRepoPermissionsGetterGetByUserFunc) SetDefaultHook(hook func(context.Context, int32) (map[api.RepoName]authz.SubRepoPermissions, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// GetByUser method of the parent MockSubRepoPermissionsGetter 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 *SubRepoPermissionsGetterGetByUserFunc) PushHook(hook func(context.Context, int32) (map[api.RepoName]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 *SubRepoPermissionsGetterGetByUserFunc) SetDefaultReturn(r0 map[api.RepoName]authz.SubRepoPermissions, r1 error) {
f.SetDefaultHook(func(context.Context, int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *SubRepoPermissionsGetterGetByUserFunc) PushReturn(r0 map[api.RepoName]authz.SubRepoPermissions, r1 error) {
f.PushHook(func(context.Context, int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
return r0, r1
})
}
func (f *SubRepoPermissionsGetterGetByUserFunc) nextHook() func(context.Context, int32) (map[api.RepoName]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 *SubRepoPermissionsGetterGetByUserFunc) appendCall(r0 SubRepoPermissionsGetterGetByUserFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of SubRepoPermissionsGetterGetByUserFuncCall
// objects describing the invocations of this function.
func (f *SubRepoPermissionsGetterGetByUserFunc) History() []SubRepoPermissionsGetterGetByUserFuncCall {
f.mutex.Lock()
history := make([]SubRepoPermissionsGetterGetByUserFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// SubRepoPermissionsGetterGetByUserFuncCall is an object that describes an
// invocation of method GetByUser on an instance of
// MockSubRepoPermissionsGetter.
type SubRepoPermissionsGetterGetByUserFuncCall 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
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 map[api.RepoName]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 SubRepoPermissionsGetterGetByUserFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c SubRepoPermissionsGetterGetByUserFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// SubRepoPermissionsGetterRepoIDSupportedFunc describes the behavior when
// the RepoIDSupported method of the parent MockSubRepoPermissionsGetter
// instance is invoked.
type SubRepoPermissionsGetterRepoIDSupportedFunc struct {
defaultHook func(context.Context, api.RepoID) (bool, error)
hooks []func(context.Context, api.RepoID) (bool, error)
history []SubRepoPermissionsGetterRepoIDSupportedFuncCall
mutex sync.Mutex
}
// RepoIDSupported delegates to the next hook function in the queue and
// stores the parameter and result values of this invocation.
func (m *MockSubRepoPermissionsGetter) RepoIDSupported(v0 context.Context, v1 api.RepoID) (bool, error) {
r0, r1 := m.RepoIDSupportedFunc.nextHook()(v0, v1)
m.RepoIDSupportedFunc.appendCall(SubRepoPermissionsGetterRepoIDSupportedFuncCall{v0, v1, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the RepoIDSupported
// method of the parent MockSubRepoPermissionsGetter instance is invoked and
// the hook queue is empty.
func (f *SubRepoPermissionsGetterRepoIDSupportedFunc) SetDefaultHook(hook func(context.Context, api.RepoID) (bool, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// RepoIDSupported method of the parent MockSubRepoPermissionsGetter
// 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 *SubRepoPermissionsGetterRepoIDSupportedFunc) PushHook(hook func(context.Context, api.RepoID) (bool, 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 *SubRepoPermissionsGetterRepoIDSupportedFunc) SetDefaultReturn(r0 bool, r1 error) {
f.SetDefaultHook(func(context.Context, api.RepoID) (bool, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *SubRepoPermissionsGetterRepoIDSupportedFunc) PushReturn(r0 bool, r1 error) {
f.PushHook(func(context.Context, api.RepoID) (bool, error) {
return r0, r1
})
}
func (f *SubRepoPermissionsGetterRepoIDSupportedFunc) nextHook() func(context.Context, api.RepoID) (bool, 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 *SubRepoPermissionsGetterRepoIDSupportedFunc) appendCall(r0 SubRepoPermissionsGetterRepoIDSupportedFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of
// SubRepoPermissionsGetterRepoIDSupportedFuncCall objects describing the
// invocations of this function.
func (f *SubRepoPermissionsGetterRepoIDSupportedFunc) History() []SubRepoPermissionsGetterRepoIDSupportedFuncCall {
f.mutex.Lock()
history := make([]SubRepoPermissionsGetterRepoIDSupportedFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// SubRepoPermissionsGetterRepoIDSupportedFuncCall is an object that
// describes an invocation of method RepoIDSupported on an instance of
// MockSubRepoPermissionsGetter.
type SubRepoPermissionsGetterRepoIDSupportedFuncCall 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 api.RepoID
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 bool
// 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 SubRepoPermissionsGetterRepoIDSupportedFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c SubRepoPermissionsGetterRepoIDSupportedFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// SubRepoPermissionsGetterRepoSupportedFunc describes the behavior when the
// RepoSupported method of the parent MockSubRepoPermissionsGetter instance
// is invoked.
type SubRepoPermissionsGetterRepoSupportedFunc struct {
defaultHook func(context.Context, api.RepoName) (bool, error)
hooks []func(context.Context, api.RepoName) (bool, error)
history []SubRepoPermissionsGetterRepoSupportedFuncCall
mutex sync.Mutex
}
// RepoSupported delegates to the next hook function in the queue and stores
// the parameter and result values of this invocation.
func (m *MockSubRepoPermissionsGetter) RepoSupported(v0 context.Context, v1 api.RepoName) (bool, error) {
r0, r1 := m.RepoSupportedFunc.nextHook()(v0, v1)
m.RepoSupportedFunc.appendCall(SubRepoPermissionsGetterRepoSupportedFuncCall{v0, v1, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the RepoSupported method
// of the parent MockSubRepoPermissionsGetter instance is invoked and the
// hook queue is empty.
func (f *SubRepoPermissionsGetterRepoSupportedFunc) SetDefaultHook(hook func(context.Context, api.RepoName) (bool, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// RepoSupported method of the parent MockSubRepoPermissionsGetter 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 *SubRepoPermissionsGetterRepoSupportedFunc) PushHook(hook func(context.Context, api.RepoName) (bool, 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 *SubRepoPermissionsGetterRepoSupportedFunc) SetDefaultReturn(r0 bool, r1 error) {
f.SetDefaultHook(func(context.Context, api.RepoName) (bool, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *SubRepoPermissionsGetterRepoSupportedFunc) PushReturn(r0 bool, r1 error) {
f.PushHook(func(context.Context, api.RepoName) (bool, error) {
return r0, r1
})
}
func (f *SubRepoPermissionsGetterRepoSupportedFunc) nextHook() func(context.Context, api.RepoName) (bool, 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 *SubRepoPermissionsGetterRepoSupportedFunc) appendCall(r0 SubRepoPermissionsGetterRepoSupportedFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of
// SubRepoPermissionsGetterRepoSupportedFuncCall objects describing the
// invocations of this function.
func (f *SubRepoPermissionsGetterRepoSupportedFunc) History() []SubRepoPermissionsGetterRepoSupportedFuncCall {
f.mutex.Lock()
history := make([]SubRepoPermissionsGetterRepoSupportedFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// SubRepoPermissionsGetterRepoSupportedFuncCall is an object that describes
// an invocation of method RepoSupported on an instance of
// MockSubRepoPermissionsGetter.
type SubRepoPermissionsGetterRepoSupportedFuncCall 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 api.RepoName
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 bool
// 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 SubRepoPermissionsGetterRepoSupportedFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c SubRepoPermissionsGetterRepoSupportedFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}

View File

@ -0,0 +1,401 @@
package subrepoperms
import (
"context"
"strconv"
"strings"
"time"
"github.com/gobwas/glob"
lru "github.com/hashicorp/golang-lru"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"go.uber.org/atomic"
"golang.org/x/sync/singleflight"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
// SubRepoPermissionsGetter allows getting sub repository permissions.
type SubRepoPermissionsGetter interface {
// GetByUser returns the sub repository permissions rules known for a user.
GetByUser(ctx context.Context, userID int32) (map[api.RepoName]authz.SubRepoPermissions, error)
// RepoIDSupported returns true if repo with the given ID has sub-repo permissions.
RepoIDSupported(ctx context.Context, repoID api.RepoID) (bool, error)
// RepoSupported returns true if repo with the given name has sub-repo permissions.
RepoSupported(ctx context.Context, repo api.RepoName) (bool, error)
}
// SubRepoPermsClient is a concrete implementation of SubRepoPermissionChecker.
// Always use NewSubRepoPermsClient to instantiate an instance.
type SubRepoPermsClient struct {
permissionsGetter SubRepoPermissionsGetter
clock func() time.Time
since func(time.Time) time.Duration
group *singleflight.Group
cache *lru.Cache
enabled *atomic.Bool
}
const (
defaultCacheSize = 1000
defaultCacheTTL = 10 * time.Second
)
// cachedRules caches the perms rules known for a particular user by repo.
type cachedRules struct {
rules map[api.RepoName]compiledRules
timestamp time.Time
}
type path struct {
globPath glob.Glob
exclusion bool
// the original rule before it was compiled into a glob matcher
original string
}
type compiledRules struct {
paths []path
}
// GetPermissionsForPath tries to match a given path to a list of rules.
// Since the last applicable rule is the one that applies, the list is
// traversed in reverse, and the function returns as soon as a match is found.
// If no match is found, None is returned.
func (rules compiledRules) GetPermissionsForPath(path string) authz.Perms {
for i := len(rules.paths) - 1; i >= 0; i-- {
if rules.paths[i].globPath.Match(path) {
if rules.paths[i].exclusion {
return authz.None
}
return authz.Read
}
}
// Return None if no rule matches
return authz.None
}
// NewSubRepoPermsClient instantiates an instance of authz.SubRepoPermsClient
// which implements SubRepoPermissionChecker.
//
// SubRepoPermissionChecker is responsible for checking whether a user has access
// to data within a repo. Sub-repository permissions enforcement is on top of
// existing repository permissions, which means the user must already have access
// to the repository itself. The intention is for this client to be created once
// at startup and passed in to all places that need to check sub repo
// permissions.
//
// Note that sub-repo permissions are currently opt-in via the
// experimentalFeatures.enableSubRepoPermissions option.
func NewSubRepoPermsClient(permissionsGetter SubRepoPermissionsGetter) (*SubRepoPermsClient, error) {
cache, err := lru.New(defaultCacheSize)
if err != nil {
return nil, errors.Wrap(err, "creating LRU cache")
}
enabled := atomic.NewBool(false)
conf.Watch(func() {
c := conf.Get()
if c.ExperimentalFeatures == nil || c.ExperimentalFeatures.SubRepoPermissions == nil {
enabled.Store(false)
return
}
cacheSize := c.ExperimentalFeatures.SubRepoPermissions.UserCacheSize
if cacheSize == 0 {
cacheSize = defaultCacheSize
}
cache.Resize(cacheSize)
enabled.Store(c.ExperimentalFeatures.SubRepoPermissions.Enabled)
})
return &SubRepoPermsClient{
permissionsGetter: permissionsGetter,
clock: time.Now,
since: time.Since,
group: &singleflight.Group{},
cache: cache,
enabled: enabled,
}, nil
}
var (
metricSubRepoPermsPermissionsDurationSuccess prometheus.Observer
metricSubRepoPermsPermissionsDurationError prometheus.Observer
)
func init() {
// We cache the result of WithLabelValues since we call them in
// performance sensitive code. See BenchmarkFilterActorPaths.
metric := promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authz_sub_repo_perms_permissions_duration_seconds",
Help: "Time spent calculating permissions of a file for an actor.",
}, []string{"error"})
metricSubRepoPermsPermissionsDurationSuccess = metric.WithLabelValues("false")
metricSubRepoPermsPermissionsDurationError = metric.WithLabelValues("true")
}
var (
metricSubRepoPermCacheHit prometheus.Counter
metricSubRepoPermCacheMiss prometheus.Counter
)
func init() {
// We cache the result of WithLabelValues since we call them in
// performance sensitive code. See BenchmarkFilterActorPaths.
metric := promauto.NewCounterVec(prometheus.CounterOpts{
Name: "authz_sub_repo_perms_permissions_cache_count",
Help: "The number of sub-repo perms cache hits or misses",
}, []string{"hit"})
metricSubRepoPermCacheHit = metric.WithLabelValues("true")
metricSubRepoPermCacheMiss = metric.WithLabelValues("false")
}
// Permissions return the current permissions granted to the given user on the
// given content. If sub-repo permissions are disabled, it is a no-op that return
// Read.
func (s *SubRepoPermsClient) Permissions(ctx context.Context, userID int32, content authz.RepoContent) (perms authz.Perms, err error) {
// Are sub-repo permissions enabled at the site level
if !s.Enabled() {
return authz.Read, nil
}
began := time.Now()
defer func() {
took := time.Since(began).Seconds()
if err == nil {
metricSubRepoPermsPermissionsDurationSuccess.Observe(took)
} else {
metricSubRepoPermsPermissionsDurationError.Observe(took)
}
}()
f, err := s.FilePermissionsFunc(ctx, userID, content.Repo)
if err != nil {
return authz.None, err
}
return f(content.Path)
}
// filePermissionsFuncAllRead is a FilePermissionFunc which _always_ returns
// Read. Only use in cases that sub repo permission checks should not be done.
func filePermissionsFuncAllRead(_ string) (authz.Perms, error) {
return authz.Read, nil
}
func (s *SubRepoPermsClient) FilePermissionsFunc(ctx context.Context, userID int32, repo api.RepoName) (authz.FilePermissionFunc, error) {
// Are sub-repo permissions enabled at the site level
if !s.Enabled() {
return filePermissionsFuncAllRead, nil
}
if s.permissionsGetter == nil {
return nil, errors.New("permissionsGetter is nil")
}
if userID == 0 {
return nil, &authz.ErrUnauthenticated{}
}
repoRules, err := s.getCompiledRules(ctx, userID)
if err != nil {
return nil, errors.Wrap(err, "compiling match rules")
}
rules, rulesExist := repoRules[repo]
if !rulesExist {
// If we make it this far it implies that we have access at the repo level.
// Having any empty set of rules here implies that we can access the whole repo.
// Repos that support sub-repo permissions will only have an entry in our
// repo_permissions table after all sub-repo permissions have been processed.
return filePermissionsFuncAllRead, nil
}
return func(path string) (authz.Perms, error) {
// An empty path is equivalent to repo permissions so we can assume it has
// already been checked at that level.
if path == "" {
return authz.Read, nil
}
// Prefix path with "/", otherwise suffix rules like "**/file.txt" won't match
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
// Iterate through all rules for the current path, and the final match takes
// preference.
return rules.GetPermissionsForPath(path), nil
}, nil
}
// getCompiledRules fetches rules for the given repo with caching.
func (s *SubRepoPermsClient) getCompiledRules(ctx context.Context, userID int32) (map[api.RepoName]compiledRules, error) {
// Fast path for cached rules
item, _ := s.cache.Get(userID)
cached, ok := item.(cachedRules)
ttl := defaultCacheTTL
if c := conf.Get(); c.ExperimentalFeatures != nil && c.ExperimentalFeatures.SubRepoPermissions != nil && c.ExperimentalFeatures.SubRepoPermissions.UserCacheTTLSeconds > 0 {
ttl = time.Duration(c.ExperimentalFeatures.SubRepoPermissions.UserCacheTTLSeconds) * time.Second
}
if ok && s.since(cached.timestamp) <= ttl {
metricSubRepoPermCacheHit.Inc()
return cached.rules, nil
}
metricSubRepoPermCacheMiss.Inc()
// Slow path on cache miss or expiry. Ensure that only one goroutine is doing the
// work
groupKey := strconv.FormatInt(int64(userID), 10)
result, err, _ := s.group.Do(groupKey, func() (any, error) {
repoPerms, err := s.permissionsGetter.GetByUser(ctx, userID)
if err != nil {
return nil, errors.Wrap(err, "fetching rules")
}
toCache := cachedRules{
rules: make(map[api.RepoName]compiledRules, len(repoPerms)),
}
for repo, perms := range repoPerms {
paths := make([]path, 0, len(perms.Paths))
for _, rule := range perms.Paths {
exclusion := strings.HasPrefix(rule, "-")
rule = strings.TrimPrefix(rule, "-")
if !strings.HasPrefix(rule, "/") {
rule = "/" + rule
}
g, err := glob.Compile(rule, '/')
if err != nil {
return nil, errors.Wrap(err, "building include matcher")
}
paths = append(paths, path{globPath: g, exclusion: exclusion, original: rule})
// Special case. Our glob package does not handle rules starting with a double
// wildcard correctly. For example, we would expect `/**/*.java` to match all
// java files, but it does not match files at the root, eg `/foo.java`. To get
// around this we add an extra rule to cover this case.
if strings.HasPrefix(rule, "/**/") {
trimmed := rule
for {
trimmed = strings.TrimPrefix(trimmed, "/**")
if strings.HasPrefix(trimmed, "/**/") {
// Keep trimming
continue
}
g, err := glob.Compile(trimmed, '/')
if err != nil {
return nil, errors.Wrap(err, "building include matcher")
}
paths = append(paths, path{globPath: g, exclusion: exclusion, original: trimmed})
break
}
}
// We should include all directories above an include rule so that we can browse
// to the included items.
if exclusion {
// Not required for an exclude rule
continue
}
dirs := expandDirs(rule)
for _, dir := range dirs {
g, err := glob.Compile(dir, '/')
if err != nil {
return nil, errors.Wrap(err, "building include matcher for dir")
}
paths = append(paths, path{globPath: g, exclusion: false, original: dir})
}
}
toCache.rules[repo] = compiledRules{
paths: paths,
}
}
toCache.timestamp = s.clock()
s.cache.Add(userID, toCache)
return toCache.rules, nil
})
if err != nil {
return nil, err
}
compiled := result.(map[api.RepoName]compiledRules)
return compiled, nil
}
func (s *SubRepoPermsClient) Enabled() bool {
return s.enabled.Load()
}
func (s *SubRepoPermsClient) EnabledForRepoID(ctx context.Context, id api.RepoID) (bool, error) {
return s.permissionsGetter.RepoIDSupported(ctx, id)
}
func (s *SubRepoPermsClient) EnabledForRepo(ctx context.Context, repo api.RepoName) (bool, error) {
return s.permissionsGetter.RepoSupported(ctx, repo)
}
// expandDirs will return a new set of rules that will match all directories
// above the supplied rule. As a special case, if the rule starts with a wildcard
// we return a rule to match all directories.
func expandDirs(rule string) []string {
dirs := make([]string, 0)
// Make sure the rule starts with a slash
if !strings.HasPrefix(rule, "/") {
rule = "/" + rule
}
// If a rule starts with a wildcard it can match at any level in the tree
// structure so there's no way of walking up the tree and expand out to the list
// of valid directories. Instead, we just return a rule that matches any
// directory
if strings.HasPrefix(rule, "/*") {
dirs = append(dirs, "**/")
return dirs
}
for {
lastSlash := strings.LastIndex(rule, "/")
if lastSlash <= 0 { // we have to ignore the slash at index 0
break
}
// Drop anything after the last slash
rule = rule[:lastSlash]
dirs = append(dirs, rule+"/")
}
return dirs
}
// NewSimpleChecker is exposed for testing and allows creation of a simple
// checker based on the rules provided. The rules are expected to be in glob
// format.
func NewSimpleChecker(repo api.RepoName, paths []string) (authz.SubRepoPermissionChecker, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
return map[api.RepoName]authz.SubRepoPermissions{
repo: {
Paths: paths,
},
}, nil
})
getter.RepoSupportedFunc.SetDefaultReturn(true, nil)
getter.RepoIDSupportedFunc.SetDefaultReturn(true, nil)
return NewSubRepoPermsClient(getter)
}

View File

@ -0,0 +1,430 @@
package subrepoperms
import (
"context"
"fmt"
"sort"
"testing"
"time"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/schema"
)
func TestSubRepoPermsPermissions(t *testing.T) {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
t.Cleanup(func() { conf.Mock(nil) })
testCases := []struct {
name string
userID int32
content authz.RepoContent
clientFn func() (*SubRepoPermsClient, error)
want authz.Perms
}{
{
name: "Empty path",
userID: 1,
content: authz.RepoContent{
Repo: "sample",
Path: "",
},
clientFn: func() (*SubRepoPermsClient, error) {
return NewSubRepoPermsClient(NewMockSubRepoPermissionsGetter())
},
want: authz.Read,
},
{
name: "No rules",
userID: 1,
content: authz.RepoContent{
Repo: "sample",
Path: "/dev/thing",
},
clientFn: func() (*SubRepoPermsClient, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
return map[api.RepoName]authz.SubRepoPermissions{
"sample": {
Paths: []string{},
},
}, nil
})
return NewSubRepoPermsClient(getter)
},
want: authz.None,
},
{
name: "Exclude",
userID: 1,
content: authz.RepoContent{
Repo: "sample",
Path: "/dev/thing",
},
clientFn: func() (*SubRepoPermsClient, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
return map[api.RepoName]authz.SubRepoPermissions{
"sample": {
Paths: []string{"-/dev/*"},
},
}, nil
})
return NewSubRepoPermsClient(getter)
},
want: authz.None,
},
{
name: "Include",
userID: 1,
content: authz.RepoContent{
Repo: "sample",
Path: "/dev/thing",
},
clientFn: func() (*SubRepoPermsClient, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
return map[api.RepoName]authz.SubRepoPermissions{
"sample": {
Paths: []string{"/*"},
},
}, nil
})
return NewSubRepoPermsClient(getter)
},
want: authz.None,
},
{
name: "Last rule takes precedence (exclude)",
userID: 1,
content: authz.RepoContent{
Repo: "sample",
Path: "/dev/thing",
},
clientFn: func() (*SubRepoPermsClient, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
return map[api.RepoName]authz.SubRepoPermissions{
"sample": {
Paths: []string{"/**", "-/dev/*"},
},
}, nil
})
return NewSubRepoPermsClient(getter)
},
want: authz.None,
},
{
name: "Last rule takes precedence (include)",
userID: 1,
content: authz.RepoContent{
Repo: "sample",
Path: "/dev/thing",
},
clientFn: func() (*SubRepoPermsClient, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
return map[api.RepoName]authz.SubRepoPermissions{
"sample": {
Paths: []string{"-/dev/*", "/**"},
},
}, nil
})
return NewSubRepoPermsClient(getter)
},
want: authz.Read,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client, err := tc.clientFn()
if err != nil {
t.Fatal(err)
}
have, err := client.Permissions(context.Background(), tc.userID, tc.content)
if err != nil {
t.Fatal(err)
}
if have != tc.want {
t.Fatalf("have %v, want %v", have, tc.want)
}
})
}
}
func BenchmarkFilterActorPaths(b *testing.B) {
// This benchmark is simulating the code path taken by a monorepo with sub
// repo permissions. Our goal is to support repos with millions of files.
// For now we target a lower number since large numbers don't give enough
// runs of the benchmark to be useful.
const pathCount = 5_000
pathPatterns := []string{
"base/%d/foo.go",
"%d/stuff/baz",
"frontend/%d/stuff/baz/bam",
"subdir/sub/sub/sub/%d",
"%d/foo/README.md",
"subdir/remove/me/please/%d",
"subdir/%d/also-remove/me/please",
"a/deep/path/%d/.secrets.env",
"%d/does/not/match/anything",
"does/%d/not/match/anything",
"does/not/%d/match/anything",
"does/not/match/%d/anything",
"does/not/match/anything/%d",
}
paths := []string{
"config.yaml",
"dir.yaml",
}
for i := 0; len(paths) < pathCount; i++ {
for _, pat := range pathPatterns {
paths = append(paths, fmt.Sprintf(pat, i))
}
}
paths = paths[:pathCount]
sort.Strings(paths)
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
defer conf.Mock(nil)
repo := api.RepoName("repo")
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
return map[api.RepoName]authz.SubRepoPermissions{
repo: {
Paths: []string{
"/base/**",
"/*/stuff/**",
"/frontend/**/stuff/*",
"/config.yaml",
"/subdir/**",
"/**/README.md",
"/dir.yaml",
"-/subdir/remove/",
"-/subdir/*/also-remove/**",
"-/**/.secrets.env",
},
},
}, nil
})
checker, err := NewSubRepoPermsClient(getter)
if err != nil {
b.Fatal(err)
}
a := &actor.Actor{
UID: 1,
}
ctx := actor.WithActor(context.Background(), a)
b.ResetTimer()
start := time.Now()
for n := 0; n <= b.N; n++ {
filtered, err := authz.FilterActorPaths(ctx, checker, a, repo, paths)
if err != nil {
b.Fatal(err)
}
if len(filtered) == 0 {
b.Fatal("expected paths to be returned")
}
if len(filtered) == len(paths) {
b.Fatal("expected to filter out some paths")
}
}
b.ReportMetric(float64(len(paths))*float64(b.N)/time.Since(start).Seconds(), "paths/s")
}
func TestSubRepoPermissionsCanReadDirectoriesInPath(t *testing.T) {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
t.Cleanup(func() { conf.Mock(nil) })
repoName := api.RepoName("repo")
testCases := []struct {
paths []string
canReadAll []string
cannotReadAny []string
}{
{
paths: []string{"foo/bar/thing.txt"},
canReadAll: []string{"foo/", "foo/bar/"},
cannotReadAny: []string{"foo/thing.txt", "foo/bar/other.txt"},
},
{
paths: []string{"foo/bar/**"},
canReadAll: []string{"foo/", "foo/bar/", "foo/bar/baz/", "foo/bar/baz/fox/"},
},
{
paths: []string{"foo/bar/"},
canReadAll: []string{"foo/", "foo/bar/"},
cannotReadAny: []string{"foo/thing.txt", "foo/bar/thing.txt"},
},
{
paths: []string{"baz/*/foo/bar/thing.txt"},
canReadAll: []string{"baz/", "baz/x/", "baz/x/foo/bar/"},
cannotReadAny: []string{"baz/thing.txt"},
},
// If we have a wildcard in a path we allow all directories that are not
// explicitly excluded.
{
paths: []string{"**/foo/bar/thing.txt"},
canReadAll: []string{"foo/", "foo/bar/"},
},
{
paths: []string{"*/foo/bar/thing.txt"},
canReadAll: []string{"foo/", "foo/bar/"},
},
{
paths: []string{"/**/foo/bar/thing.txt"},
canReadAll: []string{"foo/", "foo/bar/"},
},
{
paths: []string{"/*/foo/bar/thing.txt"},
canReadAll: []string{"foo/", "foo/bar/"},
},
{
paths: []string{"-/**", "/storage/redis/**"},
canReadAll: []string{"storage/", "/storage/", "/storage/redis/"},
},
{
paths: []string{"-/**", "-/storage/**", "/storage/redis/**"},
canReadAll: []string{"storage/", "/storage/", "/storage/redis/"},
},
// Even with a wildcard include rule, we should still exclude directories that
// are explicitly excluded later
{
paths: []string{"/**", "-/storage/**"},
canReadAll: []string{"/foo"},
cannotReadAny: []string{"storage/", "/storage/", "/storage/redis/"},
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]authz.SubRepoPermissions, error) {
return map[api.RepoName]authz.SubRepoPermissions{
repoName: {
Paths: tc.paths,
},
}, nil
})
client, err := NewSubRepoPermsClient(getter)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
for _, path := range tc.canReadAll {
content := authz.RepoContent{
Repo: repoName,
Path: path,
}
perm, err := client.Permissions(ctx, 1, content)
if err != nil {
t.Error(err)
}
if !perm.Include(authz.Read) {
t.Errorf("Should be able to read %q, cannot", path)
}
}
for _, path := range tc.cannotReadAny {
content := authz.RepoContent{
Repo: repoName,
Path: path,
}
perm, err := client.Permissions(ctx, 1, content)
if err != nil {
t.Error(err)
}
if perm.Include(authz.Read) {
t.Errorf("Should not be able to read %q, can", path)
}
}
})
}
}
func TestSubRepoPermsPermissionsCache(t *testing.T) {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
t.Cleanup(func() { conf.Mock(nil) })
getter := NewMockSubRepoPermissionsGetter()
client, err := NewSubRepoPermsClient(getter)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
content := authz.RepoContent{
Repo: api.RepoName("thing"),
Path: "/stuff",
}
// Should hit DB only once
for i := 0; i < 3; i++ {
_, err = client.Permissions(ctx, 1, content)
if err != nil {
t.Fatal(err)
}
h := getter.GetByUserFunc.History()
if len(h) != 1 {
t.Fatal("Should have been called once")
}
}
// Trigger expiry
client.since = func(time time.Time) time.Duration {
return defaultCacheTTL + 1
}
_, err = client.Permissions(ctx, 1, content)
if err != nil {
t.Fatal(err)
}
h := getter.GetByUserFunc.History()
if len(h) != 2 {
t.Fatal("Should have been called twice")
}
}

View File

@ -16,10 +16,11 @@ import (
// NewAuthzStore returns an OSS database.AuthzStore set with enterprise implementation.
func NewAuthzStore(logger log.Logger, db database.DB, clock func() time.Time) database.AuthzStore {
enterpriseDB := NewEnterpriseDB(db)
return &authzStore{
logger: logger,
store: Perms(logger, db, clock),
srpStore: database.SubRepoPermsWith(basestore.NewWithHandle(db.Handle())),
store: Perms(logger, enterpriseDB, clock),
srpStore: enterpriseDB.SubRepoPerms(),
}
}
@ -27,14 +28,14 @@ func NewAuthzStoreWith(logger log.Logger, other basestore.ShareableStore, clock
return &authzStore{
logger: logger,
store: PermsWith(logger, other, clock),
srpStore: database.SubRepoPermsWith(other),
srpStore: SubRepoPermsWith(other),
}
}
type authzStore struct {
logger log.Logger
store PermsStore
srpStore database.SubRepoPermsStore
srpStore SubRepoPermsStore
}
// GrantPendingPermissions grants pending permissions for a user, which implements the database.AuthzStore interface.

View File

@ -17,6 +17,7 @@ type EnterpriseDB interface {
database.DB
CodeMonitors() CodeMonitorStore
Perms() PermsStore
SubRepoPerms() SubRepoPermsStore
}
func NewEnterpriseDB(db database.DB) EnterpriseDB {
@ -42,6 +43,10 @@ func (edb *enterpriseDB) Perms() PermsStore {
return &permsStore{Store: basestore.NewWithHandle(edb.Handle()), clock: time.Now}
}
func (edb *enterpriseDB) SubRepoPerms() SubRepoPermsStore {
return SubRepoPermsWith(basestore.NewWithHandle(edb.Handle()))
}
type InsightsDB interface {
dbutil.DB
basestore.ShareableStore

File diff suppressed because it is too large Load Diff

View File

@ -18,8 +18,10 @@ import (
// and exclude patterns.
const SubRepoPermsVersion = 1
var SubRepoSupportedCodeHostTypes = []string{extsvc.TypePerforce}
var supportedTypesQuery = make([]*sqlf.Query, len(SubRepoSupportedCodeHostTypes))
var (
SubRepoSupportedCodeHostTypes = []string{extsvc.TypePerforce}
supportedTypesQuery = make([]*sqlf.Query, len(SubRepoSupportedCodeHostTypes))
)
func init() {
// Build this up at startup, so we don't need to rebuild it every time
@ -53,6 +55,8 @@ type subRepoPermsStore struct {
*basestore.Store
}
var _ SubRepoPermsStore = (*subRepoPermsStore)(nil)
func SubRepoPermsWith(other basestore.ShareableStore) SubRepoPermsStore {
return &subRepoPermsStore{Store: basestore.NewWithHandle(other.Handle())}
}

View File

@ -13,6 +13,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
"github.com/sourcegraph/sourcegraph/internal/database/dbutil"
"github.com/sourcegraph/sourcegraph/schema"
@ -25,7 +26,7 @@ func TestSubRepoPermsInsert(t *testing.T) {
t.Parallel()
logger := logtest.Scoped(t)
db := NewDB(logger, dbtest.NewDB(logger, t))
db := NewEnterpriseDB(database.NewDB(logger, dbtest.NewDB(logger, t)))
ctx := context.Background()
prepareSubRepoTestData(ctx, t, db)
@ -57,7 +58,7 @@ func TestSubRepoPermsDeleteByUser(t *testing.T) {
t.Parallel()
logger := logtest.Scoped(t)
db := NewDB(logger, dbtest.NewDB(logger, t))
db := NewEnterpriseDB(database.NewDB(logger, dbtest.NewDB(logger, t)))
ctx := context.Background()
prepareSubRepoTestData(ctx, t, db)
@ -92,7 +93,7 @@ func TestSubRepoPermsUpsert(t *testing.T) {
t.Parallel()
logger := logtest.Scoped(t)
db := NewDB(logger, dbtest.NewDB(logger, t))
db := NewEnterpriseDB(database.NewDB(logger, dbtest.NewDB(logger, t)))
ctx := context.Background()
prepareSubRepoTestData(ctx, t, db)
@ -133,7 +134,7 @@ func TestSubRepoPermsUpsertWithSpec(t *testing.T) {
t.Parallel()
logger := logtest.Scoped(t)
db := NewDB(logger, dbtest.NewDB(logger, t))
db := NewEnterpriseDB(database.NewDB(logger, dbtest.NewDB(logger, t)))
ctx := context.Background()
prepareSubRepoTestData(ctx, t, db)
@ -181,7 +182,7 @@ func TestSubRepoPermsGetByUser(t *testing.T) {
t.Cleanup(func() { conf.Mock(nil) })
logger := logtest.Scoped(t)
db := NewDB(logger, dbtest.NewDB(logger, t))
db := NewEnterpriseDB(database.NewDB(logger, dbtest.NewDB(logger, t)))
ctx := context.Background()
s := db.SubRepoPerms()
@ -264,7 +265,7 @@ func TestSubRepoPermsGetByUserAndService(t *testing.T) {
logger := logtest.Scoped(t)
db := NewDB(logger, dbtest.NewDB(logger, t))
db := NewEnterpriseDB(database.NewDB(logger, dbtest.NewDB(logger, t)))
ctx := context.Background()
s := db.SubRepoPerms()
@ -342,7 +343,7 @@ func TestSubRepoPermsSupportedForRepoId(t *testing.T) {
t.Parallel()
logger := logtest.Scoped(t)
db := NewDB(logger, dbtest.NewDB(logger, t))
db := NewEnterpriseDB(database.NewDB(logger, dbtest.NewDB(logger, t)))
ctx := context.Background()
s := db.SubRepoPerms()

View File

@ -0,0 +1,158 @@
package inttests
import (
"context"
"fmt"
"net/http"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
inttests "github.com/sourcegraph/sourcegraph/internal/gitserver/integration_tests"
)
func TestGetCommits(t *testing.T) {
t.Parallel()
inttests.InitGitserver()
ctx := actor.WithActor(context.Background(), &actor.Actor{
UID: 1,
})
db := database.NewMockDB()
gr := database.NewMockGitserverRepoStore()
db.GitserverReposFunc.SetDefaultReturn(gr)
repo1 := inttests.MakeGitRepository(t, getGitCommandsWithFiles("file1", "file2")...)
repo2 := inttests.MakeGitRepository(t, getGitCommandsWithFiles("file3", "file4")...)
repo3 := inttests.MakeGitRepository(t, getGitCommandsWithFiles("file5", "file6")...)
repoCommits := []api.RepoCommit{
{Repo: repo1, CommitID: api.CommitID("HEAD")}, // HEAD (file2)
{Repo: repo1, CommitID: api.CommitID("HEAD~1")}, // HEAD~1 (file1)
{Repo: repo2, CommitID: api.CommitID("67762ad757dd26cac4145f2b744fd93ad10a48e0")}, // HEAD (file4)
{Repo: repo2, CommitID: api.CommitID("2b988222e844b570959a493f5b07ec020b89e122")}, // HEAD~1 (file3)
{Repo: repo3, CommitID: api.CommitID("01bed0a")}, // abbrev HEAD (file6)
{Repo: repo3, CommitID: api.CommitID("unresolvable")}, // unresolvable
{Repo: api.RepoName("unresolvable"), CommitID: api.CommitID("deadbeef")}, // unresolvable
}
t.Run("with sub-repo permissions", func(t *testing.T) {
expectedCommits := []*gitdomain.Commit{
{
ID: "2ba4dd2b9a27ec125fea7d72e12b9824ead18631",
Author: gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Committer: &gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Message: "commit2",
Parents: []api.CommitID{"d38233a79e037d2ab8170b0d0bc0aa438473e6da"},
},
nil, // file 1
{
ID: "67762ad757dd26cac4145f2b744fd93ad10a48e0",
Author: gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Committer: &gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Message: "commit2",
Parents: []api.CommitID{"2b988222e844b570959a493f5b07ec020b89e122"},
},
nil, // file 3
{
ID: "01bed0ae660668c57539cecaacb4c33d77609f43",
Author: gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Committer: &gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Message: "commit2",
Parents: []api.CommitID{"d6ce2e76d171569d81c0afdc4573f461cec17d45"},
},
nil,
nil,
}
commits, err := gitserver.NewTestClient(http.DefaultClient, db, inttests.GitserverAddresses).GetCommits(ctx, getTestSubRepoPermsChecker("file1", "file3"), repoCommits, true)
if err != nil {
t.Fatalf("unexpected error calling getCommits: %s", err)
}
if diff := cmp.Diff(expectedCommits, commits); diff != "" {
t.Errorf("unexpected commits (-want +got):\n%s", diff)
}
})
}
// get a test sub-repo permissions checker which allows access to all files (so should be a no-op)
func getTestSubRepoPermsChecker(noAccessPaths ...string) authz.SubRepoPermissionChecker {
checker := authz.NewMockSubRepoPermissionChecker()
checker.EnabledFunc.SetDefaultHook(func() bool {
return true
})
checker.PermissionsFunc.SetDefaultHook(func(ctx context.Context, i int32, content authz.RepoContent) (authz.Perms, error) {
for _, noAccessPath := range noAccessPaths {
if content.Path == noAccessPath {
return authz.None, nil
}
}
return authz.Read, nil
})
return checker
}
func getGitCommandsWithFiles(fileName1, fileName2 string) []string {
return []string{
fmt.Sprintf("touch %s", fileName1),
fmt.Sprintf("git add %s", fileName1),
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m commit1 --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
fmt.Sprintf("touch %s", fileName2),
fmt.Sprintf("git add %s", fileName2),
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m commit2 --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
}
}
func mustParseDate(s string, t *testing.T) *time.Time {
t.Helper()
date, err := time.Parse(time.RFC3339, s)
if err != nil {
t.Fatalf("unexpected error parsing date string: %s", err)
}
return &date
}
func TestHead(t *testing.T) {
inttests.InitGitserver()
client := gitserver.NewTestClient(http.DefaultClient, database.NewMockDB(), inttests.GitserverAddresses)
t.Run("with sub-repo permissions", func(t *testing.T) {
gitCommands := []string{
"touch file",
"git add file",
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m foo --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
}
repo := inttests.MakeGitRepository(t, gitCommands...)
ctx := actor.WithActor(context.Background(), &actor.Actor{
UID: 1,
})
checker := getTestSubRepoPermsChecker("file")
// call Head() when user doesn't have access to view the commit
_, exists, err := client.Head(ctx, checker, repo)
if err != nil {
t.Fatal(err)
}
if exists {
t.Fatalf("exists should be false since the user doesn't have access to view the commit")
}
readAllChecker := getTestSubRepoPermsChecker()
// call Head() when user has access to view the commit; should return expected commit
head, exists, err := client.Head(ctx, readAllChecker, repo)
if err != nil {
t.Fatal(err)
}
wantHead := "46619ad353dbe4ed4108ebde9aa59ef676994a0b"
if head != wantHead {
t.Fatalf("Want %q, got %q", wantHead, head)
}
if !exists {
t.Fatal("Should exist")
}
})
}

View File

@ -0,0 +1,74 @@
package inttests
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
srp "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms"
edb "github.com/sourcegraph/sourcegraph/enterprise/internal/database"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
inttests "github.com/sourcegraph/sourcegraph/internal/gitserver/integration_tests"
"github.com/sourcegraph/sourcegraph/schema"
)
func TestReadDir_SubRepoFiltering(t *testing.T) {
inttests.InitGitserver()
ctx := actor.WithActor(context.Background(), &actor.Actor{
UID: 1,
})
gitCommands := []string{
"touch file1",
"git add file1",
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m commit1 --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
"mkdir app",
"touch app/file2",
"git add app",
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m commit2 --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
}
repo := inttests.MakeGitRepository(t, gitCommands...)
commitID := api.CommitID("b1c725720de2bbd0518731b4a61959797ff345f3")
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
defer conf.Mock(nil)
srpGetter := edb.NewMockSubRepoPermsStore()
testSubRepoPerms := map[api.RepoName]authz.SubRepoPermissions{
repo: {
Paths: []string{"/**", "-/app/**"},
},
}
srpGetter.GetByUserFunc.SetDefaultReturn(testSubRepoPerms, nil)
checker, err := srp.NewSubRepoPermsClient(srpGetter)
if err != nil {
t.Fatalf("unexpected error creating sub-repo perms client: %s", err)
}
db := database.NewMockDB()
gr := database.NewMockGitserverRepoStore()
db.GitserverReposFunc.SetDefaultReturn(gr)
client := gitserver.NewTestClient(http.DefaultClient, db, inttests.GitserverAddresses)
files, err := client.ReadDir(ctx, checker, repo, commitID, "", false)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
// Because we have a wildcard matcher we still allow directory visibility
assert.Len(t, files, 1)
assert.Equal(t, "file1", files[0].Name())
assert.False(t, files[0].IsDir())
}

View File

@ -0,0 +1,60 @@
package symbol
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
srp "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/search/result"
"github.com/sourcegraph/sourcegraph/internal/search/symbol"
"github.com/sourcegraph/sourcegraph/schema"
)
func TestFilterZoektResults(t *testing.T) {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
t.Cleanup(func() { conf.Mock(nil) })
repoName := api.RepoName("foo")
ctx := context.Background()
ctx = actor.WithActor(ctx, &actor.Actor{
UID: 1,
})
checker, err := srp.NewSimpleChecker(repoName, []string{"/**", "-/*_test.go"})
if err != nil {
t.Fatal(err)
}
results := []*result.SymbolMatch{
{
Symbol: result.Symbol{},
File: &result.File{
Path: "foo.go",
},
},
{
Symbol: result.Symbol{},
File: &result.File{
Path: "foo_test.go",
},
},
}
filtered, err := symbol.FilterZoektResults(ctx, checker, repoName, results)
if err != nil {
t.Fatal(err)
}
assert.Len(t, filtered, 1)
r := filtered[0]
assert.Equal(t, r.File.Path, "foo.go")
}

View File

@ -677,416 +677,3 @@ func (c SubRepoPermissionCheckerPermissionsFuncCall) Args() []interface{} {
func (c SubRepoPermissionCheckerPermissionsFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// MockSubRepoPermissionsGetter is a mock implementation of the
// SubRepoPermissionsGetter interface (from the package
// github.com/sourcegraph/sourcegraph/internal/authz) used for unit testing.
type MockSubRepoPermissionsGetter struct {
// GetByUserFunc is an instance of a mock function object controlling
// the behavior of the method GetByUser.
GetByUserFunc *SubRepoPermissionsGetterGetByUserFunc
// RepoIDSupportedFunc is an instance of a mock function object
// controlling the behavior of the method RepoIDSupported.
RepoIDSupportedFunc *SubRepoPermissionsGetterRepoIDSupportedFunc
// RepoSupportedFunc is an instance of a mock function object
// controlling the behavior of the method RepoSupported.
RepoSupportedFunc *SubRepoPermissionsGetterRepoSupportedFunc
}
// NewMockSubRepoPermissionsGetter creates a new mock of the
// SubRepoPermissionsGetter interface. All methods return zero values for
// all results, unless overwritten.
func NewMockSubRepoPermissionsGetter() *MockSubRepoPermissionsGetter {
return &MockSubRepoPermissionsGetter{
GetByUserFunc: &SubRepoPermissionsGetterGetByUserFunc{
defaultHook: func(context.Context, int32) (r0 map[api.RepoName]SubRepoPermissions, r1 error) {
return
},
},
RepoIDSupportedFunc: &SubRepoPermissionsGetterRepoIDSupportedFunc{
defaultHook: func(context.Context, api.RepoID) (r0 bool, r1 error) {
return
},
},
RepoSupportedFunc: &SubRepoPermissionsGetterRepoSupportedFunc{
defaultHook: func(context.Context, api.RepoName) (r0 bool, r1 error) {
return
},
},
}
}
// NewStrictMockSubRepoPermissionsGetter creates a new mock of the
// SubRepoPermissionsGetter interface. All methods panic on invocation,
// unless overwritten.
func NewStrictMockSubRepoPermissionsGetter() *MockSubRepoPermissionsGetter {
return &MockSubRepoPermissionsGetter{
GetByUserFunc: &SubRepoPermissionsGetterGetByUserFunc{
defaultHook: func(context.Context, int32) (map[api.RepoName]SubRepoPermissions, error) {
panic("unexpected invocation of MockSubRepoPermissionsGetter.GetByUser")
},
},
RepoIDSupportedFunc: &SubRepoPermissionsGetterRepoIDSupportedFunc{
defaultHook: func(context.Context, api.RepoID) (bool, error) {
panic("unexpected invocation of MockSubRepoPermissionsGetter.RepoIDSupported")
},
},
RepoSupportedFunc: &SubRepoPermissionsGetterRepoSupportedFunc{
defaultHook: func(context.Context, api.RepoName) (bool, error) {
panic("unexpected invocation of MockSubRepoPermissionsGetter.RepoSupported")
},
},
}
}
// NewMockSubRepoPermissionsGetterFrom creates a new mock of the
// MockSubRepoPermissionsGetter interface. All methods delegate to the given
// implementation, unless overwritten.
func NewMockSubRepoPermissionsGetterFrom(i SubRepoPermissionsGetter) *MockSubRepoPermissionsGetter {
return &MockSubRepoPermissionsGetter{
GetByUserFunc: &SubRepoPermissionsGetterGetByUserFunc{
defaultHook: i.GetByUser,
},
RepoIDSupportedFunc: &SubRepoPermissionsGetterRepoIDSupportedFunc{
defaultHook: i.RepoIDSupported,
},
RepoSupportedFunc: &SubRepoPermissionsGetterRepoSupportedFunc{
defaultHook: i.RepoSupported,
},
}
}
// SubRepoPermissionsGetterGetByUserFunc describes the behavior when the
// GetByUser method of the parent MockSubRepoPermissionsGetter instance is
// invoked.
type SubRepoPermissionsGetterGetByUserFunc struct {
defaultHook func(context.Context, int32) (map[api.RepoName]SubRepoPermissions, error)
hooks []func(context.Context, int32) (map[api.RepoName]SubRepoPermissions, error)
history []SubRepoPermissionsGetterGetByUserFuncCall
mutex sync.Mutex
}
// GetByUser delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockSubRepoPermissionsGetter) GetByUser(v0 context.Context, v1 int32) (map[api.RepoName]SubRepoPermissions, error) {
r0, r1 := m.GetByUserFunc.nextHook()(v0, v1)
m.GetByUserFunc.appendCall(SubRepoPermissionsGetterGetByUserFuncCall{v0, v1, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the GetByUser method of
// the parent MockSubRepoPermissionsGetter instance is invoked and the hook
// queue is empty.
func (f *SubRepoPermissionsGetterGetByUserFunc) SetDefaultHook(hook func(context.Context, int32) (map[api.RepoName]SubRepoPermissions, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// GetByUser method of the parent MockSubRepoPermissionsGetter 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 *SubRepoPermissionsGetterGetByUserFunc) PushHook(hook func(context.Context, int32) (map[api.RepoName]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 *SubRepoPermissionsGetterGetByUserFunc) SetDefaultReturn(r0 map[api.RepoName]SubRepoPermissions, r1 error) {
f.SetDefaultHook(func(context.Context, int32) (map[api.RepoName]SubRepoPermissions, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *SubRepoPermissionsGetterGetByUserFunc) PushReturn(r0 map[api.RepoName]SubRepoPermissions, r1 error) {
f.PushHook(func(context.Context, int32) (map[api.RepoName]SubRepoPermissions, error) {
return r0, r1
})
}
func (f *SubRepoPermissionsGetterGetByUserFunc) nextHook() func(context.Context, int32) (map[api.RepoName]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 *SubRepoPermissionsGetterGetByUserFunc) appendCall(r0 SubRepoPermissionsGetterGetByUserFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of SubRepoPermissionsGetterGetByUserFuncCall
// objects describing the invocations of this function.
func (f *SubRepoPermissionsGetterGetByUserFunc) History() []SubRepoPermissionsGetterGetByUserFuncCall {
f.mutex.Lock()
history := make([]SubRepoPermissionsGetterGetByUserFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// SubRepoPermissionsGetterGetByUserFuncCall is an object that describes an
// invocation of method GetByUser on an instance of
// MockSubRepoPermissionsGetter.
type SubRepoPermissionsGetterGetByUserFuncCall 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
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 map[api.RepoName]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 SubRepoPermissionsGetterGetByUserFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c SubRepoPermissionsGetterGetByUserFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// SubRepoPermissionsGetterRepoIDSupportedFunc describes the behavior when
// the RepoIDSupported method of the parent MockSubRepoPermissionsGetter
// instance is invoked.
type SubRepoPermissionsGetterRepoIDSupportedFunc struct {
defaultHook func(context.Context, api.RepoID) (bool, error)
hooks []func(context.Context, api.RepoID) (bool, error)
history []SubRepoPermissionsGetterRepoIDSupportedFuncCall
mutex sync.Mutex
}
// RepoIDSupported delegates to the next hook function in the queue and
// stores the parameter and result values of this invocation.
func (m *MockSubRepoPermissionsGetter) RepoIDSupported(v0 context.Context, v1 api.RepoID) (bool, error) {
r0, r1 := m.RepoIDSupportedFunc.nextHook()(v0, v1)
m.RepoIDSupportedFunc.appendCall(SubRepoPermissionsGetterRepoIDSupportedFuncCall{v0, v1, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the RepoIDSupported
// method of the parent MockSubRepoPermissionsGetter instance is invoked and
// the hook queue is empty.
func (f *SubRepoPermissionsGetterRepoIDSupportedFunc) SetDefaultHook(hook func(context.Context, api.RepoID) (bool, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// RepoIDSupported method of the parent MockSubRepoPermissionsGetter
// 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 *SubRepoPermissionsGetterRepoIDSupportedFunc) PushHook(hook func(context.Context, api.RepoID) (bool, 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 *SubRepoPermissionsGetterRepoIDSupportedFunc) SetDefaultReturn(r0 bool, r1 error) {
f.SetDefaultHook(func(context.Context, api.RepoID) (bool, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *SubRepoPermissionsGetterRepoIDSupportedFunc) PushReturn(r0 bool, r1 error) {
f.PushHook(func(context.Context, api.RepoID) (bool, error) {
return r0, r1
})
}
func (f *SubRepoPermissionsGetterRepoIDSupportedFunc) nextHook() func(context.Context, api.RepoID) (bool, 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 *SubRepoPermissionsGetterRepoIDSupportedFunc) appendCall(r0 SubRepoPermissionsGetterRepoIDSupportedFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of
// SubRepoPermissionsGetterRepoIDSupportedFuncCall objects describing the
// invocations of this function.
func (f *SubRepoPermissionsGetterRepoIDSupportedFunc) History() []SubRepoPermissionsGetterRepoIDSupportedFuncCall {
f.mutex.Lock()
history := make([]SubRepoPermissionsGetterRepoIDSupportedFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// SubRepoPermissionsGetterRepoIDSupportedFuncCall is an object that
// describes an invocation of method RepoIDSupported on an instance of
// MockSubRepoPermissionsGetter.
type SubRepoPermissionsGetterRepoIDSupportedFuncCall 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 api.RepoID
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 bool
// 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 SubRepoPermissionsGetterRepoIDSupportedFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c SubRepoPermissionsGetterRepoIDSupportedFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// SubRepoPermissionsGetterRepoSupportedFunc describes the behavior when the
// RepoSupported method of the parent MockSubRepoPermissionsGetter instance
// is invoked.
type SubRepoPermissionsGetterRepoSupportedFunc struct {
defaultHook func(context.Context, api.RepoName) (bool, error)
hooks []func(context.Context, api.RepoName) (bool, error)
history []SubRepoPermissionsGetterRepoSupportedFuncCall
mutex sync.Mutex
}
// RepoSupported delegates to the next hook function in the queue and stores
// the parameter and result values of this invocation.
func (m *MockSubRepoPermissionsGetter) RepoSupported(v0 context.Context, v1 api.RepoName) (bool, error) {
r0, r1 := m.RepoSupportedFunc.nextHook()(v0, v1)
m.RepoSupportedFunc.appendCall(SubRepoPermissionsGetterRepoSupportedFuncCall{v0, v1, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the RepoSupported method
// of the parent MockSubRepoPermissionsGetter instance is invoked and the
// hook queue is empty.
func (f *SubRepoPermissionsGetterRepoSupportedFunc) SetDefaultHook(hook func(context.Context, api.RepoName) (bool, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// RepoSupported method of the parent MockSubRepoPermissionsGetter 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 *SubRepoPermissionsGetterRepoSupportedFunc) PushHook(hook func(context.Context, api.RepoName) (bool, 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 *SubRepoPermissionsGetterRepoSupportedFunc) SetDefaultReturn(r0 bool, r1 error) {
f.SetDefaultHook(func(context.Context, api.RepoName) (bool, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *SubRepoPermissionsGetterRepoSupportedFunc) PushReturn(r0 bool, r1 error) {
f.PushHook(func(context.Context, api.RepoName) (bool, error) {
return r0, r1
})
}
func (f *SubRepoPermissionsGetterRepoSupportedFunc) nextHook() func(context.Context, api.RepoName) (bool, 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 *SubRepoPermissionsGetterRepoSupportedFunc) appendCall(r0 SubRepoPermissionsGetterRepoSupportedFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of
// SubRepoPermissionsGetterRepoSupportedFuncCall objects describing the
// invocations of this function.
func (f *SubRepoPermissionsGetterRepoSupportedFunc) History() []SubRepoPermissionsGetterRepoSupportedFuncCall {
f.mutex.Lock()
history := make([]SubRepoPermissionsGetterRepoSupportedFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// SubRepoPermissionsGetterRepoSupportedFuncCall is an object that describes
// an invocation of method RepoSupported on an instance of
// MockSubRepoPermissionsGetter.
type SubRepoPermissionsGetterRepoSupportedFuncCall 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 api.RepoName
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 bool
// 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 SubRepoPermissionsGetterRepoSupportedFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c SubRepoPermissionsGetterRepoSupportedFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}

View File

@ -7,16 +7,11 @@ import (
"strings"
"time"
"github.com/gobwas/glob"
lru "github.com/hashicorp/golang-lru"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"go.uber.org/atomic"
"golang.org/x/sync/singleflight"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
@ -90,336 +85,12 @@ func (*noopPermsChecker) EnabledForRepo(ctx context.Context, repo api.RepoName)
return false, nil
}
var _ SubRepoPermissionChecker = &SubRepoPermsClient{}
// SubRepoPermissionsGetter allows getting sub repository permissions.
type SubRepoPermissionsGetter interface {
// GetByUser returns the sub repository permissions rules known for a user.
GetByUser(ctx context.Context, userID int32) (map[api.RepoName]SubRepoPermissions, error)
// RepoIDSupported returns true if repo with the given ID has sub-repo permissions.
RepoIDSupported(ctx context.Context, repoID api.RepoID) (bool, error)
// RepoSupported returns true if repo with the given name has sub-repo permissions.
RepoSupported(ctx context.Context, repo api.RepoName) (bool, error)
}
// SubRepoPermsClient is a concrete implementation of SubRepoPermissionChecker.
// Always use NewSubRepoPermsClient to instantiate an instance.
type SubRepoPermsClient struct {
permissionsGetter SubRepoPermissionsGetter
clock func() time.Time
since func(time.Time) time.Duration
group *singleflight.Group
cache *lru.Cache
enabled *atomic.Bool
}
const defaultCacheSize = 1000
const defaultCacheTTL = 10 * time.Second
// cachedRules caches the perms rules known for a particular user by repo.
type cachedRules struct {
rules map[api.RepoName]compiledRules
timestamp time.Time
}
type path struct {
globPath glob.Glob
exclusion bool
// the original rule before it was compiled into a glob matcher
original string
}
type compiledRules struct {
paths []path
}
// GetPermissionsForPath tries to match a given path to a list of rules.
// Since the last applicable rule is the one that applies, the list is
// traversed in reverse, and the function returns as soon as a match is found.
// If no match is found, None is returned.
func (rules compiledRules) GetPermissionsForPath(path string) Perms {
for i := len(rules.paths) - 1; i >= 0; i-- {
if rules.paths[i].globPath.Match(path) {
if rules.paths[i].exclusion {
return None
}
return Read
}
}
// Return None if no rule matches
return None
}
// NewSubRepoPermsClient instantiates an instance of authz.SubRepoPermsClient
// which implements SubRepoPermissionChecker.
//
// SubRepoPermissionChecker is responsible for checking whether a user has access
// to data within a repo. Sub-repository permissions enforcement is on top of
// existing repository permissions, which means the user must already have access
// to the repository itself. The intention is for this client to be created once
// at startup and passed in to all places that need to check sub repo
// permissions.
//
// Note that sub-repo permissions are currently opt-in via the
// experimentalFeatures.enableSubRepoPermissions option.
func NewSubRepoPermsClient(permissionsGetter SubRepoPermissionsGetter) (*SubRepoPermsClient, error) {
cache, err := lru.New(defaultCacheSize)
if err != nil {
return nil, errors.Wrap(err, "creating LRU cache")
}
enabled := atomic.NewBool(false)
conf.Watch(func() {
c := conf.Get()
if c.ExperimentalFeatures == nil || c.ExperimentalFeatures.SubRepoPermissions == nil {
enabled.Store(false)
return
}
cacheSize := c.ExperimentalFeatures.SubRepoPermissions.UserCacheSize
if cacheSize == 0 {
cacheSize = defaultCacheSize
}
cache.Resize(cacheSize)
enabled.Store(c.ExperimentalFeatures.SubRepoPermissions.Enabled)
})
return &SubRepoPermsClient{
permissionsGetter: permissionsGetter,
clock: time.Now,
since: time.Since,
group: &singleflight.Group{},
cache: cache,
enabled: enabled,
}, nil
}
var (
metricSubRepoPermsPermissionsDurationSuccess prometheus.Observer
metricSubRepoPermsPermissionsDurationError prometheus.Observer
)
func init() {
// We cache the result of WithLabelValues since we call them in
// performance sensitive code. See BenchmarkFilterActorPaths.
metric := promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "authz_sub_repo_perms_permissions_duration_seconds",
Help: "Time spent calculating permissions of a file for an actor.",
}, []string{"error"})
metricSubRepoPermsPermissionsDurationSuccess = metric.WithLabelValues("false")
metricSubRepoPermsPermissionsDurationError = metric.WithLabelValues("true")
}
var (
metricSubRepoPermCacheHit prometheus.Counter
metricSubRepoPermCacheMiss prometheus.Counter
)
func init() {
// We cache the result of WithLabelValues since we call them in
// performance sensitive code. See BenchmarkFilterActorPaths.
metric := promauto.NewCounterVec(prometheus.CounterOpts{
Name: "authz_sub_repo_perms_permissions_cache_count",
Help: "The number of sub-repo perms cache hits or misses",
}, []string{"hit"})
metricSubRepoPermCacheHit = metric.WithLabelValues("true")
metricSubRepoPermCacheMiss = metric.WithLabelValues("false")
}
// Permissions return the current permissions granted to the given user on the
// given content. If sub-repo permissions are disabled, it is a no-op that return
// Read.
func (s *SubRepoPermsClient) Permissions(ctx context.Context, userID int32, content RepoContent) (perms Perms, err error) {
// Are sub-repo permissions enabled at the site level
if !s.Enabled() {
return Read, nil
}
began := time.Now()
defer func() {
took := time.Since(began).Seconds()
if err == nil {
metricSubRepoPermsPermissionsDurationSuccess.Observe(took)
} else {
metricSubRepoPermsPermissionsDurationError.Observe(took)
}
}()
f, err := s.FilePermissionsFunc(ctx, userID, content.Repo)
if err != nil {
return None, err
}
return f(content.Path)
}
// filePermissionsFuncAllRead is a FilePermissionFunc which _always_ returns
// Read. Only use in cases that sub repo permission checks should not be done.
func filePermissionsFuncAllRead(_ string) (Perms, error) {
return Read, nil
}
func (s *SubRepoPermsClient) FilePermissionsFunc(ctx context.Context, userID int32, repo api.RepoName) (FilePermissionFunc, error) {
// Are sub-repo permissions enabled at the site level
if !s.Enabled() {
return filePermissionsFuncAllRead, nil
}
if s.permissionsGetter == nil {
return nil, errors.New("permissionsGetter is nil")
}
if userID == 0 {
return nil, &ErrUnauthenticated{}
}
repoRules, err := s.getCompiledRules(ctx, userID)
if err != nil {
return nil, errors.Wrap(err, "compiling match rules")
}
rules, rulesExist := repoRules[repo]
if !rulesExist {
// If we make it this far it implies that we have access at the repo level.
// Having any empty set of rules here implies that we can access the whole repo.
// Repos that support sub-repo permissions will only have an entry in our
// repo_permissions table after all sub-repo permissions have been processed.
return filePermissionsFuncAllRead, nil
}
return func(path string) (Perms, error) {
// An empty path is equivalent to repo permissions so we can assume it has
// already been checked at that level.
if path == "" {
return Read, nil
}
// Prefix path with "/", otherwise suffix rules like "**/file.txt" won't match
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
// Iterate through all rules for the current path, and the final match takes
// preference.
return rules.GetPermissionsForPath(path), nil
}, nil
}
// getCompiledRules fetches rules for the given repo with caching.
func (s *SubRepoPermsClient) getCompiledRules(ctx context.Context, userID int32) (map[api.RepoName]compiledRules, error) {
// Fast path for cached rules
item, _ := s.cache.Get(userID)
cached, ok := item.(cachedRules)
ttl := defaultCacheTTL
if c := conf.Get(); c.ExperimentalFeatures != nil && c.ExperimentalFeatures.SubRepoPermissions != nil && c.ExperimentalFeatures.SubRepoPermissions.UserCacheTTLSeconds > 0 {
ttl = time.Duration(c.ExperimentalFeatures.SubRepoPermissions.UserCacheTTLSeconds) * time.Second
}
if ok && s.since(cached.timestamp) <= ttl {
metricSubRepoPermCacheHit.Inc()
return cached.rules, nil
}
metricSubRepoPermCacheMiss.Inc()
// Slow path on cache miss or expiry. Ensure that only one goroutine is doing the
// work
groupKey := strconv.FormatInt(int64(userID), 10)
result, err, _ := s.group.Do(groupKey, func() (any, error) {
repoPerms, err := s.permissionsGetter.GetByUser(ctx, userID)
if err != nil {
return nil, errors.Wrap(err, "fetching rules")
}
toCache := cachedRules{
rules: make(map[api.RepoName]compiledRules, len(repoPerms)),
}
for repo, perms := range repoPerms {
paths := make([]path, 0, len(perms.Paths))
for _, rule := range perms.Paths {
exclusion := strings.HasPrefix(rule, "-")
rule = strings.TrimPrefix(rule, "-")
if !strings.HasPrefix(rule, "/") {
rule = "/" + rule
}
g, err := glob.Compile(rule, '/')
if err != nil {
return nil, errors.Wrap(err, "building include matcher")
}
paths = append(paths, path{globPath: g, exclusion: exclusion, original: rule})
// Special case. Our glob package does not handle rules starting with a double
// wildcard correctly. For example, we would expect `/**/*.java` to match all
// java files, but it does not match files at the root, eg `/foo.java`. To get
// around this we add an extra rule to cover this case.
if strings.HasPrefix(rule, "/**/") {
trimmed := rule
for {
trimmed = strings.TrimPrefix(trimmed, "/**")
if strings.HasPrefix(trimmed, "/**/") {
// Keep trimming
continue
}
g, err := glob.Compile(trimmed, '/')
if err != nil {
return nil, errors.Wrap(err, "building include matcher")
}
paths = append(paths, path{globPath: g, exclusion: exclusion, original: trimmed})
break
}
}
// We should include all directories above an include rule so that we can browse
// to the included items.
if exclusion {
// Not required for an exclude rule
continue
}
dirs := expandDirs(rule)
for _, dir := range dirs {
g, err := glob.Compile(dir, '/')
if err != nil {
return nil, errors.Wrap(err, "building include matcher for dir")
}
paths = append(paths, path{globPath: g, exclusion: false, original: dir})
}
}
toCache.rules[repo] = compiledRules{
paths: paths,
}
}
toCache.timestamp = s.clock()
s.cache.Add(userID, toCache)
return toCache.rules, nil
})
if err != nil {
return nil, err
}
compiled := result.(map[api.RepoName]compiledRules)
return compiled, nil
}
func (s *SubRepoPermsClient) Enabled() bool {
return s.enabled.Load()
}
func (s *SubRepoPermsClient) EnabledForRepoID(ctx context.Context, id api.RepoID) (bool, error) {
return s.permissionsGetter.RepoIDSupported(ctx, id)
}
func (s *SubRepoPermsClient) EnabledForRepo(ctx context.Context, repo api.RepoName) (bool, error) {
return s.permissionsGetter.RepoSupported(ctx, repo)
}
// expandDirs will return a new set of rules that will match all directories
// above the supplied rule. As a special case, if the rule starts with a wildcard
// we return a rule to match all directories.
@ -454,23 +125,6 @@ func expandDirs(rule string) []string {
return dirs
}
// NewSimpleChecker is exposed for testing and allows creation of a simple
// checker based on the rules provided. The rules are expected to be in glob
// format.
func NewSimpleChecker(repo api.RepoName, paths []string) (SubRepoPermissionChecker, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]SubRepoPermissions, error) {
return map[api.RepoName]SubRepoPermissions{
repo: {
Paths: paths,
},
}, nil
})
getter.RepoSupportedFunc.SetDefaultReturn(true, nil)
getter.RepoIDSupportedFunc.SetDefaultReturn(true, nil)
return NewSubRepoPermsClient(getter)
}
// ActorPermissions returns the level of access the given actor has for the requested
// content.
//

View File

@ -2,11 +2,8 @@ package authz
import (
"context"
"fmt"
"io/fs"
"sort"
"testing"
"time"
"github.com/gobwas/glob"
"github.com/google/go-cmp/cmp"
@ -14,161 +11,9 @@ import (
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/fileutil"
"github.com/sourcegraph/sourcegraph/schema"
)
func TestSubRepoPermsPermissions(t *testing.T) {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
t.Cleanup(func() { conf.Mock(nil) })
testCases := []struct {
name string
userID int32
content RepoContent
clientFn func() (*SubRepoPermsClient, error)
want Perms
}{
{
name: "Empty path",
userID: 1,
content: RepoContent{
Repo: "sample",
Path: "",
},
clientFn: func() (*SubRepoPermsClient, error) {
return NewSubRepoPermsClient(NewMockSubRepoPermissionsGetter())
},
want: Read,
},
{
name: "No rules",
userID: 1,
content: RepoContent{
Repo: "sample",
Path: "/dev/thing",
},
clientFn: func() (*SubRepoPermsClient, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]SubRepoPermissions, error) {
return map[api.RepoName]SubRepoPermissions{
"sample": {
Paths: []string{},
},
}, nil
})
return NewSubRepoPermsClient(getter)
},
want: None,
},
{
name: "Exclude",
userID: 1,
content: RepoContent{
Repo: "sample",
Path: "/dev/thing",
},
clientFn: func() (*SubRepoPermsClient, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]SubRepoPermissions, error) {
return map[api.RepoName]SubRepoPermissions{
"sample": {
Paths: []string{"-/dev/*"},
},
}, nil
})
return NewSubRepoPermsClient(getter)
},
want: None,
},
{
name: "Include",
userID: 1,
content: RepoContent{
Repo: "sample",
Path: "/dev/thing",
},
clientFn: func() (*SubRepoPermsClient, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]SubRepoPermissions, error) {
return map[api.RepoName]SubRepoPermissions{
"sample": {
Paths: []string{"/*"},
},
}, nil
})
return NewSubRepoPermsClient(getter)
},
want: None,
},
{
name: "Last rule takes precedence (exclude)",
userID: 1,
content: RepoContent{
Repo: "sample",
Path: "/dev/thing",
},
clientFn: func() (*SubRepoPermsClient, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]SubRepoPermissions, error) {
return map[api.RepoName]SubRepoPermissions{
"sample": {
Paths: []string{"/**", "-/dev/*"},
},
}, nil
})
return NewSubRepoPermsClient(getter)
},
want: None,
},
{
name: "Last rule takes precedence (include)",
userID: 1,
content: RepoContent{
Repo: "sample",
Path: "/dev/thing",
},
clientFn: func() (*SubRepoPermsClient, error) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]SubRepoPermissions, error) {
return map[api.RepoName]SubRepoPermissions{
"sample": {
Paths: []string{"-/dev/*", "/**"},
},
}, nil
})
return NewSubRepoPermsClient(getter)
},
want: Read,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client, err := tc.clientFn()
if err != nil {
t.Fatal(err)
}
have, err := client.Permissions(context.Background(), tc.userID, tc.content)
if err != nil {
t.Fatal(err)
}
if have != tc.want {
t.Fatalf("have %v, want %v", have, tc.want)
}
})
}
}
func TestFilterActorPaths(t *testing.T) {
testPaths := []string{"file1", "file2", "file3"}
checker := NewMockSubRepoPermissionChecker()
@ -202,98 +47,6 @@ func TestFilterActorPaths(t *testing.T) {
}
}
func BenchmarkFilterActorPaths(b *testing.B) {
// This benchmark is simulating the code path taken by a monorepo with sub
// repo permissions. Our goal is to support repos with millions of files.
// For now we target a lower number since large numbers don't give enough
// runs of the benchmark to be useful.
const pathCount = 5_000
pathPatterns := []string{
"base/%d/foo.go",
"%d/stuff/baz",
"frontend/%d/stuff/baz/bam",
"subdir/sub/sub/sub/%d",
"%d/foo/README.md",
"subdir/remove/me/please/%d",
"subdir/%d/also-remove/me/please",
"a/deep/path/%d/.secrets.env",
"%d/does/not/match/anything",
"does/%d/not/match/anything",
"does/not/%d/match/anything",
"does/not/match/%d/anything",
"does/not/match/anything/%d",
}
paths := []string{
"config.yaml",
"dir.yaml",
}
for i := 0; len(paths) < pathCount; i++ {
for _, pat := range pathPatterns {
paths = append(paths, fmt.Sprintf(pat, i))
}
}
paths = paths[:pathCount]
sort.Strings(paths)
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
defer conf.Mock(nil)
repo := api.RepoName("repo")
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]SubRepoPermissions, error) {
return map[api.RepoName]SubRepoPermissions{
repo: {
Paths: []string{
"/base/**",
"/*/stuff/**",
"/frontend/**/stuff/*",
"/config.yaml",
"/subdir/**",
"/**/README.md",
"/dir.yaml",
"-/subdir/remove/",
"-/subdir/*/also-remove/**",
"-/**/.secrets.env",
},
},
}, nil
})
checker, err := NewSubRepoPermsClient(getter)
if err != nil {
b.Fatal(err)
}
a := &actor.Actor{
UID: 1,
}
ctx := actor.WithActor(context.Background(), a)
b.ResetTimer()
start := time.Now()
for n := 0; n <= b.N; n++ {
filtered, err := FilterActorPaths(ctx, checker, a, repo, paths)
if err != nil {
b.Fatal(err)
}
if len(filtered) == 0 {
b.Fatal("expected paths to be returned")
}
if len(filtered) == len(paths) {
b.Fatal("expected to filter out some paths")
}
}
b.ReportMetric(float64(len(paths))*float64(b.N)/time.Since(start).Seconds(), "paths/s")
}
func TestCanReadAllPaths(t *testing.T) {
testPaths := []string{"file1", "file2", "file3"}
checker := NewMockSubRepoPermissionChecker()
@ -352,179 +105,6 @@ func TestCanReadAllPaths(t *testing.T) {
}
}
func TestSubRepoPermissionsCanReadDirectoriesInPath(t *testing.T) {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
t.Cleanup(func() { conf.Mock(nil) })
repoName := api.RepoName("repo")
testCases := []struct {
paths []string
canReadAll []string
cannotReadAny []string
}{
{
paths: []string{"foo/bar/thing.txt"},
canReadAll: []string{"foo/", "foo/bar/"},
cannotReadAny: []string{"foo/thing.txt", "foo/bar/other.txt"},
},
{
paths: []string{"foo/bar/**"},
canReadAll: []string{"foo/", "foo/bar/", "foo/bar/baz/", "foo/bar/baz/fox/"},
},
{
paths: []string{"foo/bar/"},
canReadAll: []string{"foo/", "foo/bar/"},
cannotReadAny: []string{"foo/thing.txt", "foo/bar/thing.txt"},
},
{
paths: []string{"baz/*/foo/bar/thing.txt"},
canReadAll: []string{"baz/", "baz/x/", "baz/x/foo/bar/"},
cannotReadAny: []string{"baz/thing.txt"},
},
// If we have a wildcard in a path we allow all directories that are not
// explicitly excluded.
{
paths: []string{"**/foo/bar/thing.txt"},
canReadAll: []string{"foo/", "foo/bar/"},
},
{
paths: []string{"*/foo/bar/thing.txt"},
canReadAll: []string{"foo/", "foo/bar/"},
},
{
paths: []string{"/**/foo/bar/thing.txt"},
canReadAll: []string{"foo/", "foo/bar/"},
},
{
paths: []string{"/*/foo/bar/thing.txt"},
canReadAll: []string{"foo/", "foo/bar/"},
},
{
paths: []string{"-/**", "/storage/redis/**"},
canReadAll: []string{"storage/", "/storage/", "/storage/redis/"},
},
{
paths: []string{"-/**", "-/storage/**", "/storage/redis/**"},
canReadAll: []string{"storage/", "/storage/", "/storage/redis/"},
},
// Even with a wildcard include rule, we should still exclude directories that
// are explicitly excluded later
{
paths: []string{"/**", "-/storage/**"},
canReadAll: []string{"/foo"},
cannotReadAny: []string{"storage/", "/storage/", "/storage/redis/"},
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
getter := NewMockSubRepoPermissionsGetter()
getter.GetByUserFunc.SetDefaultHook(func(ctx context.Context, i int32) (map[api.RepoName]SubRepoPermissions, error) {
return map[api.RepoName]SubRepoPermissions{
repoName: {
Paths: tc.paths,
},
}, nil
})
client, err := NewSubRepoPermsClient(getter)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
for _, path := range tc.canReadAll {
content := RepoContent{
Repo: repoName,
Path: path,
}
perm, err := client.Permissions(ctx, 1, content)
if err != nil {
t.Error(err)
}
if !perm.Include(Read) {
t.Errorf("Should be able to read %q, cannot", path)
}
}
for _, path := range tc.cannotReadAny {
content := RepoContent{
Repo: repoName,
Path: path,
}
perm, err := client.Permissions(ctx, 1, content)
if err != nil {
t.Error(err)
}
if perm.Include(Read) {
t.Errorf("Should not be able to read %q, can", path)
}
}
})
}
}
func TestSubRepoPermsPermissionsCache(t *testing.T) {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
t.Cleanup(func() { conf.Mock(nil) })
getter := NewMockSubRepoPermissionsGetter()
client, err := NewSubRepoPermsClient(getter)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
content := RepoContent{
Repo: api.RepoName("thing"),
Path: "/stuff",
}
// Should hit DB only once
for i := 0; i < 3; i++ {
_, err = client.Permissions(ctx, 1, content)
if err != nil {
t.Fatal(err)
}
h := getter.GetByUserFunc.History()
if len(h) != 1 {
t.Fatal("Should have been called once")
}
}
// Trigger expiry
client.since = func(time time.Time) time.Duration {
return defaultCacheTTL + 1
}
_, err = client.Permissions(ctx, 1, content)
if err != nil {
t.Fatal(err)
}
h := getter.GetByUserFunc.History()
if len(h) != 2 {
t.Fatal("Should have been called twice")
}
}
func TestSubRepoEnabled(t *testing.T) {
t.Run("checker is nil", func(t *testing.T) {
if SubRepoEnabled(nil) {

View File

@ -45,7 +45,6 @@ type DB interface {
SavedSearches() SavedSearchStore
SearchContexts() SearchContextsStore
Settings() SettingsStore
SubRepoPerms() SubRepoPermsStore
TemporarySettings() TemporarySettingsStore
UserCredentials(encryption.Key) UserCredentialsStore
UserEmails() UserEmailsStore
@ -205,10 +204,6 @@ func (d *db) Settings() SettingsStore {
return SettingsWith(d.Store)
}
func (d *db) SubRepoPerms() SubRepoPermsStore {
return SubRepoPermsWith(d.Store)
}
func (d *db) TemporarySettings() TemporarySettingsStore {
return TemporarySettingsWith(d.Store)
}

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,6 @@ import (
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
@ -79,7 +78,7 @@ func TestGetCommits(t *testing.T) {
nil,
}
commits, err := gitserver.NewTestClient(http.DefaultClient, db, gitserverAddresses).GetCommits(ctx, nil, repoCommits, true)
commits, err := gitserver.NewTestClient(http.DefaultClient, db, GitserverAddresses).GetCommits(ctx, nil, repoCommits, true)
if err != nil {
t.Fatalf("unexpected error calling getCommits: %s", err)
}
@ -87,61 +86,6 @@ func TestGetCommits(t *testing.T) {
t.Errorf("unexpected commits (-want +got):\n%s", diff)
}
})
t.Run("with sub-repo permissions", func(t *testing.T) {
expectedCommits := []*gitdomain.Commit{
{
ID: "2ba4dd2b9a27ec125fea7d72e12b9824ead18631",
Author: gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Committer: &gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Message: "commit2",
Parents: []api.CommitID{"d38233a79e037d2ab8170b0d0bc0aa438473e6da"},
},
nil, // file 1
{
ID: "67762ad757dd26cac4145f2b744fd93ad10a48e0",
Author: gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Committer: &gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Message: "commit2",
Parents: []api.CommitID{"2b988222e844b570959a493f5b07ec020b89e122"},
},
nil, // file 3
{
ID: "01bed0ae660668c57539cecaacb4c33d77609f43",
Author: gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Committer: &gitdomain.Signature{Name: "a", Email: "a@a.com", Date: *mustParseDate("2006-01-02T15:04:05Z", t)},
Message: "commit2",
Parents: []api.CommitID{"d6ce2e76d171569d81c0afdc4573f461cec17d45"},
},
nil,
nil,
}
commits, err := gitserver.NewTestClient(http.DefaultClient, db, gitserverAddresses).GetCommits(ctx, getTestSubRepoPermsChecker("file1", "file3"), repoCommits, true)
if err != nil {
t.Fatalf("unexpected error calling getCommits: %s", err)
}
if diff := cmp.Diff(expectedCommits, commits); diff != "" {
t.Errorf("unexpected commits (-want +got):\n%s", diff)
}
})
}
// get a test sub-repo permissions checker which allows access to all files (so should be a no-op)
func getTestSubRepoPermsChecker(noAccessPaths ...string) authz.SubRepoPermissionChecker {
checker := authz.NewMockSubRepoPermissionChecker()
checker.EnabledFunc.SetDefaultHook(func() bool {
return true
})
checker.PermissionsFunc.SetDefaultHook(func(ctx context.Context, i int32, content authz.RepoContent) (authz.Perms, error) {
for _, noAccessPath := range noAccessPaths {
if content.Path == noAccessPath {
return authz.None, nil
}
}
return authz.Read, nil
})
return checker
}
func getGitCommandsWithFiles(fileName1, fileName2 string) []string {
@ -165,7 +109,7 @@ func mustParseDate(s string, t *testing.T) *time.Time {
}
func TestHead(t *testing.T) {
client := gitserver.NewTestClient(http.DefaultClient, database.NewMockDB(), gitserverAddresses)
client := gitserver.NewTestClient(http.DefaultClient, database.NewMockDB(), GitserverAddresses)
t.Run("basic", func(t *testing.T) {
gitCommands := []string{
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit --allow-empty -m foo --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
@ -185,38 +129,4 @@ func TestHead(t *testing.T) {
t.Fatal("Should exist")
}
})
t.Run("with sub-repo permissions", func(t *testing.T) {
gitCommands := []string{
"touch file",
"git add file",
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m foo --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
}
repo := MakeGitRepository(t, gitCommands...)
ctx := actor.WithActor(context.Background(), &actor.Actor{
UID: 1,
})
checker := getTestSubRepoPermsChecker("file")
// call Head() when user doesn't have access to view the commit
_, exists, err := client.Head(ctx, checker, repo)
if err != nil {
t.Fatal(err)
}
if exists {
t.Fatalf("exists should be false since the user doesn't have access to view the commit")
}
readAllChecker := getTestSubRepoPermsChecker()
// call Head() when user has access to view the commit; should return expected commit
head, exists, err := client.Head(ctx, readAllChecker, repo)
if err != nil {
t.Fatal(err)
}
wantHead := "46619ad353dbe4ed4108ebde9aa59ef676994a0b"
if head != wantHead {
t.Fatalf("Want %q, got %q", wantHead, head)
}
if !exists {
t.Fatal("Should exist")
}
})
}

View File

@ -1,40 +1,18 @@
package inttests
import (
"context"
"flag"
"log"
"net"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
"time"
"golang.org/x/sync/semaphore"
sglog "github.com/sourcegraph/log"
"github.com/sourcegraph/log/logtest"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/server"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/httpcli"
"github.com/sourcegraph/sourcegraph/internal/observation"
)
var root string
// This is a default gitserver test client currently used for RequestRepoUpdate
// gitserver calls during invocation of MakeGitRepository function
var testGitserverClient gitserver.Client
var gitserverAddresses []string
func TestMain(m *testing.M) {
InitGitserver()
flag.Parse()
if !testing.Verbose() {
@ -48,101 +26,11 @@ func TestMain(m *testing.M) {
os.Exit(code)
}
// done in init since the go vet analysis "ctrlflow" is tripped up if this is
// done as part of TestMain.
func init() {
// Ignore users configuration in tests
os.Setenv("GIT_CONFIG_NOSYSTEM", "true")
os.Setenv("HOME", "/dev/null")
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatalf("listen failed: %s", err)
}
root, err = os.MkdirTemp("", "test")
if err != nil {
log.Fatal(err)
}
db := database.NewMockDB()
gr := database.NewMockGitserverRepoStore()
db.GitserverReposFunc.SetDefaultReturn(gr)
srv := &http.Server{
Handler: (&server.Server{
Logger: sglog.Scoped("server", "the gitserver service"),
ObservationCtx: &observation.TestContext,
ReposDir: filepath.Join(root, "repos"),
GetRemoteURLFunc: func(ctx context.Context, name api.RepoName) (string, error) {
return filepath.Join(root, "remotes", string(name)), nil
},
GetVCSSyncer: func(ctx context.Context, name api.RepoName) (server.VCSSyncer, error) {
return &server.GitRepoSyncer{}, nil
},
GlobalBatchLogSemaphore: semaphore.NewWeighted(32),
DB: db,
}).Handler(),
}
go func() {
if err := srv.Serve(l); err != nil {
log.Fatal(err)
}
}()
serverAddress := l.Addr().String()
testGitserverClient = gitserver.NewTestClient(httpcli.InternalDoer, db, []string{serverAddress})
gitserverAddresses = []string{serverAddress}
}
var Times = []string{
AppleTime("2006-01-02T15:04:05Z"),
AppleTime("2014-05-06T19:20:21Z"),
}
// InitGitRepository initializes a new Git repository and runs cmds in a new
// temporary directory (returned as dir).
func InitGitRepository(t testing.TB, cmds ...string) string {
t.Helper()
remotes := filepath.Join(root, "remotes")
if err := os.MkdirAll(remotes, 0o700); err != nil {
t.Fatal(err)
}
dir, err := os.MkdirTemp(remotes, strings.ReplaceAll(t.Name(), "/", "__"))
if err != nil {
t.Fatal(err)
}
cmds = append([]string{"git init"}, cmds...)
for _, cmd := range cmds {
out, err := GitCommand(dir, "bash", "-c", cmd).CombinedOutput()
if err != nil {
t.Fatalf("Command %q failed. Output was:\n\n%s", cmd, out)
}
}
return dir
}
func GitCommand(dir, name string, args ...string) *exec.Cmd {
c := exec.Command(name, args...)
c.Dir = dir
c.Env = append(os.Environ(), "GIT_CONFIG="+path.Join(dir, ".git", "config"))
return c
}
// MakeGitRepository calls initGitRepository to create a new Git repository and returns a handle to
// it.
func MakeGitRepository(t testing.TB, cmds ...string) api.RepoName {
t.Helper()
dir := InitGitRepository(t, cmds...)
repo := api.RepoName(filepath.Base(dir))
if resp, err := testGitserverClient.RequestRepoUpdate(context.Background(), repo, 0); err != nil {
t.Fatal(err)
} else if resp.Error != "" {
t.Fatal(resp.Error)
}
return repo
}
func AppleTime(t string) string {
ti, _ := time.Parse(time.RFC3339, t)
return ti.Local().Format("200601021504.05")

View File

@ -35,7 +35,7 @@ func TestGetObject(t *testing.T) {
for label, test := range tests {
t.Run(label, func(t *testing.T) {
obj, err := gitserver.NewTestClient(http.DefaultClient, database.NewMockDB(), gitserverAddresses).GetObject(context.Background(), test.repo, test.objectName)
obj, err := gitserver.NewTestClient(http.DefaultClient, database.NewMockDB(), GitserverAddresses).GetObject(context.Background(), test.repo, test.objectName)
if err != nil {
t.Fatal(err)
}

View File

@ -0,0 +1,121 @@
package inttests
import (
"context"
"net"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
sglog "github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/server"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/httpcli"
"github.com/sourcegraph/sourcegraph/internal/observation"
"golang.org/x/sync/semaphore"
)
var root string
// This is a default gitserver test client currently used for RequestRepoUpdate
// gitserver calls during invocation of MakeGitRepository function
var (
testGitserverClient gitserver.Client
GitserverAddresses []string
)
func InitGitserver() {
// Ignore users configuration in tests
os.Setenv("GIT_CONFIG_NOSYSTEM", "true")
os.Setenv("HOME", "/dev/null")
logger := sglog.Scoped("gitserver_integration_tests", "")
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
logger.Fatal("listen failed", sglog.Error(err))
}
root, err = os.MkdirTemp("", "test")
if err != nil {
logger.Fatal(err.Error())
}
db := database.NewMockDB()
gr := database.NewMockGitserverRepoStore()
db.GitserverReposFunc.SetDefaultReturn(gr)
srv := &http.Server{
Handler: (&server.Server{
Logger: sglog.Scoped("server", "the gitserver service"),
ObservationCtx: &observation.TestContext,
ReposDir: filepath.Join(root, "repos"),
GetRemoteURLFunc: func(ctx context.Context, name api.RepoName) (string, error) {
return filepath.Join(root, "remotes", string(name)), nil
},
GetVCSSyncer: func(ctx context.Context, name api.RepoName) (server.VCSSyncer, error) {
return &server.GitRepoSyncer{}, nil
},
GlobalBatchLogSemaphore: semaphore.NewWeighted(32),
DB: db,
}).Handler(),
}
go func() {
if err := srv.Serve(l); err != nil {
logger.Fatal(err.Error())
}
}()
serverAddress := l.Addr().String()
testGitserverClient = gitserver.NewTestClient(httpcli.InternalDoer, db, []string{serverAddress})
GitserverAddresses = []string{serverAddress}
}
// MakeGitRepository calls initGitRepository to create a new Git repository and returns a handle to
// it.
func MakeGitRepository(t testing.TB, cmds ...string) api.RepoName {
t.Helper()
dir := InitGitRepository(t, cmds...)
repo := api.RepoName(filepath.Base(dir))
if resp, err := testGitserverClient.RequestRepoUpdate(context.Background(), repo, 0); err != nil {
t.Fatal(err)
} else if resp.Error != "" {
t.Fatal(resp.Error)
}
return repo
}
// InitGitRepository initializes a new Git repository and runs cmds in a new
// temporary directory (returned as dir).
func InitGitRepository(t testing.TB, cmds ...string) string {
t.Helper()
remotes := filepath.Join(root, "remotes")
if err := os.MkdirAll(remotes, 0o700); err != nil {
t.Fatal(err)
}
dir, err := os.MkdirTemp(remotes, strings.ReplaceAll(t.Name(), "/", "__"))
if err != nil {
t.Fatal(err)
}
cmds = append([]string{"git init"}, cmds...)
for _, cmd := range cmds {
out, err := GitCommand(dir, "bash", "-c", cmd).CombinedOutput()
if err != nil {
t.Fatalf("Command %q failed. Output was:\n\n%s", cmd, out)
}
}
return dir
}
func GitCommand(dir, name string, args ...string) *exec.Cmd {
c := exec.Command(name, args...)
c.Dir = dir
c.Env = append(os.Environ(), "GIT_CONFIG="+path.Join(dir, ".git", "config"))
return c
}

View File

@ -11,70 +11,13 @@ import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
"github.com/sourcegraph/sourcegraph/schema"
)
func TestReadDir_SubRepoFiltering(t *testing.T) {
ctx := actor.WithActor(context.Background(), &actor.Actor{
UID: 1,
})
gitCommands := []string{
"touch file1",
"git add file1",
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m commit1 --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
"mkdir app",
"touch app/file2",
"git add app",
"GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05Z git commit -m commit2 --author='a <a@a.com>' --date 2006-01-02T15:04:05Z",
}
repo := MakeGitRepository(t, gitCommands...)
commitID := api.CommitID("b1c725720de2bbd0518731b4a61959797ff345f3")
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
defer conf.Mock(nil)
srpGetter := database.NewMockSubRepoPermsStore()
testSubRepoPerms := map[api.RepoName]authz.SubRepoPermissions{
repo: {
Paths: []string{"/**", "-/app/**"},
},
}
srpGetter.GetByUserFunc.SetDefaultReturn(testSubRepoPerms, nil)
checker, err := authz.NewSubRepoPermsClient(srpGetter)
if err != nil {
t.Fatalf("unexpected error creating sub-repo perms client: %s", err)
}
db := database.NewMockDB()
gr := database.NewMockGitserverRepoStore()
db.GitserverReposFunc.SetDefaultReturn(gr)
client := gitserver.NewTestClient(http.DefaultClient, db, gitserverAddresses)
files, err := client.ReadDir(ctx, checker, repo, commitID, "", false)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
// Because we have a wildcard matcher we still allow directory visibility
assert.Len(t, files, 1)
assert.Equal(t, "file1", files[0].Name())
assert.False(t, files[0].IsDir())
}
func TestRepository_FileSystem(t *testing.T) {
t.Parallel()
ctx := context.Background()
@ -116,7 +59,7 @@ func TestRepository_FileSystem(t *testing.T) {
},
}
client := gitserver.NewTestClient(http.DefaultClient, db, gitserverAddresses)
client := gitserver.NewTestClient(http.DefaultClient, db, GitserverAddresses)
for label, test := range tests {
// notafile should not exist.
if _, err := client.Stat(ctx, authz.DefaultSubRepoPermsChecker, test.repo, test.first, "notafile"); !os.IsNotExist(err) {
@ -142,7 +85,7 @@ func TestRepository_FileSystem(t *testing.T) {
if got, want := "ab771ba54f5571c99ffdae54f44acc7993d9f115", dir1Info.Sys().(gitdomain.ObjectInfo).OID().String(); got != want {
t.Errorf("%s: got dir1 OID %q, want %q", label, got, want)
}
client := gitserver.NewTestClient(http.DefaultClient, db, gitserverAddresses)
client := gitserver.NewTestClient(http.DefaultClient, db, GitserverAddresses)
// dir1 should contain one entry: file1.
dir1Entries, err := client.ReadDir(ctx, authz.DefaultSubRepoPermsChecker, test.repo, test.first, "dir1", false)
@ -311,7 +254,7 @@ func TestRepository_FileSystem_quoteChars(t *testing.T) {
},
}
client := gitserver.NewTestClient(http.DefaultClient, db, gitserverAddresses)
client := gitserver.NewTestClient(http.DefaultClient, db, GitserverAddresses)
for label, test := range tests {
commitID, err := client.ResolveRevision(ctx, test.repo, "master", gitserver.ResolveRevisionOptions{})
if err != nil {
@ -374,7 +317,7 @@ func TestRepository_FileSystem_gitSubmodules(t *testing.T) {
},
}
client := gitserver.NewTestClient(http.DefaultClient, db, gitserverAddresses)
client := gitserver.NewTestClient(http.DefaultClient, db, GitserverAddresses)
for label, test := range tests {
commitID, err := client.ResolveRevision(ctx, test.repo, "master", gitserver.ResolveRevisionOptions{})
if err != nil {

View File

@ -51,7 +51,7 @@ func indexedSymbolsBranch(ctx context.Context, repo *types.MinimalRepo, commit s
return ""
}
func filterZoektResults(ctx context.Context, checker authz.SubRepoPermissionChecker, repo api.RepoName, results []*result.SymbolMatch) ([]*result.SymbolMatch, error) {
func FilterZoektResults(ctx context.Context, checker authz.SubRepoPermissionChecker, repo api.RepoName, results []*result.SymbolMatch) ([]*result.SymbolMatch, error) {
if !authz.SubRepoEnabled(checker) {
return results, nil
}
@ -197,7 +197,7 @@ func Compute(ctx context.Context, checker authz.SubRepoPermissionChecker, repoNa
if err != nil {
return nil, errors.Wrap(err, "zoekt symbol search")
}
results, err = filterZoektResults(ctx, checker, repoName.Name, results)
results, err = FilterZoektResults(ctx, checker, repoName.Name, results)
if err != nil {
return nil, errors.Wrap(err, "checking permissions")
}

View File

@ -8,59 +8,8 @@ import (
"github.com/sourcegraph/sourcegraph/internal/types"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/stretchr/testify/assert"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/search/result"
"github.com/sourcegraph/sourcegraph/schema"
)
func TestFilterZoektResults(t *testing.T) {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
SubRepoPermissions: &schema.SubRepoPermissions{
Enabled: true,
},
},
},
})
t.Cleanup(func() { conf.Mock(nil) })
repoName := api.RepoName("foo")
ctx := context.Background()
ctx = actor.WithActor(ctx, &actor.Actor{
UID: 1,
})
checker, err := authz.NewSimpleChecker(repoName, []string{"/**", "-/*_test.go"})
if err != nil {
t.Fatal(err)
}
results := []*result.SymbolMatch{
{
Symbol: result.Symbol{},
File: &result.File{
Path: "foo.go",
},
},
{
Symbol: result.Symbol{},
File: &result.File{
Path: "foo_test.go",
},
},
}
filtered, err := filterZoektResults(ctx, checker, repoName, results)
if err != nil {
t.Fatal(err)
}
assert.Len(t, filtered, 1)
r := filtered[0]
assert.Equal(t, r.File.Path, "foo.go")
}
func TestSearchZoektDoesntPanicWithNilQuery(t *testing.T) {
// As soon as we reach Streamer.Search function, we can consider test successful,
// that's why we can just mock it.

View File

@ -17,6 +17,7 @@
- CodeMonitorStore
- EnterpriseDB
- PermsStore
- SubRepoPermsStore
- filename: enterprise/internal/insights/discovery/mocks_temp.go
path: github.com/sourcegraph/sourcegraph/enterprise/internal/insights/discovery
interfaces:
@ -32,6 +33,9 @@
path: github.com/sourcegraph/sourcegraph/internal/authz
interfaces:
- SubRepoPermissionChecker
- filename: enterprise/internal/authz/subrepoperms/mocks_temp.go
path: github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms
interfaces:
- SubRepoPermissionsGetter
- filename: internal/database/mocks_temp.go
path: github.com/sourcegraph/sourcegraph/internal/database
@ -57,7 +61,6 @@
- SearchContextsStore
- SecurityEventLogsStore
- SettingsStore
- SubRepoPermsStore
- TemporarySettingsStore
- UserCredentialsStore
- UserEmailsStore

View File

@ -51,6 +51,8 @@ env:
{ "Name": "frontend", "Host": "127.0.0.1:6063" },
{ "Name": "gitserver-0", "Host": "127.0.0.1:3551" },
{ "Name": "gitserver-1", "Host": "127.0.0.1:3552" },
{ "Name": "oss-gitserver-0", "Host": "127.0.0.1:3551" },
{ "Name": "oss-gitserver-1", "Host": "127.0.0.1:3552" },
{ "Name": "searcher", "Host": "127.0.0.1:6069" },
{ "Name": "oss-symbols", "Host": "127.0.0.1:6071" },
{ "Name": "symbols", "Host": "127.0.0.1:6071" },
@ -189,7 +191,7 @@ commands:
if [ -n "$DELVE" ]; then
export GCFLAGS='all=-N -l'
fi
go build -gcflags="$GCFLAGS" -o .bin/gitserver github.com/sourcegraph/sourcegraph/cmd/gitserver
go build -gcflags="$GCFLAGS" -o .bin/gitserver github.com/sourcegraph/sourcegraph/enterprise/cmd/gitserver
checkBinary: .bin/gitserver
env: &gitserverenv
HOSTNAME: 127.0.0.1:3178
@ -197,6 +199,41 @@ commands:
- lib
- internal
- cmd/gitserver
- enterprise/internal
- enterprise/cmd/gitserver
oss-gitserver-template: &oss_gitserver_template
cmd: .bin/oss-gitserver
install: |
if [ -n "$DELVE" ]; then
export GCFLAGS='all=-N -l'
fi
go build -gcflags="$GCFLAGS" -o .bin/oss-gitserver github.com/sourcegraph/sourcegraph/cmd/gitserver
checkBinary: .bin/oss-gitserver
env: &oss_gitserverenv
HOSTNAME: 127.0.0.1:3178
watch:
- lib
- internal
- cmd/gitserver
oss-gitserver-0:
<<: *oss_gitserver_template
env:
<<: *gitserverenv
HOSTNAME: 127.0.0.1:3501
GITSERVER_ADDR: 127.0.0.1:3501
SRC_REPOS_DIR: $HOME/.sourcegraph/repos_1
SRC_PROF_HTTP: 127.0.0.1:3551
oss-gitserver-1:
<<: *oss_gitserver_template
env:
<<: *oss_gitserverenv
HOSTNAME: 127.0.0.1:3502
GITSERVER_ADDR: 127.0.0.1:3502
SRC_REPOS_DIR: $HOME/.sourcegraph/repos_2
SRC_PROF_HTTP: 127.0.0.1:3552
# This is only here to stay backwards-compatible with people's custom
# `sg.config.overwrite.yaml` files
@ -794,8 +831,8 @@ commandsets:
- oss-worker
- oss-repo-updater
- oss-symbols
- gitserver-0
- gitserver-1
- oss-gitserver-0
- oss-gitserver-1
- searcher
- oss-web
- caddy