From 8c7d72209842826cc9d70b1b1c3e417a0524533e Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 9 Jan 2025 09:20:48 -0800 Subject: [PATCH 1/7] feat: Add enable resource provider allowlist option --- pkg/http/types.go | 7 ++++--- pkg/options/server.go | 18 ++++++++++++++---- pkg/options/solver.go | 4 ++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/pkg/http/types.go b/pkg/http/types.go index c50d143f..126165a9 100644 --- a/pkg/http/types.go +++ b/pkg/http/types.go @@ -9,9 +9,10 @@ type ServerOptions struct { } type AccessControlOptions struct { - ValidationTokenSecret string - ValidationTokenExpiration int - ValidationTokenKid string + EnableResourceProviderAllowlist bool + ValidationTokenSecret string + ValidationTokenExpiration int + ValidationTokenKid string } type ValidationToken struct { diff --git a/pkg/options/server.go b/pkg/options/server.go index 29b1776d..84ab8d23 100644 --- a/pkg/options/server.go +++ b/pkg/options/server.go @@ -19,9 +19,11 @@ func GetDefaultServerOptions() http.ServerOptions { func GetDefaultAccessControlOptions() http.AccessControlOptions { return http.AccessControlOptions{ - ValidationTokenSecret: GetDefaultServeOptionString("SERVER_VALIDATION_TOKEN_SECRET", ""), - ValidationTokenExpiration: GetDefaultServeOptionInt("SERVER_VALIDATION_TOKEN_EXPIRATION", 604800), // one week - ValidationTokenKid: GetDefaultServeOptionString("SERVER_VALIDATION_TOKEN_KID", ""), + // When false, any resource provider may post a resource offer. + EnableResourceProviderAllowlist: GetDefaultServeOptionBool("SERVER_ENABLE_RESOURCE_PROVIDER_ALLOWLIST", false), + ValidationTokenSecret: GetDefaultServeOptionString("SERVER_VALIDATION_TOKEN_SECRET", ""), + ValidationTokenExpiration: GetDefaultServeOptionInt("SERVER_VALIDATION_TOKEN_EXPIRATION", 604800), // one week + ValidationTokenKid: GetDefaultServeOptionString("SERVER_VALIDATION_TOKEN_KID", ""), } } @@ -45,6 +47,11 @@ func AddServerCliFlags(cmd *cobra.Command, serverOptions *http.ServerOptions) { &serverOptions.Port, "server-port", serverOptions.Port, `The port to bind the api server to (SERVER_PORT).`, ) + cmd.PersistentFlags().BoolVar( + &serverOptions.AccessControl.EnableResourceProviderAllowlist, "server-enable-resource-provider-allowlist", + serverOptions.AccessControl.EnableResourceProviderAllowlist, + `Enable resource provider allowlist (SERVER_ENABLE_RESOURCE_PROVIDER_ALLOWLIST).`, + ) cmd.PersistentFlags().StringVar( &serverOptions.AccessControl.ValidationTokenSecret, "server-validation-token-secret", serverOptions.AccessControl.ValidationTokenSecret, @@ -70,10 +77,13 @@ func AddServerCliFlags(cmd *cobra.Command, serverOptions *http.ServerOptions) { ) } -func CheckServerOptions(options http.ServerOptions) error { +func CheckServerOptions(options http.ServerOptions, storeType string) error { if options.URL == "" { return fmt.Errorf("SERVER_URL is required") } + if options.AccessControl.EnableResourceProviderAllowlist && storeType == "memory" { + return fmt.Errorf("Enabling the resource provider allowlist requires the database store. Set STORE_TYPE to \"database\".") + } if options.AccessControl.ValidationTokenSecret == "" { return fmt.Errorf("SERVER_VALIDATION_TOKEN_SECRET is required") } diff --git a/pkg/options/solver.go b/pkg/options/solver.go index ae16452b..8d91cec0 100644 --- a/pkg/options/solver.go +++ b/pkg/options/solver.go @@ -29,11 +29,11 @@ func AddSolverCliFlags(cmd *cobra.Command, options *solver.SolverOptions) { } func CheckSolverOptions(options solver.SolverOptions) error { - err := CheckServerOptions(options.Server) + err := CheckStoreOptions(options.Store) if err != nil { return err } - err = CheckStoreOptions(options.Store) + err = CheckServerOptions(options.Server, options.Store.Type) if err != nil { return err } From 8978d7cb48e829065c2a203dd8486237af526aa3 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 9 Jan 2025 09:32:36 -0800 Subject: [PATCH 2/7] feat: Add AllowedResourceProvider model --- pkg/solver/store/db/db.go | 1 + pkg/solver/store/db/models.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/pkg/solver/store/db/db.go b/pkg/solver/store/db/db.go index 9d8aab99..4c8aa460 100644 --- a/pkg/solver/store/db/db.go +++ b/pkg/solver/store/db/db.go @@ -43,6 +43,7 @@ func NewSolverStoreDatabase(connStr string, gormLogLevel string) (*SolverStoreDa db.AutoMigrate(&Deal{}) db.AutoMigrate(&Result{}) db.AutoMigrate(&MatchDecision{}) + db.AutoMigrate(&AllowedResourceProvider{}) return &SolverStoreDatabase{db}, nil } diff --git a/pkg/solver/store/db/models.go b/pkg/solver/store/db/models.go index 1c405622..3586265c 100644 --- a/pkg/solver/store/db/models.go +++ b/pkg/solver/store/db/models.go @@ -47,3 +47,8 @@ type MatchDecision struct { JobOffer string `gorm:"primaryKey"` Attributes datatypes.JSONType[data.MatchDecision] } + +type AllowedResourceProvider struct { + gorm.Model + ResourceProvider string `gorm:"index"` +} From 20e6a642e188a148333383de8d99de75b99815f7 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 9 Jan 2025 15:06:23 -0800 Subject: [PATCH 3/7] feat: Add allowed resource provider store methods --- pkg/solver/store/store.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/solver/store/store.go b/pkg/solver/store/store.go index 61f86045..8cd15bd6 100644 --- a/pkg/solver/store/store.go +++ b/pkg/solver/store/store.go @@ -61,12 +61,14 @@ type SolverStore interface { AddDeal(deal data.DealContainer) (*data.DealContainer, error) AddResult(result data.Result) (*data.Result, error) AddMatchDecision(resourceOffer string, jobOffer string, deal string, result bool) (*data.MatchDecision, error) + AddAllowedResourceProvider(resourceProvider string) (string, error) GetJobOffers(query GetJobOffersQuery) ([]data.JobOfferContainer, error) GetResourceOffers(query GetResourceOffersQuery) ([]data.ResourceOfferContainer, error) GetDeals(query GetDealsQuery) ([]data.DealContainer, error) GetDealsAll() ([]data.DealContainer, error) GetResults() ([]data.Result, error) GetMatchDecisions() ([]data.MatchDecision, error) + GetAllowedResourceProviders() ([]string, error) GetJobOffer(id string) (*data.JobOfferContainer, error) GetResourceOffer(id string) (*data.ResourceOfferContainer, error) GetResourceOfferByAddress(address string) (*data.ResourceOfferContainer, error) @@ -85,6 +87,7 @@ type SolverStore interface { RemoveDeal(id string) error RemoveResult(id string) error RemoveMatchDecision(resourceOffer string, jobOffer string) error + RemoveAllowedResourceProvider(resourceProvider string) error } func GetMatchID(resourceOffer string, jobOffer string) string { From bac35dfdb75b93b92054b08497042da822eb2cf5 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 9 Jan 2025 15:07:14 -0800 Subject: [PATCH 4/7] test: Add allowed resource provider tests --- pkg/solver/store/store_test.go | 112 ++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/pkg/solver/store/store_test.go b/pkg/solver/store/store_test.go index e06bdaea..4b2f7788 100644 --- a/pkg/solver/store/store_test.go +++ b/pkg/solver/store/store_test.go @@ -1153,6 +1153,67 @@ func TestMatchDecisionRemove(t *testing.T) { } } +func TestAllowedResourceProviderOps(t *testing.T) { + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + t.Run(config.name, func(t *testing.T) { + getStore, clearStore := config.init() + store := getStore() + defer clearStore() + + // Generate multiple addresses + addresses := generateAllowedResourceProviders(5, 50) + + // Add addresses + for _, addr := range addresses { + added, err := store.AddAllowedResourceProvider(addr) + if err != nil { + t.Fatalf("Failed to add allowed resource provider: %v", err) + } + if added != addr { + t.Errorf("Expected address %s, got %s", addr, added) + } + } + + // Get all addresses + retrieved, err := store.GetAllowedResourceProviders() + if err != nil { + t.Fatalf("Failed to get allowed resource providers: %v", err) + } + + // Sort both slices for comparison + sort.Strings(addresses) + sort.Strings(retrieved) + + if !slices.Equal(retrieved, addresses) { + t.Errorf("Retrieved addresses don't match added addresses.\nAdded: %v\nRetrieved: %v", + addresses, retrieved) + } + + // Remove addresses and verify removal + for _, addr := range addresses { + err := store.RemoveAllowedResourceProvider(addr) + if err != nil { + t.Fatalf("Failed to remove allowed resource provider: %v", err) + } + + // Get updated list + remaining, err := store.GetAllowedResourceProviders() + if err != nil { + t.Fatalf("Failed to get allowed resource providers after removal: %v", err) + } + + // Verify address was removed + for _, remainingAddr := range remaining { + if remainingAddr == addr { + t.Errorf("Address %s still exists after removal", addr) + } + } + } + }) + } +} + // Concurrency for all func TestConcurrentOps(t *testing.T) { @@ -1161,6 +1222,7 @@ func TestConcurrentOps(t *testing.T) { deals := generateDeals(4, 10) results := generateResults(4, 10) matchDecisions := generateMatchDecisions(4, 10) + allowedResourceProviders := generateAllowedResourceProviders(4, 10) storeConfigs := setupStores(t) for _, config := range storeConfigs { @@ -1170,7 +1232,7 @@ func TestConcurrentOps(t *testing.T) { store := getStore() defer clearStore() - count := len(jobOffers) + len(resourceOffers) + len(deals) + len(results) + len(matchDecisions) + count := len(jobOffers) + len(resourceOffers) + len(deals) + len(results) + len(matchDecisions) + len(allowedResourceProviders) errCh := make(chan error, count) var wg sync.WaitGroup @@ -1234,6 +1296,18 @@ func TestConcurrentOps(t *testing.T) { }(decision) } + // Add allowed resource providers concurrently + for _, provider := range allowedResourceProviders { + wg.Add(1) + go func(p string) { + defer wg.Done() + _, err := store.AddAllowedResourceProvider(p) + if err != nil { + errCh <- fmt.Errorf("allowed resource provider error: %v", err) + } + }(provider) + } + wg.Wait() close(errCh) @@ -1336,6 +1410,18 @@ func TestConcurrentOps(t *testing.T) { } } + // Verify all allowed resource providers were added + retrieved, err := store.GetAllowedResourceProviders() + if err != nil { + t.Errorf("Failed to get allowed resource providers: %v", err) + } + sort.Strings(allowedResourceProviders) + sort.Strings(retrieved) + + if !slices.Equal(retrieved, allowedResourceProviders) { + t.Errorf("Retrieved allowed providers don't match added providers.\nAdded: %v\nRetrieved: %v", + allowedResourceProviders, retrieved) + } }) } } @@ -1456,6 +1542,19 @@ func clearStoreDatabase(t *testing.T, s store.SolverStore) { t.Fatalf("Failed to remove existing match decision: %v", err) } } + + // Delete allowed resource providers + providers, err := s.GetAllowedResourceProviders() + if err != nil { + t.Fatalf("Failed to get existing allowed resource providers: %v", err) + } + + for _, provider := range providers { + err := s.RemoveAllowedResourceProvider(provider) + if err != nil { + t.Fatalf("Failed to remove existing allowed resource provider: %v", err) + } + } } // Generators @@ -1586,3 +1685,14 @@ func generateMatchDecisions(min int, max int) []data.MatchDecision { return decisions } + +func generateAllowedResourceProviders(min int, max int) []string { + count := min + rand.Intn(max-min+1) + providers := make([]string, count) + + for i := 0; i < count; i++ { + providers[i] = generateEthAddress() + } + + return providers +} From 20f82623236bf6b844dff25ec5d42458fe6b4f37 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 9 Jan 2025 15:07:52 -0800 Subject: [PATCH 5/7] feat: Add allowed resource provider memory implementations --- pkg/solver/store/memory/store.go | 51 +++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/pkg/solver/store/memory/store.go b/pkg/solver/store/memory/store.go index bc83ce3b..85b2bdc6 100644 --- a/pkg/solver/store/memory/store.go +++ b/pkg/solver/store/memory/store.go @@ -10,21 +10,23 @@ import ( ) type SolverStoreMemory struct { - jobOfferMap map[string]*data.JobOfferContainer - resourceOfferMap map[string]*data.ResourceOfferContainer - dealMap map[string]*data.DealContainer - resultMap map[string]*data.Result - matchDecisionMap map[string]*data.MatchDecision - mutex sync.RWMutex + jobOfferMap map[string]*data.JobOfferContainer + resourceOfferMap map[string]*data.ResourceOfferContainer + dealMap map[string]*data.DealContainer + resultMap map[string]*data.Result + matchDecisionMap map[string]*data.MatchDecision + allowedResourceProviderMap map[string]string + mutex sync.RWMutex } func NewSolverStoreMemory() (*SolverStoreMemory, error) { return &SolverStoreMemory{ - jobOfferMap: map[string]*data.JobOfferContainer{}, - resourceOfferMap: map[string]*data.ResourceOfferContainer{}, - dealMap: map[string]*data.DealContainer{}, - resultMap: map[string]*data.Result{}, - matchDecisionMap: map[string]*data.MatchDecision{}, + jobOfferMap: map[string]*data.JobOfferContainer{}, + resourceOfferMap: map[string]*data.ResourceOfferContainer{}, + dealMap: map[string]*data.DealContainer{}, + resultMap: map[string]*data.Result{}, + matchDecisionMap: map[string]*data.MatchDecision{}, + allowedResourceProviderMap: map[string]string{}, }, nil } @@ -79,6 +81,14 @@ func (s *SolverStoreMemory) AddMatchDecision(resourceOffer string, jobOffer stri return decision, nil } +func (store *SolverStoreMemory) AddAllowedResourceProvider(resourceProvider string) (string, error) { + store.mutex.Lock() + defer store.mutex.Unlock() + store.allowedResourceProviderMap[resourceProvider] = resourceProvider + + return resourceProvider, nil +} + func (s *SolverStoreMemory) GetJobOffers(query store.GetJobOffersQuery) ([]data.JobOfferContainer, error) { s.mutex.RLock() defer s.mutex.RUnlock() @@ -190,6 +200,18 @@ func (s *SolverStoreMemory) GetMatchDecisions() ([]data.MatchDecision, error) { return results, nil } +func (store *SolverStoreMemory) GetAllowedResourceProviders() ([]string, error) { + store.mutex.RLock() + defer store.mutex.RUnlock() + + providers := []string{} + for provider := range store.allowedResourceProviderMap { + providers = append(providers, provider) + } + + return providers, nil +} + func (s *SolverStoreMemory) GetJobOffer(id string) (*data.JobOfferContainer, error) { s.mutex.RLock() defer s.mutex.RUnlock() @@ -414,6 +436,13 @@ func (s *SolverStoreMemory) RemoveMatchDecision(resourceOffer string, jobOffer s return nil } +func (store *SolverStoreMemory) RemoveAllowedResourceProvider(resourceProvider string) error { + store.mutex.Lock() + defer store.mutex.Unlock() + delete(store.allowedResourceProviderMap, resourceProvider) + return nil +} + // Strictly speaking, the compiler will check the interface // implementation without this check. But some code editors // report errors more effectively when we have it. From 0c69edf2ed7ae6184f28c87209c938838c06c413 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 9 Jan 2025 15:08:38 -0800 Subject: [PATCH 6/7] feat: Add allowed resource provider database implementations --- pkg/solver/store/db/db.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pkg/solver/store/db/db.go b/pkg/solver/store/db/db.go index 4c8aa460..633dd60b 100644 --- a/pkg/solver/store/db/db.go +++ b/pkg/solver/store/db/db.go @@ -136,6 +136,19 @@ func (store *SolverStoreDatabase) AddMatchDecision(resourceOffer string, jobOffe return decision, nil } +func (store *SolverStoreDatabase) AddAllowedResourceProvider(resourceProvider string) (string, error) { + record := AllowedResourceProvider{ + ResourceProvider: resourceProvider, + } + + result := store.db.Create(&record) + if result.Error != nil { + return "", result.Error + } + + return resourceProvider, nil +} + func (store *SolverStoreDatabase) GetJobOffers(query store.GetJobOffersQuery) ([]data.JobOfferContainer, error) { q := store.db.Where([]JobOffer{}) @@ -269,6 +282,20 @@ func (store *SolverStoreDatabase) GetMatchDecisions() ([]data.MatchDecision, err return decisions, nil } +func (store *SolverStoreDatabase) GetAllowedResourceProviders() ([]string, error) { + var records []AllowedResourceProvider + if err := store.db.Find(&records).Error; err != nil { + return nil, err + } + + providers := make([]string, len(records)) + for i, record := range records { + providers[i] = record.ResourceProvider + } + + return providers, nil +} + func (store *SolverStoreDatabase) GetJobOffer(id string) (*data.JobOfferContainer, error) { // Offers are unique by CID, so we can query first var record JobOffer @@ -616,6 +643,14 @@ func (store *SolverStoreDatabase) RemoveMatchDecision(resourceOffer string, jobO return nil } +func (store *SolverStoreDatabase) RemoveAllowedResourceProvider(resourceProvider string) error { + result := store.db.Where("resource_provider = ?", resourceProvider).Delete(&AllowedResourceProvider{}) + if result.Error != nil { + return result.Error + } + return nil +} + // Strictly speaking, the compiler will check the interface // implementation without this check. But some code editors // report errors more effectively when we have it. From 873428b1f7a60c85b169bf6084d2739b8d11ac75 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 9 Jan 2025 15:10:30 -0800 Subject: [PATCH 7/7] feat: Check resource provider when allowlist enabled --- pkg/solver/server.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/solver/server.go b/pkg/solver/server.go index 37887542..b57e5a99 100644 --- a/pkg/solver/server.go +++ b/pkg/solver/server.go @@ -9,6 +9,7 @@ import ( corehttp "net/http" "os" "path/filepath" + "slices" "strings" "time" @@ -312,6 +313,21 @@ func (solverServer *solverServer) addResourceOffer(resourceOffer data.ResourceOf if signerAddress != resourceOffer.ResourceProvider { return nil, fmt.Errorf("resource provider address does not match signer address") } + + // Resource provider must be in allowlist when enabled + if solverServer.options.AccessControl.EnableResourceProviderAllowlist { + allowedProviders, err := solverServer.store.GetAllowedResourceProviders() + if err != nil { + log.Error().Err(err).Msgf("Unable to load resource provider allowlist: %s", err) + return nil, err + } + + if !slices.Contains(allowedProviders, resourceOffer.ResourceProvider) { + log.Debug().Msgf("resource provider not in allowlist %s", resourceOffer.ResourceProvider) + return nil, errors.New("resource provider not in beta program, request beta program access here: https://forms.gle/XaE3rRuXVLxTnZto7") + } + } + err = data.CheckResourceOffer(resourceOffer) if err != nil { log.Error().Err(err).Msgf("Error checking resource offer")